Skip to content

Commit 3f2e664

Browse files
committed
Fix playback better
1 parent 78edca0 commit 3f2e664

13 files changed

Lines changed: 548 additions & 171 deletions

.work/todo.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
* [ ] Open .m4a recording file, split and trim parts of them
1+
22

33
## [ ] Restructure
44

bench/run-text-bench.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Run text rendering benchmark on multiple browsers
2+
// Usage: node bench/run-text-bench.mjs
3+
4+
import { chromium, webkit } from 'playwright';
5+
6+
for (let [name, launcher] of [['Chromium', chromium], ['WebKit', webkit]]) {
7+
console.log(`\n${'='.repeat(60)}\n${name}\n${'='.repeat(60)}`)
8+
let browser = await launcher.launch()
9+
let page = await browser.newPage()
10+
11+
page.on('console', msg => console.log(msg.text()))
12+
13+
await page.goto('http://127.0.0.1:8777/bench/text-render-bench.html')
14+
await page.waitForTimeout(1000)
15+
16+
// click run and wait for "Done!"
17+
await page.click('#run')
18+
await page.waitForFunction(() => document.getElementById('results').textContent.includes('Done!'), { timeout: 120000 })
19+
20+
await browser.close()
21+
}

bench/text-render-bench.html

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<!DOCTYPE html>
2+
<meta charset="UTF-8">
3+
<title>Wavefont Text Rendering Benchmark</title>
4+
<style>
5+
body { font-family: sans-serif; margin: 2rem; }
6+
.wavefont {
7+
font-family: wavefont;
8+
font-size: 50px;
9+
line-height: 70px;
10+
letter-spacing: 1px;
11+
font-variation-settings: 'wght' 30, 'ROND' 0, 'YALN' 0;
12+
}
13+
#results { white-space: pre-wrap; font-family: monospace; margin-top: 1rem; }
14+
.test-area { width: 80vw; overflow: hidden; visibility: hidden; position: absolute; }
15+
.opt-speed { text-rendering: optimizeSpeed; }
16+
.opt-smooth { -webkit-font-smoothing: antialiased; }
17+
.opt-none { -webkit-font-smoothing: none; }
18+
.no-ligatures { font-variant-ligatures: none; }
19+
.will-change { will-change: contents; }
20+
.contain { contain: content; }
21+
</style>
22+
<link rel="stylesheet" href="../node_modules/wavefont/wavefont.css">
23+
24+
<h3>Wavefont Text Rendering Benchmark</h3>
25+
<button id="run">Run Benchmark</button>
26+
<div id="results"></div>
27+
28+
<script type="module">
29+
// generate fake waveform string: base chars (U+0100-U+01FF) + combining marks
30+
function genWaveform(n) {
31+
let str = ''
32+
for (let i = 0; i < n; i++) {
33+
str += String.fromCharCode(0x0100 + (i % 128))
34+
str += (i % 3 === 0) ? '\u0301' : '\u0300'
35+
}
36+
return str
37+
}
38+
39+
let log = s => { document.getElementById('results').textContent += s + '\n'; console.log(s) }
40+
41+
async function bench(label, el, text, appendText) {
42+
// force layout
43+
el.offsetHeight
44+
45+
// 1. Set full text
46+
let t0 = performance.now()
47+
el.textContent = text
48+
el.offsetHeight // force layout
49+
let setTime = performance.now() - t0
50+
51+
// 2. Append chunk
52+
let t1 = performance.now()
53+
el.firstChild.appendData(appendText)
54+
el.offsetHeight // force layout
55+
let appendTime = performance.now() - t1
56+
57+
// 3. getClientRects on a range near middle
58+
let mid = Math.floor(el.firstChild.textContent.length / 2)
59+
let r = new Range()
60+
r.setStart(el.firstChild, mid)
61+
r.setEnd(el.firstChild, mid + 1)
62+
let t2 = performance.now()
63+
r.getClientRects()
64+
let rectsTime = performance.now() - t2
65+
66+
// cleanup
67+
el.textContent = ''
68+
el.offsetHeight
69+
70+
log(`${label.padEnd(35)} set=${setTime.toFixed(0)}ms append=${appendTime.toFixed(0)}ms rects=${rectsTime.toFixed(0)}ms`)
71+
}
72+
73+
document.getElementById('run').onclick = async () => {
74+
log('Generating test data...')
75+
let small = genWaveform(1000) // ~2000 chars
76+
let medium = genWaveform(5000) // ~10000 chars
77+
let large = genWaveform(15000) // ~30000 chars
78+
let chunk = genWaveform(200) // ~400 chars append chunk
79+
80+
let configs = [
81+
{ label: 'baseline (wavefont)', classes: 'wavefont' },
82+
{ label: '+ optimizeSpeed', classes: 'wavefont opt-speed' },
83+
{ label: '+ antialiased', classes: 'wavefont opt-smooth' },
84+
{ label: '+ no smoothing', classes: 'wavefont opt-none' },
85+
{ label: '+ no ligatures', classes: 'wavefont no-ligatures' },
86+
{ label: '+ will-change:contents', classes: 'wavefont will-change' },
87+
{ label: '+ contain:content', classes: 'wavefont contain' },
88+
{ label: '+ all optimizations', classes: 'wavefont opt-speed opt-smooth no-ligatures contain' },
89+
{ label: 'monospace (no wavefont)', classes: '', style: 'font-family:monospace;font-size:14px;line-height:20px' },
90+
]
91+
92+
for (let size of [['1K blocks', small], ['5K blocks', medium], ['15K blocks', large]]) {
93+
log(`\n=== ${size[0]} (${size[1].length} chars) ===`)
94+
for (let cfg of configs) {
95+
let el = document.createElement('div')
96+
el.className = 'test-area ' + cfg.classes
97+
if (cfg.style) el.style.cssText += cfg.style
98+
document.body.appendChild(el)
99+
await bench(cfg.label, el, size[1], chunk)
100+
el.remove()
101+
}
102+
}
103+
104+
// contenteditable comparison
105+
log('\n=== contenteditable vs plain div (5K blocks) ===')
106+
for (let ce of [false, true]) {
107+
let el = document.createElement('div')
108+
el.className = 'test-area wavefont opt-speed contain'
109+
if (ce) el.contentEditable = 'true'
110+
document.body.appendChild(el)
111+
await bench(ce ? 'contenteditable=true' : 'contenteditable=false', el, medium, chunk)
112+
el.remove()
113+
}
114+
115+
log('\nDone!')
116+
}
117+
</script>

main.css

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ body {
4141
letter-spacing: var(--wavefont-spacing, 0px);
4242
font-variation-settings: 'wght' var(--wavefont-wght, 25), 'ROND' var(--wavefont-rond, 100), 'YALN' var(--wavefont-yaln, 0);
4343
text-rendering: optimizeSpeed;
44-
font-smooth: grayscale;
45-
-webkit-font-smoothing: grayscale;
46-
-moz-osx-font-smoothing: grayscale;
44+
font-smooth: antialiased;
45+
-webkit-font-smoothing: antialiased;
46+
-moz-osx-font-smoothing: antialiased;
4747
}
4848

4949
.container {
@@ -118,8 +118,7 @@ body {
118118
}
119119

120120

121-
#timecodes,
122-
#status {
121+
#timecodes {
123122
position: absolute;
124123
top: 0;
125124
left: -3rem;
@@ -133,6 +132,24 @@ body {
133132
line-height: var(--wavefont-lh);
134133
}
135134

135+
#status {
136+
position: fixed;
137+
bottom: 0;
138+
left: 0;
139+
right: 0;
140+
font-family: sans-serif;
141+
letter-spacing: 0;
142+
font-size: .75rem;
143+
margin: 0;
144+
padding: .35rem .75rem;
145+
white-space: pre;
146+
color: var(--secondary);
147+
background: var(--bg, #fff);
148+
border-top: 1px solid var(--border, #eee);
149+
z-index: 100;
150+
text-align: center;
151+
}
152+
136153
#timecodes {
137154
display: flex;
138155
flex-direction: column;
@@ -144,10 +161,6 @@ body {
144161
color: var(--secondary);
145162
}
146163

147-
#status {
148-
left: -4rem;
149-
}
150-
151164
#error {
152165
font-family: sans-serif;
153166
font-size: .875rem;

playwright.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default defineConfig({
55
testIgnore: '**/unit/**',
66
timeout: 30000,
77
fullyParallel: true,
8-
workers: 4,
8+
workers: 2,
99
use: {
1010
baseURL: 'http://127.0.0.1:8777',
1111
headless: true,

src/api.js

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// API layer connecting UI, worker and storage
2+
// PCM stored on main thread to avoid slow Worker postMessage on playback
23

34
import * as Comlink from 'comlink';
45
import { createStore } from './store/index.js';
@@ -8,23 +9,65 @@ const worker = Comlink.wrap(new Worker(new URL('./worker.js', import.meta.url),
89
export default function createApi({ store } = {}) {
910
if (!store) store = createStore()
1011

12+
// main-thread PCM storage — chunks per channel
13+
let pcmChunks = [] // [ch][chunkIndex] = Float32Array
14+
let pcmTotal = 0
15+
let channelCount = 0
16+
1117
return {
1218
async loadFile(file, onWaveform) {
1319
if (typeof file === 'string') {
1420
file = await store.getFile(file);
1521
}
1622

23+
// reset PCM storage
24+
pcmChunks = []
25+
pcmTotal = 0
26+
channelCount = 0
27+
1728
let decodeError = null;
1829
const result = await worker.decode(file, Comlink.proxy({
1930
onWaveform,
31+
onPCM: (chunkData, frames) => {
32+
if (!channelCount) {
33+
channelCount = chunkData.length
34+
pcmChunks = Array.from({ length: channelCount }, () => [])
35+
}
36+
for (let ch = 0; ch < channelCount; ch++) {
37+
pcmChunks[ch].push(chunkData[ch])
38+
}
39+
pcmTotal += frames
40+
},
2041
onError: (e) => { decodeError = e }
2142
}));
2243
if (decodeError) throw decodeError;
2344
return result;
2445
},
2546

47+
// getWindow from main-thread PCM — instant, no Worker postMessage
2648
getWindow(fromSample, toSample) {
27-
return worker.getWindow(fromSample, toSample)
49+
if (!pcmChunks.length || !pcmChunks[0].length) return null
50+
if (toSample == null || toSample > pcmTotal) toSample = pcmTotal
51+
if (fromSample >= toSample) return null
52+
53+
let len = toSample - fromSample
54+
let result = Array.from({ length: channelCount }, () => new Float32Array(len))
55+
56+
for (let ch = 0; ch < channelCount; ch++) {
57+
let pos = 0
58+
for (let chunk of pcmChunks[ch]) {
59+
let chunkEnd = pos + chunk.length
60+
if (chunkEnd > fromSample && pos < toSample) {
61+
let srcStart = Math.max(0, fromSample - pos)
62+
let srcEnd = Math.min(chunk.length, toSample - pos)
63+
let dstStart = Math.max(0, pos + srcStart - fromSample)
64+
result[ch].set(chunk.subarray(srcStart, srcEnd), dstStart)
65+
}
66+
pos = chunkEnd
67+
}
68+
}
69+
70+
return result
2871
},
2972

3073
async saveFile(file, meta) {

src/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export const BLOCK_SIZE = 1024
2+
// export const BLOCK_SIZE = 2048
3+
// export const BLOCK_SIZE = 4096

0 commit comments

Comments
 (0)