Skip to content
This repository was archived by the owner on Apr 18, 2024. It is now read-only.

Commit 5a7210b

Browse files
authored
feat: DEV-4034: Rendering performance improvements for large-duration audio (#1138)
* wip: chunk decoding in webworker with ffmpeg * wip: process chunks within the generator to allow quick fail and cleanup * allow earlier render of timeline and updating of timecontrols for total duration * refactor AudioDecoder from WaveformAudio * fix chunk decoding to properly await the promise * retain decoded audio for a single source so that quick switching between annotations is fluid * colocate wasm built files with the js files importing them * show chunk decoding progress information in the ui * wip: render chunks * wip: chunk render * wip: render channel height correctly * wip: interruptible render * fix render of all buffer chunks * do not need buffer allocator anymore, this was the problem * fix mute/set volume of player * fix duration reporting to be based on htmlmedia duration * wip: quickview loading seems to double load some of the time * fix the loading of decoded audio * wip: working perf improvement on partial render * render partial cache when samples to render becomes too high * ensure amplitude scale change rerenders cache * extract constant for default sample rate * removing unused code * update comments * cleanup the async handling, wip audio decoder pool * pool audio decoders so data can be reused efficiently while still being aggressive of memory eviction * updates for review feedback * bubble up an error message to the user if something goes wrong in audio load/decode * fix resizing of waveform and timeline, make render cancellation possible for improved performance
1 parent 7607825 commit 5a7210b

19 files changed

+936
-445
lines changed

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
"start": "yarn run copy-examples && node dev-server.js",
4545
"test:coverage": "yarn test -- --coverage",
4646
"test:watch": "react-scripts test",
47-
"test": "yarn jest src"
47+
"test": "yarn jest src",
48+
"postinstall": "sh scripts/postinstall.sh"
4849
},
4950
"husky": {
5051
"hooks": {
@@ -85,6 +86,7 @@
8586
],
8687
"dependencies": {
8788
"@thi.ng/rle-pack": "^2.1.6",
89+
"audio-file-decoder": "^2.3.0",
8890
"babel-preset-react-app": "^9.1.1",
8991
"d3": "^5.16.0",
9092
"magic-wand-js": "^1.0.0",
@@ -114,8 +116,8 @@
114116
"@types/jest": "^29.2.3",
115117
"@types/keymaster": "^1.6.30",
116118
"@types/lodash.ismatch": "^4.4.6",
117-
"@types/offscreencanvas": "^2019.6.4",
118119
"@types/nanoid": "^3.0.0",
120+
"@types/offscreencanvas": "^2019.6.4",
119121
"@types/react-dom": "^17.0.11",
120122
"@types/react-window": "^1.8.5",
121123
"@types/strman": "^2.0.0",
@@ -140,6 +142,7 @@
140142
"enzyme-to-json": "^3.5.0",
141143
"eslint": "^8.28.0",
142144
"eslint-webpack-plugin": "^3.0.1",
145+
"file-loader": "^6.2.0",
143146
"html-webpack-plugin": "^5.3.1",
144147
"husky": "^3.1.0",
145148
"identity-obj-proxy": "^3.0.0",

scripts/postinstall.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
cp ./node_modules/audio-file-decoder/decode-audio.wasm ./node_modules/audio-file-decoder/dist/decode-audio.wasm

src/components/Timeline/Controls/AudioControl.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { IconSoundConfig, IconSoundMutedConfig } from '../../../assets/icons/tim
66
import { ControlButton } from '../Controls';
77
import { Slider } from './Slider';
88

9-
const MAX_VOL = 200;
9+
const MAX_VOL = 100;
1010

1111
export interface AudioControlProps {
1212
volume: number;
@@ -38,15 +38,15 @@ export const AudioControl: FC<AudioControlProps> = ({
3838
onVolumeChange?.(0);
3939
return;
4040
}
41-
if (_volumeValue > (MAX_VOL)) {
41+
if (_volumeValue > MAX_VOL) {
4242
onVolumeChange?.(MAX_VOL / 100);
4343
return;
4444
} else if (_volumeValue < 0) {
4545
onVolumeChange?.(0);
4646
return;
4747
}
4848

49-
onVolumeChange?.(_volumeValue / MAX_VOL * 2);
49+
onVolumeChange?.(_volumeValue / MAX_VOL);
5050
};
5151

5252
const handleSetMute = () => {
@@ -60,7 +60,7 @@ export const AudioControl: FC<AudioControlProps> = ({
6060
<Slider
6161
min={0}
6262
max={MAX_VOL}
63-
value={Math.round(volume * MAX_VOL / 2)}
63+
value={Math.round(volume * MAX_VOL)}
6464
onChange={handleSetVolume}
6565
description={'Volume'}
6666
info={'Increase or decrease the volume of the audio'}

src/lib/AudioUltra/Controls/Player.ts

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ export class Player extends Destructable {
2727
* Get current playback speed
2828
*/
2929
get rate() {
30-
if (this.audio?.source?.playbackRate.value) {
31-
if (this.audio.source.playbackRate.value !== this._rate) {
32-
this.audio.source.playbackRate.value = this._rate; // restore the correct rate
30+
if (this.audio) {
31+
if (this.audio.speed !== this._rate) {
32+
this.audio.speed = this._rate; // restore the correct rate
3333
}
3434
}
3535

@@ -44,8 +44,8 @@ export class Player extends Destructable {
4444

4545
this._rate = value;
4646

47-
if (this.audio?.source) {
48-
this.audio.source.playbackRate.value = value;
47+
if (this.audio) {
48+
this.audio.speed = value;
4949

5050
if (rateChanged) {
5151
this.wf.invoke('rateChanged', [value]);
@@ -54,22 +54,28 @@ export class Player extends Destructable {
5454
}
5555

5656
get duration() {
57-
return this.audio?.buffer?.duration ?? 0;
57+
return this.audio?.duration ?? 0;
5858
}
5959

6060
get volume() {
61-
return this.audio?.gain?.gain.value ?? 1;
61+
return this.audio?.volume ?? 1;
6262
}
6363

6464
set volume(value: number) {
6565
if (this.audio) {
6666

6767
const volumeChanged = this.volume !== value;
6868

69-
this.audio.volume = value;
70-
7169
if (volumeChanged) {
72-
this.wf.invoke('volumeChange', [value]);
70+
if (value === 0) {
71+
this.muted = true;
72+
} else if(this.muted) {
73+
this.muted = false;
74+
} else {
75+
this.audio.volume = value;
76+
}
77+
78+
this.wf.invoke('volumeChanged', [this.volume]);
7379
}
7480
}
7581
}
@@ -91,13 +97,12 @@ export class Player extends Destructable {
9197
}
9298

9399
get muted() {
94-
return this.audio?.volume === 0;
100+
return this.audio?.muted ?? false;
95101
}
96102

97103
set muted(muted: boolean) {
98104
if (!this.audio) return;
99-
100-
if (this.audio.muted === muted) return;
105+
if (this.muted === muted) return;
101106

102107
if (muted) {
103108
this.audio.mute();
@@ -134,11 +139,14 @@ export class Player extends Destructable {
134139

135140
handleEnded = () => {
136141
if (this.loop) return;
137-
this.ended = true;
138142
this.updateCurrentTime(true);
143+
};
144+
145+
private playEnded() {
146+
this.ended = true;
139147
this.pause();
140148
this.wf.invoke('playend');
141-
};
149+
}
142150

143151
pause() {
144152
if (this.isDestroyed || !this.playing || !this.audio) return;
@@ -183,7 +191,7 @@ export class Player extends Destructable {
183191
this.timestamp = performance.now();
184192
this.recreateSource();
185193

186-
if (!this.audio?.source) return;
194+
if (!this.audio) return;
187195

188196
this.playing = true;
189197

@@ -196,8 +204,12 @@ export class Player extends Destructable {
196204
start = clamp(this.loop.start, 0, duration);
197205
}
198206

199-
this.audio.source.start(0, start ?? 0, duration ?? this.duration);
200-
this.audio.source.addEventListener('ended', this.handleEnded);
207+
if (this.audio.el) {
208+
this.audio.el.currentTime = this.currentTime;
209+
this.audio.el.addEventListener('ended', this.handleEnded);
210+
this.audio.el.play();
211+
}
212+
201213
this.watch();
202214
}
203215

@@ -241,16 +253,16 @@ export class Player extends Destructable {
241253
private disconnectSource() {
242254
if (this.isDestroyed || !this.audio || !this.connected) return;
243255
this.connected = false;
244-
this.audio.source?.removeEventListener('ended', this.handleEnded);
245-
this.audio.source?.stop(0);
256+
257+
if (this.audio.el) {
258+
this.audio.el.removeEventListener('ended', this.handleEnded);
259+
}
246260
this.audio.disconnect();
247261
}
248262

249263
private cleanupSource() {
250264
if (this.isDestroyed || !this.audio) return;
251265
this.disconnectSource();
252-
253-
delete this.audio.source;
254266
delete this.audio;
255267
}
256268

@@ -273,18 +285,25 @@ export class Player extends Destructable {
273285
}
274286
}
275287

276-
private updateCurrentTime(forceTimeToEnd?: boolean) {
288+
private updateCurrentTime(forceEnd = false) {
277289
const now = performance.now();
278-
const tick = (( now - this.timestamp) / 1000) * this.rate;
290+
const tick = ((now - this.timestamp) / 1000) * this.rate;
279291

280292
this.timestamp = now;
281293

282294
const end = this.loop?.end ?? this.duration;
283295

284-
const newTime = forceTimeToEnd ? this.duration : clamp(this.time + tick, 0, end);
296+
const newTime = forceEnd ? this.duration : clamp(this.time + tick, 0, end);
285297

286298
this.time = newTime;
287-
this.wf.invoke('playing', [this.time]);
299+
300+
if (!this.loop && this.time >= this.duration - tick) {
301+
this.time = this.duration;
302+
this.wf.invoke('playing', [this.duration]);
303+
this.playEnded();
304+
} else {
305+
this.wf.invoke('playing', [this.time]);
306+
}
288307
}
289308

290309
private stopWatch() {

0 commit comments

Comments
 (0)