From a2a497e35c04cfe60bff20a359c16ca0f6886cb4 Mon Sep 17 00:00:00 2001 From: Dmitry Iv Date: Thu, 30 May 2024 16:51:13 -0400 Subject: [PATCH] Factor out editarea --- index.html | 4 +- main.css | 14 ++-- src/{audio-utils.js => audio-util.js} | 26 +++---- src/util.js | 102 +++++++++++++++++++++++++ src/wavearea.js | 106 +------------------------- src/worker.js | 25 +++--- 6 files changed, 138 insertions(+), 139 deletions(-) rename src/{audio-utils.js => audio-util.js} (90%) create mode 100644 src/util.js 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' - - -