From cd0e037a8684461424a230822fd5c18b82576986 Mon Sep 17 00:00:00 2001 From: Dmitry Iv Date: Mon, 20 May 2024 22:52:09 -0400 Subject: [PATCH 1/4] Outline plan --- index.html | 92 +++++++++++++++++++++++-------------------- package-lock.json | 42 +++++++++----------- package.json | 2 +- r&d.md | 7 +++- src/template.html | 96 +++++++++++++++++++++++++++++++++++++++++++++ src/wavearea.js | 99 ++++++++++++++++++++++------------------------- todo.md | 53 ++++++++++++++++++++----- 7 files changed, 261 insertions(+), 130 deletions(-) create mode 100644 src/template.html diff --git a/index.html b/index.html index a5cafcb..b285e2e 100644 --- a/index.html +++ b/index.html @@ -3,51 +3,57 @@ Wavearea - + -
+
-
(!playing && (e.stopImmediatePropagation(), selecting = false, handleCaret(e)))" + :onkeydown.arrow="e=>raf(handleCaret)" + :onselectstart..onselectionchange.document.once="e => (selecting = true, () => (selecting = false, handleCaret()))"> +
- -
...
+
+ ...
- +
-
-

+
+
+
+

+
@@ -108,4 +117,3 @@

Wavearea

- diff --git a/package-lock.json b/package-lock.json index e71ae5e..8a70baa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "audio-decode": "^2.1.0", "kv-storage-polyfill": "^2.0.0", "pseudo-worker": "^1.3.0", - "sprae": "^8.1.2", + "sprae": "^10.1.2", "wavefont": "^3.5.0" }, "devDependencies": { @@ -377,15 +377,6 @@ "resolved": "https://registry.npmjs.org/@eshaz/web-worker/-/web-worker-1.2.0.tgz", "integrity": "sha512-HWobmNKFZ8eARo39vEjkviTIISudgzrTyAqab9pOB/qNfnUPIUUMZv6+eEUUfXnkqWCLS3zreTn9YzaRvO8vIA==" }, - "node_modules/@preact/signals-core": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.5.1.tgz", - "integrity": "sha512-dE6f+WCX5ZUDwXzUIWNMhhglmuLpqJhuy3X3xHrhZYI0Hm2LyQwOu0l9mdPiWrVNsE+Q7txOnJPgtIqHCYoBVA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, "node_modules/@thi.ng/bitstream": { "version": "2.2.16", "resolved": "https://registry.npmjs.org/@thi.ng/bitstream/-/bitstream-2.2.16.tgz", @@ -594,13 +585,18 @@ } }, "node_modules/sprae": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/sprae/-/sprae-8.1.2.tgz", - "integrity": "sha512-XbYZRuGj+AQGVousRp8nE7FsE0jKtQp77c4QeV/g/KG52zk/mH/v/F7EQhEuFnp2jQ9TwqsSpxP3NSY/5ESU+g==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/sprae/-/sprae-10.1.2.tgz", + "integrity": "sha512-2FTx1OhHUPgS8Uyh15hl7jbOlwtu3yDQaDW2fGjoGjBu4PrDQdDEP+K947+5F+RkeolLR+DtEyXYj59IETEpzg==", "dependencies": { - "@preact/signals-core": "^1.5.0" + "ulive": "^1.0.2" } }, + "node_modules/ulive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ulive/-/ulive-1.0.2.tgz", + "integrity": "sha512-ejP7iKTzxtrhxQVdyfchH4rvZU5KRK0kT1ecEQ7oo82XZ1IUHWUrTdV2BGDylcZJcgh7j7f63FR8Y8j/xe815g==" + }, "node_modules/wavefont": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/wavefont/-/wavefont-3.5.0.tgz", @@ -767,11 +763,6 @@ "resolved": "https://registry.npmjs.org/@eshaz/web-worker/-/web-worker-1.2.0.tgz", "integrity": "sha512-HWobmNKFZ8eARo39vEjkviTIISudgzrTyAqab9pOB/qNfnUPIUUMZv6+eEUUfXnkqWCLS3zreTn9YzaRvO8vIA==" }, - "@preact/signals-core": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.5.1.tgz", - "integrity": "sha512-dE6f+WCX5ZUDwXzUIWNMhhglmuLpqJhuy3X3xHrhZYI0Hm2LyQwOu0l9mdPiWrVNsE+Q7txOnJPgtIqHCYoBVA==" - }, "@thi.ng/bitstream": { "version": "2.2.16", "resolved": "https://registry.npmjs.org/@thi.ng/bitstream/-/bitstream-2.2.16.tgz", @@ -921,13 +912,18 @@ } }, "sprae": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/sprae/-/sprae-8.1.2.tgz", - "integrity": "sha512-XbYZRuGj+AQGVousRp8nE7FsE0jKtQp77c4QeV/g/KG52zk/mH/v/F7EQhEuFnp2jQ9TwqsSpxP3NSY/5ESU+g==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/sprae/-/sprae-10.1.2.tgz", + "integrity": "sha512-2FTx1OhHUPgS8Uyh15hl7jbOlwtu3yDQaDW2fGjoGjBu4PrDQdDEP+K947+5F+RkeolLR+DtEyXYj59IETEpzg==", "requires": { - "@preact/signals-core": "^1.5.0" + "ulive": "^1.0.2" } }, + "ulive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ulive/-/ulive-1.0.2.tgz", + "integrity": "sha512-ejP7iKTzxtrhxQVdyfchH4rvZU5KRK0kT1ecEQ7oo82XZ1IUHWUrTdV2BGDylcZJcgh7j7f63FR8Y8j/xe815g==" + }, "wavefont": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/wavefont/-/wavefont-3.5.0.tgz", diff --git a/package.json b/package.json index 42746fb..de95fdc 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "audio-decode": "^2.1.0", "kv-storage-polyfill": "^2.0.0", "pseudo-worker": "^1.3.0", - "sprae": "^8.1.2", + "sprae": "^10.1.2", "wavefont": "^3.5.0" }, "devDependencies": { diff --git a/r&d.md b/r&d.md index 8360577..f52e0a8 100644 --- a/r&d.md +++ b/r&d.md @@ -8,6 +8,10 @@ * wavee * waveplay + free + + works with waveplayer +* wavely + + wave-ply + + works with wavedy for editor * waveplayer * playwave * waev @@ -16,6 +20,7 @@ + free + refers to sprae - can be done without sprae + - registered company name * wavea + wavearea * wavescope @@ -27,6 +32,7 @@ + player + refers to waveplae + refers to sprae + - taken * wavr + registered + short for wave-area @@ -246,4 +252,3 @@ 3. web-audio-player https://github.com/Jam3/web-audio-player + attempt to fix many gotchas - switches between 2 modes: element / waa - diff --git a/src/template.html b/src/template.html new file mode 100644 index 0000000..bfd9927 --- /dev/null +++ b/src/template.html @@ -0,0 +1,96 @@ +
+
+
+ + +
... +
+
+ + + +
+
+
+

+
+
+
+ + +
+ + +
diff --git a/src/wavearea.js b/src/wavearea.js index 2736f1c..ff3136c 100644 --- a/src/wavearea.js +++ b/src/wavearea.js @@ -25,11 +25,6 @@ const audio = new Audio const worker = new Worker('./dist/worker.js', { type: "module" }); const audioCtx = new AudioContext() -Object.assign(sprae.globals, { - clearInterval: clearInterval.bind(window), - setInterval: setInterval.bind(window), - raf: window.requestAnimationFrame.bind(window) -}) // UI state let state = sprae(wavearea, { @@ -47,7 +42,7 @@ let state = sprae(wavearea, { loop: false, clipEnd: null, _startTime: 0, - _startTimeOffset:0, + _startTimeOffset: 0, volume: 1, @@ -116,8 +111,8 @@ let state = sprae(wavearea, { let arrayBuf = await fileToArrayBuffer(file); let audioBuf = await decodeAudio(arrayBuf); let wavBuffer = await encodeAudio(audioBuf); - let blob = new Blob([wavBuffer], {type:'audio/wav'}); - let url = URL.createObjectURL( blob ); + let blob = new Blob([wavBuffer], { type: 'audio/wav' }); + let url = URL.createObjectURL(blob); await applyOp(['src', url]); state.loading = false; @@ -132,7 +127,7 @@ let state = sprae(wavearea, { let file = e.target.files[0]; let arrayBuf = await fileToArrayBuffer(file); let audioBuf = await audioCtx.decodeAudioData(arrayBuf); - let channelData = Array.from({length: audioBuf.numberOfChannels}, (i)=> audioBuf.getChannelData(i)) + let channelData = Array.from({ length: audioBuf.numberOfChannels }, (i) => audioBuf.getChannelData(i)) await pushOp(['file', { name: file.name, @@ -146,14 +141,14 @@ let state = sprae(wavearea, { scrollIntoCaret() { if (state.caretOffscreen && !state.scrolling) { - caretLinePointer.scrollIntoView({ behavior: 'smooth', block: 'center'}) + caretLinePointer.scrollIntoView({ behavior: 'smooth', block: 'center' }) state.scrolling = true setTimeout(() => (state.scrolling = false), 108) } }, // start playback - play (e) { + play(e) { state.playing = true; state.scrolling = false; editarea.focus(); @@ -163,7 +158,7 @@ let state = sprae(wavearea, { state.scrollIntoCaret(); - let {clipStart, clipEnd, loop} = state; + let { clipStart, clipEnd, loop } = state; const toggleStop = () => (playButton.click()) @@ -203,7 +198,7 @@ let state = sprae(wavearea, { } // audio takes time to init before play on mobile, so we hold on caret - audio.addEventListener('play', resetStartTime, {once: true}) + audio.addEventListener('play', resetStartTime, { once: true }) // audio looped - reset caret if (state.loop) audio.addEventListener('seeked', resetStartTime) @@ -236,7 +231,7 @@ let state = sprae(wavearea, { }, // navigate to history state - async goto (params) { + async goto(params) { try { await renderAudio(params) } @@ -276,8 +271,8 @@ let state = sprae(wavearea, { // let lines = Math.ceil(cleanText(segNode.textContent).length / state.cols) || 1; for (let i = 0; i < lines; i++) { let a = document.createElement('a') - let tc = timecode(i * (state.cols||0) + offset) - a.href=`#${tc}` + let tc = timecode(i * (state.cols || 0) + offset) + a.href = `#${tc}` a.textContent = tc timecodes.appendChild(a) } @@ -294,7 +289,7 @@ const inputHandlers = { // insertReplacementText(){}, // insertLineBreak(){}, // insertParagraph(){}, - insertFromDrop(e){ + insertFromDrop(e) { console.log('insert from drop', e) }, // insertFromPaste(){}, @@ -309,13 +304,13 @@ const inputHandlers = { // deleteByDrag(){}, // deleteByCut(){}, // deleteContent(){}, - async deleteContentBackward(e){ + async deleteContentBackward(e) { let range = e.getTargetRanges()[0] let fromNode = range.startContainer.parentNode.closest('.w-segment'), - toNode = range.endContainer.parentNode.closest('.w-segment'), - fromId = Number(fromNode.dataset.id), toId = Number(toNode.dataset.id) - let from = range.startOffset + state.segments.slice(0, fromId).reduce((off,seg)=>off+seg.length,0), - to = range.endOffset + state.segments.slice(0, toId).reduce((off,seg)=>off+seg.length,0) + toNode = range.endContainer.parentNode.closest('.w-segment'), + fromId = Number(fromNode.dataset.id), toId = Number(toNode.dataset.id) + let from = range.startOffset + state.segments.slice(0, fromId).reduce((off, seg) => off + seg.length, 0), + to = range.endOffset + state.segments.slice(0, toId).reduce((off, seg) => off + seg.length, 0) // debounce push op to collect multiple deletes if (this._deleteTimeout) { @@ -399,10 +394,10 @@ const selection = (start, end) => { // swap selection direction let startNode = s.anchorNode.parentNode.closest('.w-segment'), startNodeOffset = s.anchorOffset, - endNode = s.focusNode.parentNode.closest('.w-segment'), endNodeOffset = s.focusOffset; + 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] + [start, startNode, startNodeOffset, end, endNode, endNodeOffset] } return { @@ -435,28 +430,28 @@ function relOffset(offset) { // 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++ + while (content[i + skip] >= '\u0300') skip++ } return [node, offset + skip] } // produce display time from frames -function timecode (block, ms=0) { +function timecode(block, ms = 0) { let time = ((block / state?.total)) * state?.duration || 0 - return `${Math.floor(time/60).toFixed(0)}:${(Math.floor(time)%60).toFixed(0).padStart(2,0)}${ms?`.${(time%1).toFixed(ms).slice(2).padStart(ms)}`:''}` + return `${Math.floor(time / 60).toFixed(0)}:${(Math.floor(time) % 60).toFixed(0).padStart(2, 0)}${ms ? `.${(time % 1).toFixed(ms).slice(2).padStart(ms)}` : ''}` } // create play button position observer const caretObserver = new IntersectionObserver(([item]) => { - state.caretOffscreen = item.isIntersecting ? 0 : + state.caretOffscreen = item.isIntersecting ? 0 : (item.intersectionRect.top <= item.rootBounds.top ? 1 : item.intersectionRect.bottom >= item.rootBounds.bottom ? -1 : - 0); - }, { - // root: document, - threshold: 0.999, - rootMargin: '0px' - }); + 0); +}, { + // root: document, + threshold: 0.999, + rootMargin: '0px' +}); caretObserver.observe(caretLinePointer); @@ -478,9 +473,9 @@ function measureLines() { range.setStart(textNode, 0), range.setEnd(textNode, 1) let y = range.getClientRects()[0].y - for (var i = 0, offset = 0 ; i < str.length; offset++) { - let skip = 1; while (str[i+skip] >= '\u0300') skip++; - range.setStart(textNode, 0), range.setEnd(textNode, i=i+skip); + for (var i = 0, offset = 0; i < str.length; offset++) { + let skip = 1; while (str[i + skip] >= '\u0300') skip++; + range.setStart(textNode, 0), range.setEnd(textNode, i = i + skip); // 2nd line means we counted chars per line let rects = range.getClientRects() if (rects[rects.length - 1].y > y) return offset @@ -492,13 +487,13 @@ function measureLines() { // update history, post operation & schedule update // NOTE: we imply that ops are applied once and not multiple times // so that ops can be combined as del=0-10..20-30 instead of del=0-10&del=20-30 -async function pushOp (...ops) { +async function pushOp(...ops) { let url = new URL(location) for (let op of ops) { let [name, ...args] = op if (args[0].name) url.searchParams.set(name, args[0].name) - else if (url.searchParams.has(name)) url.searchParams.set(name, `${url.searchParams.get(name)}..${args.join('-')}` ) + else if (url.searchParams.has(name)) url.searchParams.set(name, `${url.searchParams.get(name)}..${args.join('-')}`) else url.searchParams.append(name, args.join('-')) } state.loading = 'Processing' @@ -512,23 +507,23 @@ async function pushOp (...ops) { } // post op message and wait for update response -function runOp (...ops) { +function runOp(...ops) { return new Promise(resolve => { // worker manages history, so id indicates which point in history we commit changes to - worker.postMessage({id: history.state?.id || 0, ops}) + worker.postMessage({ id: history.state?.id || 0, ops }) worker.addEventListener('message', e => { resolve(e.data) - }, {once: true}) + }, { once: true }) }) } // return clean from modifiers text function cleanText(str) { - return str.replace(/\u0300|\u0301/g,'') + return str.replace(/\u0300|\u0301/g, '') } // update audio url & assert waveform -function renderAudio ({url, segments, duration, offsets}) { +function renderAudio({ url, segments, duration, offsets }) { // assert waveform same as current content (must be!) state.total = segments.reduce((total, seg) => total += cleanText(seg).length, 0); state.duration = duration @@ -537,33 +532,33 @@ function renderAudio ({url, segments, duration, offsets}) { state.updateTimecodes() // URL.revokeObjectURL(audio.src) // can be persisted from history, so we keep it audio.src = url - audio.preload="metadata" // preload avoids redundant fetch requests and needed by Safari + audio.preload = "metadata" // preload avoids redundant fetch requests and needed by Safari return new Promise((ok, nok) => { audio.addEventListener('error', nok) - audio.addEventListener('loadedmetadata',()=>{ + audio.addEventListener('loadedmetadata', () => { audio.currentTime = duration * state.caretOffset / state.total || 0 - }, {once: true}); + }, { once: true }); }) } // reconstruct audio from url -async function loadAudioFromURL (url = new URL(location)) { +async function loadAudioFromURL(url = new URL(location)) { state.loading = 'Fetching' let ops = [] for (const [op, arg] of url.searchParams) ops.push(...arg.split('..').map(arg => { // skip https:// as single argument - return [op, ...(op==='src'||op==='file' ? [arg] : arg.split('-'))] + return [op, ...(op === 'src' || op === 'file' ? [arg] : arg.split('-'))] })) // shortcut for src op if (ops[0][0] === 'src') { - let [,src] = ops.shift() + let [, src] = ops.shift() let resp = await fetch(src, { cache: 'force-cache' }); let arrayBuf = await resp.arrayBuffer(); state.loading = 'Decoding' let audioBuf = await audioCtx.decodeAudioData(arrayBuf); - let channelData = Array.from({length: audioBuf.numberOfChannels}, (i)=> audioBuf.getChannelData(i)) + let channelData = Array.from({ length: audioBuf.numberOfChannels }, (i) => audioBuf.getChannelData(i)) ops.push(['file', { name: src, numberOfChannels: audioBuf.numberOfChannels, @@ -596,5 +591,3 @@ async function loadAudioFromURL (url = new URL(location)) { if (location.search.length) { loadAudioFromURL() } - - diff --git a/todo.md b/todo.md index 1cfb7b1..ebd6876 100644 --- a/todo.md +++ b/todo.md @@ -8,24 +8,24 @@ * [x] loses caret on play, like insert silence, press play etc * [x] serialize file in url: ?src=path/to/url/file/to/fetch * [x] sprae :onfile-attachment-accepted -* [x] add preloader (sprae mount-unmount) -* [x] delete fragments -> updates audio -* [x] create silence by space -* [x] download +* [ ] add preloader (sprae mount-unmount) +* [ ] delete fragments -> updates audio +* [ ] create silence by space +* [ ] download * [x] caret must be able to be reoriented during the playback * [x] Safari: wrong current time positioning * [x] BUG: stopping drops focus * [x] Make 'Enter' create segments * [x] time codes next to lines -* [x] br -* [x] del +* [ ] br +* [ ] del * [x] fix deleting tail properly * [x] normalize * [x] BUG: setting caret to the beginning of segment (a bit from the left of segment) doesn't start playback properly * [x] faster encoder by just copying changed subbuffer data, opposed to full rerender * [x] fix playback multiple segments * [x] Add vertical shift of average -* [x] Shift + select +* [ ] Shift + select * [x] ~~interleaved buffers pointing to chunks of wav file, rather than audiobuffers~~ same as below * [x] ~~immediate audio ops via copy~~ - saves 15ms, takes a lot in terms of losing AudioBuffer primitive * [x] worker processor @@ -57,16 +57,26 @@ * [x] Alt-Space for start/stop * [x] Loop play selection -## Must fix v1 +## [ ] V1 -* [ ] History separate from URLs +* [ ] FIXME: make MVP work with sprae10 +* [ ] FIXME: rethink if we need build step, ideally not +* [ ] Create UI tester - playwright vs puppeteer vs cypress vs nightwatch +* [ ] FIXME: make selectable time (now click doesn't work) +* [ ] Move play a bit to the left from time (like debug button) +* [ ] FIXME: It displays some false signal at the beginning. Must be precice waveform +* [ ] FIXME: Selection sometimes doesn't happen +* [ ] FIXME: Proper deleting +* [ ] Fragments by Enter +* [ ] Current time under cursor +* [ ] History - separate from URLs * [ ] Ctrl-z/y * [ ] Collapse last operation, eg. delete + * [x] Outsource audio-decode, add missing codecs * [x] Outsource media loopStart / loopEnd * [x] Better selection logic: must be immediate * [x] Display open/loading status -* [ ] Enter for frags * [ ] Display + for newlines * [ ] . for silence * [x] Empty URL shows "Open file" @@ -166,3 +176,26 @@ * [ ] Vary color based on spectrum * [x] ~~?Use timing object https://github.com/chrisguttandin/timing-object~~ -> nah * [ ] Editable labeling / phrases + +## Final vision + +* [ ] Stable, reliable, simple audio editor + * [ ] Drop/open any audio: movie speech track + * [ ] Separate by fragments (scenes) via enter + * [ ] Reliably playback, no glitches + * [ ] Playback bar with current time, play/stop, more + * [ ] Position: bottom floating/appearing, bottom fixed, balloon next to cursor, no (melded into UI) + * [ ] Delete / space out pieces + * [ ] Ctrl+C/Ctrl+V + * [ ] Normalize audio (from playback bar?) + * [ ] Revolume selected fragments + * [ ] Noise-gate plugin + * [ ] Speedup silences (plugin?) + * [ ] Change number of channels + * [ ] Save / download +* [ ] Customizable view (3-5 themes) + * [ ] Bar size (zoom) + * [ ] Inline vs multiline + * [ ] Freq weighting + * [ ] Paletter +* [ ] 11labs integration: generate speech of length From 00963c5868adb835a5ed8a0d15420a4add7e2fd2 Mon Sep 17 00:00:00 2001 From: Dmitry Iv Date: Wed, 22 May 2024 08:03:09 -0400 Subject: [PATCH 2/4] Bump sprae --- index.html | 2 +- package-lock.json | 14 +++++++------- package.json | 2 +- todo.md | 10 ++++++---- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/index.html b/index.html index b285e2e..83287d5 100644 --- a/index.html +++ b/index.html @@ -41,7 +41,7 @@
...
diff --git a/package-lock.json b/package-lock.json index 8a70baa..b7619ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "audio-decode": "^2.1.0", "kv-storage-polyfill": "^2.0.0", "pseudo-worker": "^1.3.0", - "sprae": "^10.1.2", + "sprae": "^10.1.4", "wavefont": "^3.5.0" }, "devDependencies": { @@ -585,9 +585,9 @@ } }, "node_modules/sprae": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/sprae/-/sprae-10.1.2.tgz", - "integrity": "sha512-2FTx1OhHUPgS8Uyh15hl7jbOlwtu3yDQaDW2fGjoGjBu4PrDQdDEP+K947+5F+RkeolLR+DtEyXYj59IETEpzg==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/sprae/-/sprae-10.1.4.tgz", + "integrity": "sha512-SZQnFj2m95YzL9qEOoTRhWjRk+aMbO2sFAJJptqEN0MlWja+N2rgZSd39Sty0AepNGe/kXxIUVYLLN9abD/G1g==", "dependencies": { "ulive": "^1.0.2" } @@ -912,9 +912,9 @@ } }, "sprae": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/sprae/-/sprae-10.1.2.tgz", - "integrity": "sha512-2FTx1OhHUPgS8Uyh15hl7jbOlwtu3yDQaDW2fGjoGjBu4PrDQdDEP+K947+5F+RkeolLR+DtEyXYj59IETEpzg==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/sprae/-/sprae-10.1.4.tgz", + "integrity": "sha512-SZQnFj2m95YzL9qEOoTRhWjRk+aMbO2sFAJJptqEN0MlWja+N2rgZSd39Sty0AepNGe/kXxIUVYLLN9abD/G1g==", "requires": { "ulive": "^1.0.2" } diff --git a/package.json b/package.json index de95fdc..7514751 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "audio-decode": "^2.1.0", "kv-storage-polyfill": "^2.0.0", "pseudo-worker": "^1.3.0", - "sprae": "^10.1.2", + "sprae": "^10.1.4", "wavefont": "^3.5.0" }, "devDependencies": { diff --git a/todo.md b/todo.md index ebd6876..d8737ac 100644 --- a/todo.md +++ b/todo.md @@ -62,6 +62,10 @@ * [ ] FIXME: make MVP work with sprae10 * [ ] FIXME: rethink if we need build step, ideally not * [ ] Create UI tester - playwright vs puppeteer vs cypress vs nightwatch +* [ ] Componentize + * [ ] Waveform + editarea attr + * [ ] Playback bar + * [ ] Timecodes * [ ] FIXME: make selectable time (now click doesn't work) * [ ] Move play a bit to the left from time (like debug button) * [ ] FIXME: It displays some false signal at the beginning. Must be precice waveform @@ -108,9 +112,6 @@ * [x] Bug: doesn't renavigate by click * [x] Bug: doesn't scroll on caret offset * [x] Bug: loop playback selection is broken - -## Improvements - * [x] Make play always cover the time, then it leaves space for "record" button * [ ] Zoom * [ ] Render only visible part (virtual) - must reduce rendering load significantly @@ -177,7 +178,7 @@ * [x] ~~?Use timing object https://github.com/chrisguttandin/timing-object~~ -> nah * [ ] Editable labeling / phrases -## Final vision +## Wavey (editor / play) * [ ] Stable, reliable, simple audio editor * [ ] Drop/open any audio: movie speech track @@ -199,3 +200,4 @@ * [ ] Freq weighting * [ ] Paletter * [ ] 11labs integration: generate speech of length +* [ ] Switchable main-thread / worker / GPU processing From 2af3ee5db93301fc6b6ee2b171d8191e1a129951 Mon Sep 17 00:00:00 2001 From: Dmitry Iv Date: Thu, 23 May 2024 08:59:09 -0400 Subject: [PATCH 3/4] Fix play interaction --- index.html | 5 +++-- package-lock.json | 14 +++++++------- package.json | 2 +- src/wavearea.js | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/index.html b/index.html index 83287d5..d83ab9d 100644 --- a/index.html +++ b/index.html @@ -20,7 +20,7 @@ -->
-
+
-

+ :ondrop="e=>e.preventDefault()"> +

diff --git a/main.css b/main.css index 14a2ac8..b941b4b 100644 --- a/main.css +++ b/main.css @@ -1,33 +1,38 @@ * { box-sizing: border-box; } -html, body { + +html, +body { margin: 0; } + body { font-family: sans-serif; } + [hidden] { - display: none!important; + display: none !important; } + [disabled] { opacity: .5; } @font-face { - font-family: wavefont; - font-display: block; - src: url(./asset/wavefont.woff2) format('woff2'); + font-family: wavefont; + font-display: block; + src: url(./asset/wavefont.woff2) format('woff2'); } .wavefont { display: block; - --wght: 20; - font-family: wavefont; + --wght: 20; + font-family: wavefont; letter-spacing: 1.5ch; font-size: var(--wavefont-size, 50px); line-height: var(--wavefont-lh); - font-variation-settings: 'wght' var(--wght), 'ROND' 0, 'YALN' 0; + font-variation-settings: 'wght' var(--wght), 'ROND' 0, 'YALN' 0; text-rendering: optimizeSpeed; font-smooth: grayscale; -webkit-font-smoothing: grayscale; @@ -41,14 +46,15 @@ body { display: flex; flex-direction: column; /* --wavefont-size: max(4rem, min(10.8vw, 6rem)); */ - --wavefont-size: 50px; /* Value is special: it doesn't break in mobiles */ + --wavefont-size: 50px; + /* Value is special: it doesn't break in mobiles */ --wavefont-lh: calc(var(--wavefont-size) * 1.4); --secondary: rgb(0 0 0 / 33%); --primary: black; } .w-loading { - cursor: wait!important; + cursor: wait !important; } .w-container { @@ -68,57 +74,61 @@ body { margin: 1rem; margin-left: 4rem; } + .w-waveform.w-dragover { /* cursor: drop; */ } -.w-editable, .w-loader { - word-break: break-all; - white-space: break-spaces; +.w-editable, +.w-loader { outline: none; width: 100%; color: var(--primary); } + .w-editable { /* background-size: 1px calc(var(--wavefont-size) * 1.4); background-position: 0% 4.2rem; background-image: repeating-linear-gradient(0deg, var(--secondary) -0.5px, rgb(255 255 255 / 0%) 0.5px, rgb(255 255 255 / 0%)); */ position: relative; } + .w-editable p::selection { - /* background-color: var(--secondary); */ + background-color: var(--secondary); } + /* played samples dimmer */ .w-editable.w-playing:before, .w-editable:focus:before, .w-editable.w-playing:after, .w-editable:focus:after { - content:''; + content: ''; position: absolute; background: rgba(255, 255, 255, .75); pointer-events: none; z-index: 1; } + .w-editable:before { bottom: 0; left: -1px; right: -1px; top: calc(var(--carety) + var(--wavefont-lh)); } + .w-editable:after { top: var(--carety); right: -1px; left: var(--caretx); height: var(--wavefont-lh); } + .w-loader { top: 0; display: block; position: absolute; z-index: 1; pointer-events: none; -} -.w-loader { color: var(--secondary); } @@ -128,8 +138,13 @@ body { margin: 0; padding: 0; min-height: var(--wavefont-lh); + + word-break: break-all; + white-space: break-spaces; } -.w-timecodes, .w-status { + +.w-timecodes, +.w-status { position: absolute; top: 0; left: -3rem; @@ -142,19 +157,25 @@ body { color: var(--secondary); line-height: var(--wavefont-lh); } + .w-timecodes { display: flex; flex-direction: column; } -.w-timecodes > * { + +.w-timecodes>* { margin: 0; text-decoration: none; color: var(--secondary); } + .w-status { left: -4rem; } -.w-play, .w-caret-line, .w-opener { + +.w-play, +.w-caret-line, +.w-opener { position: absolute; margin-left: -3.8rem; width: 3rem; @@ -164,9 +185,11 @@ body { align-items: center; justify-content: center; } + .w-opener { margin-left: -3.2rem; } + .w-play { padding: 0; text-align: center; @@ -178,7 +201,8 @@ body { justify-content: center; -webkit-tap-highlight-color: transparent; } -.w-play .w-play-clickarea { + +.w-play .w-play-clickarea { height: calc(var(--wavefont-lh) * 3); width: 100%; position: absolute; @@ -196,15 +220,15 @@ body { } .w-file { - width: 0.1px; - height: 0.1px; - opacity: 0; - overflow: hidden; - position: absolute; - z-index: -1; + width: 0.1px; + height: 0.1px; + opacity: 0; + overflow: hidden; + position: absolute; + z-index: -1; } -.w-file + label { +.w-file+label { display: flex; align-items: center; font-size: 1rem; @@ -254,6 +278,7 @@ body { bottom: 0; margin: auto; } + .w-time { font-variant-numeric: tabular-nums; } @@ -268,9 +293,11 @@ body { opacity: .108; transition: .108s ease-out; } + .w-krsnzd:hover { opacity: .82; } + .w-info-button { position: fixed; bottom: .8rem; @@ -281,9 +308,11 @@ body { opacity: 0.25; cursor: pointer; } + .w-info-button:hover { opacity: 1; } + .w-info-dialog { padding: 1.2rem; border: none; @@ -293,24 +322,28 @@ body { opacity: 0; display: none; transition: opacity .2s ease-out; - inset: 0; + inset: 0; } + .w-info-dialog[open] { opacity: 1; margin: auto; display: block; transition: opacity .2s ease-out; } -.w-info-dialog > * { + +.w-info-dialog>* { margin: 0 } + .w-info-dialog::backdrop { - background: rgba(108,108,108,.16); + background: rgba(108, 108, 108, .16); -webkit-backdrop-filter: blur(0px); backdrop-filter: blur(0px); transition: opacity .5s ease-out; } + .w-info-dialog[open]::backdrop { -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); -} \ No newline at end of file +} diff --git a/src/measure-latency.js b/src/measure-latency.js index 25afd6a..f94b033 100644 --- a/src/measure-latency.js +++ b/src/measure-latency.js @@ -13,4 +13,4 @@ export async function measureLatency() { ok(performance.now() - start) } }) -} \ No newline at end of file +} diff --git a/src/wavearea.js b/src/wavearea.js index 2c8375a..abf0ffe 100644 --- a/src/wavearea.js +++ b/src/wavearea.js @@ -27,6 +27,8 @@ const audioCtx = new AudioContext() // UI state let state = sprae(wavearea, { + // globals + raf: (fn) => window.requestAnimationFrame(fn), // state loading: false, @@ -280,6 +282,7 @@ let state = sprae(wavearea, { } }, + // util timecode }); @@ -337,7 +340,6 @@ const whatsLatency = async () => { wavearea.removeEventListener('mousedown', whatsLatency) wavearea.removeEventListener('keydown', whatsLatency) state.latency = await measureLatency() - console.log('measured latency', state.latency) } wavearea.addEventListener('touchstart', whatsLatency) wavearea.addEventListener('mousedown', whatsLatency) @@ -387,6 +389,7 @@ const selection = (start, end) => { } } + // return unknown selection if (!s.anchorNode || !editarea.contains(s.anchorNode)) return // collect start/end offsets