@@ -17,13 +17,18 @@ limitations under the License.
17
17
import EventEmitter from "events" ;
18
18
import { SimpleObservable } from "matrix-widget-api" ;
19
19
import { logger } from "matrix-js-sdk/src/logger" ;
20
+ import { defer } from "matrix-js-sdk/src/utils" ;
20
21
22
+ // @ts -ignore - `.ts` is needed here to make TS happy
23
+ import PlaybackWorker , { Request , Response } from "../workers/playback.worker.ts" ;
21
24
import { UPDATE_EVENT } from "../stores/AsyncStore" ;
22
- import { arrayFastResample , arrayRescale , arraySeed , arraySmoothingResample } from "../utils/arrays" ;
25
+ import { arrayFastResample } from "../utils/arrays" ;
23
26
import { IDestroyable } from "../utils/IDestroyable" ;
24
27
import { PlaybackClock } from "./PlaybackClock" ;
25
28
import { createAudioContext , decodeOgg } from "./compat" ;
26
29
import { clamp } from "../utils/numbers" ;
30
+ import { WorkerManager } from "../WorkerManager" ;
31
+ import { DEFAULT_WAVEFORM , PLAYBACK_WAVEFORM_SAMPLES } from "./consts" ;
27
32
28
33
export enum PlaybackState {
29
34
Decoding = "decoding" ,
@@ -32,25 +37,7 @@ export enum PlaybackState {
32
37
Playing = "playing" , // active progress through timeline
33
38
}
34
39
35
- export interface PlaybackInterface {
36
- readonly liveData : SimpleObservable < number [ ] > ;
37
- readonly timeSeconds : number ;
38
- readonly durationSeconds : number ;
39
- skipTo ( timeSeconds : number ) : Promise < void > ;
40
- }
41
-
42
- export const PLAYBACK_WAVEFORM_SAMPLES = 39 ;
43
40
const THUMBNAIL_WAVEFORM_SAMPLES = 100 ; // arbitrary: [30,120]
44
- export const DEFAULT_WAVEFORM = arraySeed ( 0 , PLAYBACK_WAVEFORM_SAMPLES ) ;
45
-
46
- function makePlaybackWaveform ( input : number [ ] ) : number [ ] {
47
- // First, convert negative amplitudes to positive so we don't detect zero as "noisy".
48
- const noiseWaveform = input . map ( ( v ) => Math . abs ( v ) ) ;
49
-
50
- // Then, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
51
- // We also rescale the waveform to be 0-1 so we end up with a clamped waveform to rely upon.
52
- return arrayRescale ( arraySmoothingResample ( noiseWaveform , PLAYBACK_WAVEFORM_SAMPLES ) , 0 , 1 ) ;
53
- }
54
41
55
42
export interface PlaybackInterface {
56
43
readonly currentState : PlaybackState ;
@@ -68,14 +55,15 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
68
55
public readonly thumbnailWaveform : number [ ] ;
69
56
70
57
private readonly context : AudioContext ;
71
- private source : AudioBufferSourceNode | MediaElementAudioSourceNode ;
58
+ private source ? : AudioBufferSourceNode | MediaElementAudioSourceNode ;
72
59
private state = PlaybackState . Decoding ;
73
- private audioBuf : AudioBuffer ;
74
- private element : HTMLAudioElement ;
60
+ private audioBuf ? : AudioBuffer ;
61
+ private element ? : HTMLAudioElement ;
75
62
private resampledWaveform : number [ ] ;
76
63
private waveformObservable = new SimpleObservable < number [ ] > ( ) ;
77
64
private readonly clock : PlaybackClock ;
78
65
private readonly fileSize : number ;
66
+ private readonly worker = new WorkerManager < Request , Response > ( PlaybackWorker ) ;
79
67
80
68
/**
81
69
* Creates a new playback instance from a buffer.
@@ -178,12 +166,11 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
178
166
// 5mb
179
167
logger . log ( "Audio file too large: processing through <audio /> element" ) ;
180
168
this . element = document . createElement ( "AUDIO" ) as HTMLAudioElement ;
181
- const prom = new Promise ( ( resolve , reject ) => {
182
- this . element . onloadeddata = ( ) => resolve ( null ) ;
183
- this . element . onerror = ( e ) => reject ( e ) ;
184
- } ) ;
169
+ const deferred = defer < unknown > ( ) ;
170
+ this . element . onloadeddata = deferred . resolve ;
171
+ this . element . onerror = deferred . reject ;
185
172
this . element . src = URL . createObjectURL ( new Blob ( [ this . buf ] ) ) ;
186
- await prom ; // make sure the audio element is ready for us
173
+ await deferred . promise ; // make sure the audio element is ready for us
187
174
} else {
188
175
// Safari compat: promise API not supported on this function
189
176
this . audioBuf = await new Promise ( ( resolve , reject ) => {
@@ -218,20 +205,23 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
218
205
219
206
// Update the waveform to the real waveform once we have channel data to use. We don't
220
207
// exactly trust the user-provided waveform to be accurate...
221
- const waveform = Array . from ( this . audioBuf . getChannelData ( 0 ) ) ;
222
- this . resampledWaveform = makePlaybackWaveform ( waveform ) ;
208
+ this . resampledWaveform = await this . makePlaybackWaveform ( this . audioBuf . getChannelData ( 0 ) ) ;
223
209
}
224
210
225
211
this . waveformObservable . update ( this . resampledWaveform ) ;
226
212
227
213
this . clock . flagLoadTime ( ) ; // must happen first because setting the duration fires a clock update
228
- this . clock . durationSeconds = this . element ? this . element . duration : this . audioBuf . duration ;
214
+ this . clock . durationSeconds = this . element ?. duration ?? this . audioBuf ! . duration ;
229
215
230
216
// Signal that we're not decoding anymore. This is done last to ensure the clock is updated for
231
217
// when the downstream callers try to use it.
232
218
this . emit ( PlaybackState . Stopped ) ; // signal that we're not decoding anymore
233
219
}
234
220
221
+ private makePlaybackWaveform ( input : Float32Array ) : Promise < number [ ] > {
222
+ return this . worker . call ( { data : Array . from ( input ) } ) . then ( ( resp ) => resp . waveform ) ;
223
+ }
224
+
235
225
private onPlaybackEnd = async ( ) : Promise < void > => {
236
226
await this . context . suspend ( ) ;
237
227
this . emit ( PlaybackState . Stopped ) ;
@@ -269,7 +259,7 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
269
259
this . source = this . context . createMediaElementSource ( this . element ) ;
270
260
} else {
271
261
this . source = this . context . createBufferSource ( ) ;
272
- this . source . buffer = this . audioBuf ;
262
+ this . source . buffer = this . audioBuf ?? null ;
273
263
}
274
264
275
265
this . source . addEventListener ( "ended" , this . onPlaybackEnd ) ;
0 commit comments