diff --git a/index.html b/index.html
index f4c665d..b51b503 100644
--- a/index.html
+++ b/index.html
@@ -62,10 +62,10 @@
Record
-->
-
+
v.setUint8(pos++, x);
const u16 = (x) => (v.setUint16(pos, x, true), pos += 2)
const u32 = (x) => (v.setUint32(pos, x, true), pos += 4)
- const string = (s) => { for (var i = 0; i < s.length; ++i) u8(s.charCodeAt(i));}
+ const string = (s) => { for (var i = 0; i < s.length; ++i) u8(s.charCodeAt(i)); }
string("RIFF");
u32(buffer.byteLength - 8);
string("WAVE");
@@ -53,8 +53,8 @@ export async function encodeAudio (...audioBuffers) {
let output = new Float32Array(buffer, pos);
for (let audioBuffer of audioBuffers) {
let channels = audioBuffer.numberOfChannels,
- channelData = Array(channels),
- length = audioBuffer.length
+ channelData = Array(channels),
+ length = audioBuffer.length
for (let ch = 0; ch < channels; ++ch) channelData[ch] = audioBuffer.getChannelData(ch)
for (let i = 0; i < length; ++i)
for (let ch = 0; ch < channels; ++ch) output[pos++] = channelData[ch][i];
@@ -65,7 +65,7 @@ export async function encodeAudio (...audioBuffers) {
}
// convert audio buffer to waveform string
-export function drawAudio (audioBuffer) {
+export function drawAudio(audioBuffer) {
if (!audioBuffer) return '';
// if waveform is rendered already - return cached
@@ -94,7 +94,7 @@ export function drawAudio (audioBuffer) {
// rms method
// drawback: waveform is smaller than needed
- for (;i < nextBlock; i++) {
+ for (; i < nextBlock; i++) {
let x = i >= channelData.length ? 0 : channelData[i]
sum += x
ssum += x ** 2
@@ -103,7 +103,7 @@ export function drawAudio (audioBuffer) {
}
const avg = sum / BLOCK_SIZE
const rms = Math.sqrt(ssum / BLOCK_SIZE)
- let v = Math.min(100, Math.ceil(rms * RANGE * VISUAL_AMP / (max-min))) || 0
+ let v = Math.min(100, Math.ceil(rms * RANGE * VISUAL_AMP / (max - min))) || 0
str += String.fromCharCode(0x0100 + v)
let shift = Math.abs(Math.round(avg * RANGE / 2))
@@ -128,7 +128,7 @@ export function drawAudio (audioBuffer) {
return str
}
-export function sliceAudio (buffer, start=0, end=buffer.length) {
+export function sliceAudio(buffer, start = 0, end = buffer.length) {
let newBuffer = new AudioBuffer({
length: end - start,
numberOfChannels: buffer.numberOfChannels,
@@ -166,7 +166,7 @@ export function joinAudio(a, b) {
return newBuffer
}
-export function deleteAudio(buffer, start=0, end=buffer.length) {
+export function deleteAudio(buffer, start = 0, end = buffer.length) {
let newBuffer = new AudioBuffer({
length: buffer.length - Math.abs(end - start),
numberOfChannels: buffer.numberOfChannels,
@@ -183,7 +183,7 @@ export function deleteAudio(buffer, start=0, end=buffer.length) {
return newBuffer
}
-export function insertAudio (a, offset, b) {
+export function insertAudio(a, offset, b) {
if (offset >= a.length) return joinAudio(a, b)
if (!offset) return joinAudio(b, a)
@@ -210,14 +210,14 @@ export function insertAudio (a, offset, b) {
return buffer
}
-export function cloneAudio (a) {
- let b = new AudioBuffer({sampleRate: a.sampleRate, numberOfChannels: a.numberOfChannels, length: a.length})
+export function cloneAudio(a) {
+ let b = new AudioBuffer({ sampleRate: a.sampleRate, numberOfChannels: a.numberOfChannels, length: a.length })
for (let ch = 0; ch < a.numberOfChannels; ch++) b.getChannelData(ch).set(a.getChannelData(ch))
return b
}
export const fileToArrayBuffer = (file) => {
- return new Promise((y,n) => {
+ return new Promise((y, n) => {
const reader = new FileReader();
reader.addEventListener('loadend', (event) => {
y(event.target.result);
diff --git a/src/util.js b/src/util.js
new file mode 100644
index 0000000..385a793
--- /dev/null
+++ b/src/util.js
@@ -0,0 +1,102 @@
+
+export const selection = {
+ // get normalized selection
+ get() {
+ let s = window.getSelection()
+
+ // return unknown selection
+ if (!s.anchorNode || !s.anchorNode.parentNode.closest('.w-editarea')) return
+
+ // collect start/end offsets
+ let start = absOffset(s.anchorNode, s.anchorOffset), end = absOffset(s.focusNode, s.focusOffset)
+
+ // swap selection direction
+ let startNode = s.anchorNode.parentNode.closest('.w-segment'), startNodeOffset = s.anchorOffset,
+ endNode = s.focusNode.parentNode.closest('.w-segment'), endNodeOffset = s.focusOffset;
+ if (start > end) {
+ [end, endNode, endNodeOffset, start, startNode, startNodeOffset] =
+ [start, startNode, startNodeOffset, end, endNode, endNodeOffset]
+ }
+
+ return {
+ start,
+ startNode,
+ startNodeOffset,
+ end,
+ endNode,
+ endNodeOffset,
+ collapsed: s.isCollapsed,
+ range: s.getRangeAt(0)
+ }
+ },
+
+ /**
+ * Set normalized selection
+ * @param {number | Array} start – absolute offset (excluding modifier chars) or relative offset [node, offset]
+ * @param {number | Array} end – absolute offset (excluding modifier chars) or relative offset [node, offset]
+ * @returns {start, , end}
+ */
+ set(start, end) {
+ let s = window.getSelection()
+
+ if (Array.isArray(start)) start = absOffset(...start)
+ if (Array.isArray(end)) end = absOffset(...end)
+
+ // start/end must be within limits
+ start = Math.max(0, start)
+ if (end == null) end = start
+
+ // find start/end nodes
+ let editarea = document.querySelector('.w-editarea')
+ let [startNode, startNodeOffset] = relOffset(editarea, start)
+ let [endNode, endNodeOffset] = relOffset(editarea, end)
+
+ let currentRange = s.getRangeAt(0)
+ if (
+ !(currentRange.startContainer === startNode.firstChild && currentRange.startOffset === startNodeOffset) &&
+ !(currentRange.endContainer === endNode.firstChild && currentRange.endOffset === endNodeOffset)
+ ) {
+ // NOTE: Safari doesn't support reusing range
+ s.removeAllRanges()
+ let range = new Range()
+ range.setStart(startNode.firstChild, startNodeOffset)
+ range.setEnd(endNode.firstChild, endNodeOffset)
+ s.addRange(range)
+ }
+
+ return {
+ start, startNode, end, endNode,
+ startNodeOffset, endNodeOffset,
+ collapsed: s.isCollapsed,
+ range: s.getRangeAt(0)
+ }
+ }
+}
+
+// calculate absolute offset from relative pair
+function absOffset(node, relOffset) {
+ let prevNode = node.parentNode.closest('.w-segment')
+ let offset = cleanText(prevNode.textContent.slice(0, relOffset)).length
+ while (prevNode = prevNode.previousSibling) offset += cleanText(prevNode.textContent).length
+ return offset
+}
+
+// calculate node and relative offset from absolute offset
+function relOffset(editarea, offset) {
+ let node = editarea.firstChild, len
+ // discount previous nodes
+ while (offset > (len = cleanText(node.textContent).length)) {
+ offset -= len, node = node.nextSibling
+ }
+ // convert current node to relative offset
+ let skip = 0
+ for (let content = node.textContent, i = 0; i < offset; i++) {
+ while (content[i + skip] >= '\u0300') skip++
+ }
+ return [node, offset + skip]
+}
+
+// return clean from modifiers text
+export function cleanText(str) {
+ return str.replace(/\u0300|\u0301/g, '')
+}
diff --git a/src/wavearea.js b/src/wavearea.js
index d4f19f2..6a0e926 100644
--- a/src/wavearea.js
+++ b/src/wavearea.js
@@ -2,9 +2,10 @@
// handles user interactions and sends commands to worker
// all the data is stored and processed in worker
import sprae from 'sprae';
-import { fileToArrayBuffer } from './audio-utils';
+import { fileToArrayBuffer } from './audio-util';
import playClip from './play-loop';
import { measureLatency } from './measure-latency';
+import { selection, cleanText } from './util';
history.scrollRestoration = 'manual'
@@ -12,7 +13,7 @@ history.scrollRestoration = 'manual'
// refs
const wavearea = document.querySelector('.wavearea')
-const editarea = wavearea.querySelector('.w-editable')
+const editarea = wavearea.querySelector('.w-editarea')
const timecodes = wavearea.querySelector('.w-timecodes')
const playButton = wavearea.querySelector('.w-play')
const waveform = wavearea.querySelector('.w-waveform')
@@ -345,102 +346,6 @@ wavearea.addEventListener('touchstart', whatsLatency)
wavearea.addEventListener('mousedown', whatsLatency)
wavearea.addEventListener('keydown', whatsLatency)
-// get normalized selection
-/**
- *
- * @param {number | Array} start – absolute offset (excluding modifier chars) or relative offset [node, offset]
- * @param {number | Array} end – absolute offset (excluding modifier chars) or relative offset [node, offset]
- * @returns {start, , end}
- */
-const selection = {
- get() {
- let s = window.getSelection()
-
- // return unknown selection
- if (!s.anchorNode || !editarea.contains(s.anchorNode)) return
-
- // collect start/end offsets
- let start = absOffset(s.anchorNode, s.anchorOffset), end = absOffset(s.focusNode, s.focusOffset)
-
- // swap selection direction
- let startNode = s.anchorNode.parentNode.closest('.w-segment'), startNodeOffset = s.anchorOffset,
- endNode = s.focusNode.parentNode.closest('.w-segment'), endNodeOffset = s.focusOffset;
- if (start > end) {
- [end, endNode, endNodeOffset, start, startNode, startNodeOffset] =
- [start, startNode, startNodeOffset, end, endNode, endNodeOffset]
- }
-
- return {
- start,
- startNode,
- startNodeOffset,
- end,
- endNode,
- endNodeOffset,
- collapsed: s.isCollapsed,
- range: s.getRangeAt(0)
- }
- },
-
- set(start, end) {
- let s = window.getSelection()
-
- if (Array.isArray(start)) start = absOffset(...start)
- if (Array.isArray(end)) end = absOffset(...end)
-
- // start/end must be within limits
- start = Math.max(0, start)
- if (end == null) end = start
-
- // find start/end nodes
- let [startNode, startNodeOffset] = relOffset(start)
- let [endNode, endNodeOffset] = relOffset(end)
-
- let currentRange = s.getRangeAt(0)
- if (
- !(currentRange.startContainer === startNode.firstChild && currentRange.startOffset === startNodeOffset) &&
- !(currentRange.endContainer === endNode.firstChild && currentRange.endOffset === endNodeOffset)
- ) {
- // NOTE: Safari doesn't support reusing range
- s.removeAllRanges()
- let range = new Range()
- range.setStart(startNode.firstChild, startNodeOffset)
- range.setEnd(endNode.firstChild, endNodeOffset)
- s.addRange(range)
- }
-
- return {
- start, startNode, end, endNode,
- startNodeOffset, endNodeOffset,
- collapsed: s.isCollapsed,
- range: s.getRangeAt(0)
- }
- }
-}
-
-// calculate absolute offset from relative pair
-function absOffset(node, relOffset) {
- let prevNode = node.parentNode.closest('.w-segment')
- let offset = cleanText(prevNode.textContent.slice(0, relOffset)).length
- while (prevNode = prevNode.previousSibling) offset += cleanText(prevNode.textContent).length
- return offset
-}
-
-// calculate node and relative offset from absolute offset
-function relOffset(offset) {
- let node = editarea.firstChild, len
- // discount previous nodes
- while (offset > (len = cleanText(node.textContent).length)) {
- offset -= len, node = node.nextSibling
- }
- // convert current node to relative offset
- let skip = 0
- for (let content = node.textContent, i = 0; i < offset; i++) {
- while (content[i + skip] >= '\u0300') skip++
- }
- return [node, offset + skip]
-}
-
// produce display time from frames
function timecode(block, ms = 0) {
let time = ((block / state?.total)) * state?.duration || 0
@@ -523,11 +428,6 @@ function runOp(...ops) {
})
}
-// return clean from modifiers text
-function cleanText(str) {
- return str.replace(/\u0300|\u0301/g, '')
-}
-
// update audio url & assert waveform
function renderAudio({ url, segments, duration, offsets }) {
// assert waveform same as current content (must be!)
diff --git a/src/worker.js b/src/worker.js
index 1a255e8..4019d72 100644
--- a/src/worker.js
+++ b/src/worker.js
@@ -1,19 +1,19 @@
// main audio processing API / backend
import { BLOCK_SIZE, SAMPLE_RATE } from "./const.js";
-import { fetchAudio, cloneAudio, drawAudio, encodeAudio, sliceAudio, fileToArrayBuffer } from "./audio-utils.js";
+import { fetchAudio, cloneAudio, drawAudio, encodeAudio, sliceAudio, fileToArrayBuffer } from "./audio-util.js";
import decodeAudio from 'audio-decode'
import AudioBuffer from "audio-buffer";
import storage from 'kv-storage-polyfill';
// shim worker for Safari
if (!globalThis.Worker) {
- let {default: Worker} = await import('pseudo-worker')
+ let { default: Worker } = await import('pseudo-worker')
globalThis.Worker = Worker
}
// ops worker - schedules message processing with debounced update
self.onmessage = async e => {
- let {id, ops} = e.data, resultBuffers
+ let { id, ops } = e.data, resultBuffers
// revert history if needed
while (id < history.length) history.pop()()
@@ -31,11 +31,11 @@ self.onmessage = async e => {
// render waveform & audio, post to client
const renderAudio = async (buffers) => {
let segments = buffers.map(buffer => drawAudio(buffer))
- let duration = buffers.reduce((total, {duration}) => total + duration, 0)
+ let duration = buffers.reduce((total, { duration }) => total + duration, 0)
let wavBuffer = await encodeAudio(...buffers);
- let blob = new Blob([wavBuffer], {type:'audio/wav'});
- let url = URL.createObjectURL( blob );
- self.postMessage({id: history.length, url, segments, duration});
+ let blob = new Blob([wavBuffer], { type: 'audio/wav' });
+ let url = URL.createObjectURL(blob);
+ self.postMessage({ id: history.length, url, segments, duration });
}
@@ -48,7 +48,7 @@ let buffers = []
// dict of operations - supposed to update history & current buffers
const Ops = {
// load/decode file from url
- async src (...urls) {
+ async src(...urls) {
history.push(() => buffers = [])
buffers = await Promise.all(urls.map(fetchAudio))
return buffers
@@ -123,7 +123,7 @@ const Ops = {
for (let j = end[1]; j < endData.length; j++) outData[i] = endData[j], i++
}
- let deleted = buffers.splice(start[0], end[0]-start[0]+1, outBuffer)
+ let deleted = buffers.splice(start[0], end[0] - start[0] + 1, outBuffer)
return buffers
},
@@ -252,11 +252,11 @@ const Ops = {
// return [bufIdx, bufOffset] from absolute offset
const bufferIndex = (blockOffset) => {
let frameOffset = blockOffset * BLOCK_SIZE
- if (frameOffset === 0) return [ 0, 0 ]
+ if (frameOffset === 0) return [0, 0]
var start = 0, end
for (let i = 0; i < buffers.length; i++) {
end = start + buffers[i].length
- if (frameOffset < end) return [ i, frameOffset - start ]
+ if (frameOffset < end) return [i, frameOffset - start]
start = end
}
@@ -267,6 +267,3 @@ const bufferIndex = (blockOffset) => {
}
const DB_KEY = 'wavearea-audio'
-
-
-