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

Commit 4ad8edb

Browse files
feat: LSDV-3028: Audio v3 dual channel render (#1185)
* Revert "Revert feat: DEV-4034: Rendering performance improvements for large-duration audio (#1138) (#1160)" This reverts commit d84c38e. * fix: LSDV-4532: Correct decoded audio data offset * update the removal grace period for audio cached decoders * only pause playback if it was playing in disconnect, remove redundant methods * force buffer audio playback by playing then pausing, and ensure media has ability to playback through buffer * adding comment about the duration offset start for decoding audio * feat: DEV-3028: Audio v3 dual channel render * fix: LSDV-4532: Audio sometimes goes missing during playback * fix: LSDV-4532: Waveform sometimes blank when toggling visibility off then on * fix: LSDV-4532: Sound sometimes delayed to playback * when layer visibility changes we now fire renderAvailableChannels * add crossorigin anonymous to audio element * testing cors issue * changing back to proper src * fix multi channel split by using forked lib with a patch fix * fix: LSDV-4532: Adding more tests fixing playbackRate setting * default to splitchannel if possible * update audio-file-decoder * adding zoom test * fix the number of chunks calculation to account for the larger sized samples returned from the decoder in splitchannel * missing the calculation to augment chunk size when channel count is greater than one * ensure a minwaveheight is adhered to, so that each channel wave is visible * fix default waveHeight when non splitchannel * fix initial render, and layerUpdated causing contentious renders that can sometimes cause unexpected results in layers * fix initial render of single channel, original height waveform * ensure the splitchannel worker is lazy to init, and can be destroyed when not in use * fix partial waveform render optimization for large data point renders that occur across multiple channels * change Audio tag param from minwaveheight to more aptly named waveheight * removing console log * adding ff to allow audio v3 multichannel support be default on --------- Co-authored-by: Yousif Yassi <[email protected]>
1 parent d108677 commit 4ad8edb

20 files changed

+457
-110
lines changed

Diff for: e2e/fragments/AtAudioView.js

+96-4
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,71 @@ module.exports = {
5656
I.wait(1);
5757
},
5858

59-
clickAt(x) {
59+
/**
60+
* Click on the Audio waveform at the given x coordinate, and optional y coordinate.
61+
* @example
62+
* await AtAudioView.lookForStage();
63+
* AtAudioView.clickAt(50);
64+
* @param {number} x
65+
* @param {number} [y=undefined] - if not provided, will click at the center of the waveform
66+
*/
67+
clickAt(x, y) {
68+
y = y !== undefined ? y : this._stageBbox.height / 2;
6069
I.scrollPageToTop();
61-
I.clickAt(this._stageBbox.x + x, this._stageBbox.y + this._stageBbox.height / 2);
70+
I.clickAt(this._stageBbox.x + x, this._stageBbox.y + y);
6271
I.wait(1); // We gotta wait here because clicks on the canvas are not processed immediately
6372
},
6473

74+
/**
75+
* Toggle the audio control menu. This houses the volume slider and mute button.
76+
*/
6577
toggleControlsMenu() {
6678
I.click(this._controlMenuSelector);
6779
},
6880

81+
/**
82+
* Toggle the audio settings menu. This houses the playback speed slider, amplitude slider, and interface visibility toggles.
83+
*/
6984
toggleSettingsMenu() {
7085
I.click(this._settingsMenuSelector);
7186
},
7287

88+
/**
89+
* Zooms in the Audio Waveform by using the mouse wheel at the given relative position.
90+
* @param {number} deltaY
91+
* @param {Object} [atPoint] - Point where the wheel action will be called
92+
* @param {number} [atPoint.x=0.5] - relative X coordinate
93+
* @param {number} [atPoint.y=0.5] - relative Y coordinate
94+
* @returns {Promise<void>}
95+
*
96+
* @example
97+
* // zoom in
98+
* await AtAudioView.zoomToPoint(-100, { x: .01 });
99+
* // zoom out
100+
* await AtAudioView.zoomToPoint(100);
101+
*/
102+
async zoomToPoint(deltaY, atPoint = { x: 0.5, y: 0.5 }) {
103+
const { x = 0.5, y = 0.5 } = atPoint;
104+
105+
I.scrollPageToTop();
106+
107+
const stageBBox = await I.grabElementBoundingRect(this._stageSelector);
108+
109+
I.clickAt(stageBBox.x + stageBBox.width * x, stageBBox.y + stageBBox.height * y); // click to focus the canvas
110+
111+
I.pressKeyDown('Control');
112+
I.mouseWheel({ deltaY });
113+
I.pressKeyUp('Control');
114+
},
115+
116+
/**
117+
* Asserts the current volume of the audio player the slider, input and the audio player.
118+
* @param {number} value - volume in the range [0, 100]
119+
* @returns {Promise<void>}
120+
*
121+
* @example
122+
* await AtAudioView.seeVolume(50);
123+
*/
73124
async seeVolume(value) {
74125
this.toggleControlsMenu();
75126
I.seeInField(this._volumeInputSelector, value);
@@ -85,12 +136,27 @@ module.exports = {
85136
this.toggleControlsMenu();
86137
},
87138

139+
/**
140+
* Sets the volume of the audio player via the input.
141+
* @param {number} value - volume in the range [0, 100]
142+
*
143+
* @example
144+
* AtAudioView.setVolumeInput(50);
145+
*/
88146
setVolumeInput(value) {
89147
this.toggleControlsMenu();
90148
I.fillField(this._volumeInputSelector, value);
91149
this.toggleControlsMenu();
92150
},
93151

152+
/**
153+
* Asserts the current playback speed of the audio player the slider, input and the audio player.
154+
* @param {number} value - speed in the range [0.5, 2.5]
155+
* @returns {Promise<void>}
156+
*
157+
* @example
158+
* await AtAudioView.seePlaybackSpeed(1.5);
159+
*/
94160
async seePlaybackSpeed(value) {
95161
this.toggleSettingsMenu();
96162

@@ -103,12 +169,27 @@ module.exports = {
103169
this.toggleSettingsMenu();
104170
},
105171

172+
/**
173+
* Sets the playback speed of the audio player via the input.
174+
* @param {number} value - speed in the range [0.5, 2.5]
175+
*
176+
* @example
177+
* AtAudioView.setPlaybackSpeedInput(1.5);
178+
*/
106179
setPlaybackSpeedInput(value) {
107180
this.toggleSettingsMenu();
108181
I.fillField(this._playbackSpeedInputSelector, value);
109182
this.toggleSettingsMenu();
110183
},
111184

185+
/**
186+
* Asserts the current amplitude of the audio player the slider, and the input.
187+
* @param {number} value - amplitude (y-axis zoom) in the range [1, 150]
188+
* @returns {Promise<void>}
189+
*
190+
* @example
191+
* await AtAudioView.seeAmplitude(10);
192+
*/
112193
async seeAmplitude(value) {
113194
this.toggleSettingsMenu();
114195

@@ -118,6 +199,13 @@ module.exports = {
118199
this.toggleSettingsMenu();
119200
},
120201

202+
/**
203+
* Sets the amplitude of the audio player via the input.
204+
* @param {number} value - speed in the range [1, 150]
205+
*
206+
* @example
207+
* AtAudioView.setAmplitudeInput(10);
208+
*/
121209
setAmplitudeInput(value) {
122210
this.toggleSettingsMenu();
123211
I.fillField(this._amplitudeInputSelector, value);
@@ -143,10 +231,14 @@ module.exports = {
143231

144232
assert.equal(selectedChoice, 'Positive');
145233
},
146-
234+
235+
/**
236+
* Asserts whether the audio player is reporting as paused.
237+
* @returns {Promise<void>}
238+
*/
147239
async seeIsPlaying(playing) {
148240
const isPaused = await I.grabAttributeFrom(this._audioElementSelector, 'paused');
149241

150-
assert.equal(!isPaused, playing, 'Audio is not playing');
242+
assert.equal(!isPaused, playing, playing ? 'Audio is not playing' : 'Audio is playing');
151243
},
152244
};

Diff for: e2e/tests/audio/audio-controls.test.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11

22
/* global Feature, Scenario */
33

4+
const assert = require('assert');
5+
46
Feature('Audio Controls');
57

68
const config = `
@@ -56,12 +58,23 @@ const annotations = [
5658

5759
const params = { annotations: [{ id: 'test', result: annotations }], config, data };
5860

59-
Scenario('Check the audio controls work', async function({ I, LabelStudio, AtAudioView, AtSidebar }) {
61+
Scenario('Check the audio controls work', async function({ I, LabelStudio, ErrorsCollector, AtAudioView, AtSidebar }) {
62+
async function doNotSeeErrors() {
63+
await I.wait(2);
64+
const errors = await ErrorsCollector.grabErrors();
65+
66+
if (errors.length) {
67+
assert.fail(`Got an error: ${errors[0]}`);
68+
}
69+
}
70+
6071
LabelStudio.setFeatureFlags({
6172
ff_front_dev_2715_audio_3_280722_short: true,
6273
});
6374
I.amOnPage('/');
6475

76+
await ErrorsCollector.run();
77+
6578
LabelStudio.init(params);
6679

6780
await AtAudioView.waitForAudio();
@@ -115,4 +128,10 @@ Scenario('Check the audio controls work', async function({ I, LabelStudio, AtAud
115128
AtAudioView.clickPauseButton();
116129

117130
await AtAudioView.seeIsPlaying(false);
131+
132+
I.say('Check the waveform can be zoomed without error');
133+
134+
await AtAudioView.zoomToPoint(-120);
135+
136+
await doNotSeeErrors();
118137
});

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
],
8787
"dependencies": {
8888
"@thi.ng/rle-pack": "^2.1.6",
89-
"audio-file-decoder": "^2.3.0",
89+
"@martel/audio-file-decoder": "2.3.15",
9090
"babel-preset-react-app": "^9.1.1",
9191
"d3": "^5.16.0",
9292
"magic-wand-js": "^1.0.0",

Diff for: scripts/postinstall.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
cp ./node_modules/audio-file-decoder/decode-audio.wasm ./node_modules/audio-file-decoder/dist/decode-audio.wasm
1+
cp ./node_modules/@martel/audio-file-decoder/decode-audio.wasm ./node_modules/@martel/audio-file-decoder/dist/decode-audio.wasm

Diff for: src/lib/AudioUltra/Common/Worker/index.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ export class ComputeWorker {
4646
};
4747

4848
self.addEventListener('message', (e) => {
49-
if (e.data) return;
49+
if (!e.data) return;
50+
5051
const { data, type, eventId } = e.data;
5152

5253
switch(type) {
@@ -69,7 +70,7 @@ export class ComputeWorker {
6970
type: 'compute',
7071
}, true);
7172

72-
return result?.data;
73+
return result?.data?.result?.data;
7374
}
7475

7576
async precompute(data: Record<string, any>) {
@@ -105,6 +106,7 @@ export class ComputeWorker {
105106
if (waitResponse) {
106107
const resolver = (e: MessageEvent) => {
107108
if (eventId === e.data.eventId) {
109+
worker.removeEventListener('message', resolver);
108110
resolve(e);
109111
}
110112
};

Diff for: src/lib/AudioUltra/Media/AudioDecoder.ts

+55-13
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { AudioDecoderWorker, getAudioDecoderWorker } from 'audio-file-decoder';
1+
import { AudioDecoderWorker, getAudioDecoderWorker } from '@martel/audio-file-decoder';
22
// eslint-disable-next-line
33
// @ts-ignore
4-
import DecodeAudioWasm from 'audio-file-decoder/decode-audio.wasm';
4+
import DecodeAudioWasm from '@martel/audio-file-decoder/decode-audio.wasm';
55
import { Events } from '../Common/Events';
66
import { clamp, info } from '../Common/Utils';
7+
import { SplitChannel } from './SplitChannel';
8+
79

810
const DURATION_CHUNK_SIZE = 60 * 30; // 30 minutes
911

@@ -14,7 +16,7 @@ interface AudioDecoderEvents {
1416
}
1517

1618
export class AudioDecoder extends Events<AudioDecoderEvents> {
17-
chunks?: Float32Array[];
19+
chunks?: Float32Array[][];
1820
private cancelled = false;
1921
private decodeId = 0; // if id=0, decode is not in progress
2022
private worker: AudioDecoderWorker | undefined;
@@ -50,14 +52,14 @@ export class AudioDecoder extends Events<AudioDecoderEvents> {
5052

5153
get dataLength() {
5254
if (this.chunks && !this._dataLength) {
53-
this._dataLength = this.chunks?.reduce((a, b) => a + b.length, 0) ?? 0;
55+
this._dataLength = (this.chunks?.reduce((a, b) => a + b.reduce((_a, _b) => _a + _b.length, 0), 0) ?? 0) / this._channelCount;
5456
}
5557
return this._dataLength;
5658
}
5759

5860
get dataSize() {
5961
if (this.chunks && !this._dataSize) {
60-
this._dataSize = this.chunks?.reduce((a, b) => a + b.byteLength, 0) ?? 0;
62+
this._dataSize = (this.chunks?.reduce((a, b) => a + b.reduce((_a, _b) => _a + _b.byteLength, 0), 0) ?? 0) / this._channelCount;
6163
}
6264
return this._dataSize;
6365
}
@@ -111,10 +113,28 @@ export class AudioDecoder extends Events<AudioDecoderEvents> {
111113
}
112114

113115
/**
114-
* Total number of chunks to decode
116+
* Total number of chunks to decode.
117+
*
118+
* This is used to allocate the number of chunks to decode and to calculate the progress.
119+
* Influenced by the number of channels and the duration of the audio file, as this will cause errors
120+
* if the decoder tries to decode and return too much data at once.
121+
*
122+
* @example
123+
* 1hour 44.1kHz 2ch = 1 * 60 * 60 * 44100 * 2 = 158760000 samples -> 4 chunks (39690000 samples/chunk)
124+
* 1hour 44.1kHz 1ch = 1 * 60 * 60 * 44100 * 1 = 79380000 samples -> 2 chunks (39690000 samples/chunk)
125+
*/
126+
getTotalChunks() {
127+
return Math.ceil((this._duration * this._channelCount) / DURATION_CHUNK_SIZE);
128+
}
129+
130+
/**
131+
* Total size in duration seconds per chunk to decode.
132+
*
133+
* This is used to work out the number of samples to decode per chunk, as the decoder will
134+
* return an error if too much data is requested at once. This is influenced by the number of channels.
115135
*/
116-
getTotalChunks(duration: number) {
117-
return Math.ceil(duration / DURATION_CHUNK_SIZE);
136+
getChunkDuration() {
137+
return DURATION_CHUNK_SIZE / this._channelCount;
118138
}
119139

120140
/**
@@ -154,17 +174,22 @@ export class AudioDecoder extends Events<AudioDecoderEvents> {
154174
// This is a shared promise which will be observed by all instances of the same source
155175
this.decodingPromise = new Promise(resolve => (this.decodingResolve = resolve as any));
156176

177+
let splitChannels: SplitChannel | undefined = undefined;
178+
157179
try {
158180
// Set the worker instance and resolve the decoder promise
159-
this._channelCount = this.worker.channelCount;
181+
this._channelCount = options?.multiChannel ? this.worker.channelCount : 1;
160182
this._sampleRate = this.worker.sampleRate;
161183
this._duration = this.worker.duration;
162184

185+
163186
let chunkIndex = 0;
164-
const totalChunks = this.getTotalChunks(this.worker.duration);
187+
const totalChunks = this.getTotalChunks();
165188
const chunkIterator = this.chunkDecoder(options);
166189

167-
const chunks = Array.from({ length: totalChunks }) as Float32Array[];
190+
splitChannels = this._channelCount > 1 ? new SplitChannel(this._channelCount) : undefined;
191+
192+
const chunks = Array.from({ length: this._channelCount }).map(() => Array.from({ length: totalChunks }) as Float32Array[]);
168193

169194
info('decode:chunk:start', this.src, chunkIndex, totalChunks);
170195

@@ -183,7 +208,23 @@ export class AudioDecoder extends Events<AudioDecoderEvents> {
183208
if (this.sourceDecodeCancelled) return;
184209

185210
if (value) {
186-
chunks[chunkIndex] = value;
211+
// Only 1 channel, just copy the data of the chunk directly
212+
if (this._channelCount === 1) {
213+
chunks[0][chunkIndex] = value;
214+
} else {
215+
216+
if (!splitChannels) throw new Error('AudioDecoder: splitChannels not initialized');
217+
218+
// Multiple channels, split the data into separate channels within a web worker
219+
// This is done to avoid blocking the UI thread
220+
const channels = await splitChannels.split(value);
221+
222+
if (this.sourceDecodeCancelled) return;
223+
224+
channels.forEach((channel, index) => {
225+
chunks[index][chunkIndex] = channel;
226+
});
227+
}
187228
}
188229

189230
this.invoke('progress', [chunkIndex + 1, totalChunks]);
@@ -202,6 +243,7 @@ export class AudioDecoder extends Events<AudioDecoderEvents> {
202243

203244
info('decode:complete', this.src);
204245
} finally {
246+
splitChannels?.destroy();
205247
this.disposeWorker();
206248
}
207249
}
@@ -238,7 +280,7 @@ export class AudioDecoder extends Events<AudioDecoderEvents> {
238280
yield new Promise((resolve, reject) => {
239281
if (!this.worker || this.sourceDecodeCancelled) return resolve(null);
240282

241-
const nextChunkDuration = clamp(totalDuration - durationOffset, 0, DURATION_CHUNK_SIZE);
283+
const nextChunkDuration = clamp(totalDuration - durationOffset, 0, this.getChunkDuration());
242284
const currentOffset = durationOffset;
243285

244286
durationOffset += nextChunkDuration;

0 commit comments

Comments
 (0)