@readium/navigator 2.4.0-alpha.8 → 2.4.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1809 -1055
- package/dist/index.umd.cjs +170 -23
- package/package.json +10 -10
- package/src/Navigator.ts +55 -1
- package/src/audio/AudioNavigator.ts +497 -0
- package/src/audio/AudioPoolManager.ts +120 -0
- package/src/audio/engine/AudioEngine.ts +26 -10
- package/src/audio/engine/PreservePitchProcessor.js +149 -0
- package/src/audio/engine/PreservePitchWorklet.ts +79 -0
- package/src/audio/engine/WebAudioEngine.ts +558 -259
- package/src/audio/index.ts +3 -1
- package/src/audio/preferences/AudioDefaults.ts +43 -0
- package/src/audio/preferences/AudioPreferences.ts +54 -0
- package/src/audio/preferences/AudioPreferencesEditor.ts +123 -0
- package/src/audio/preferences/AudioSettings.ts +36 -0
- package/src/audio/preferences/index.ts +4 -0
- package/src/epub/EpubNavigator.ts +2 -2
- package/src/epub/frame/FrameBlobBuilder.ts +33 -84
- package/src/epub/frame/FramePoolManager.ts +23 -18
- package/src/epub/fxl/FXLFrameManager.ts +4 -11
- package/src/epub/fxl/FXLFramePoolManager.ts +22 -26
- package/src/epub/preferences/EpubPreferences.ts +4 -4
- package/src/injection/Injector.ts +5 -5
- package/src/preferences/Configurable.ts +2 -3
- package/src/preferences/PreferencesEditor.ts +1 -1
- package/src/preferences/Types.ts +19 -0
- package/src/webpub/WebPubNavigator.ts +1 -2
- package/src/webpub/preferences/WebPubPreferences.ts +3 -3
- package/types/src/Navigator.d.ts +46 -0
- package/types/src/audio/AudioNavigator.d.ts +79 -0
- package/types/src/audio/AudioPoolManager.d.ts +52 -0
- package/types/src/audio/engine/AudioEngine.d.ts +21 -7
- package/types/src/audio/engine/PreservePitchWorklet.d.ts +18 -0
- package/types/src/audio/engine/WebAudioEngine.d.ts +52 -7
- package/types/src/audio/index.d.ts +2 -0
- package/types/src/audio/preferences/AudioDefaults.d.ts +21 -0
- package/types/src/audio/preferences/AudioPreferences.d.ts +23 -0
- package/types/src/audio/preferences/AudioPreferencesEditor.d.ts +19 -0
- package/types/src/audio/preferences/AudioSettings.d.ts +24 -0
- package/types/src/audio/preferences/index.d.ts +4 -0
- package/types/src/epub/EpubNavigator.d.ts +2 -2
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +3 -6
- package/types/src/epub/fxl/FXLFrameManager.d.ts +0 -2
- package/types/src/epub/preferences/EpubPreferences.d.ts +2 -2
- package/types/src/preferences/Configurable.d.ts +2 -3
- package/types/src/preferences/PreferencesEditor.d.ts +1 -1
- package/types/src/preferences/Types.d.ts +3 -0
- package/types/src/webpub/WebPubNavigator.d.ts +2 -2
- package/types/src/webpub/preferences/WebPubPreferences.d.ts +2 -2
- package/LICENSE +0 -28
- package/src/divina/DivinaNavigator.ts +0 -0
- package/src/divina/index.ts +0 -0
- package/types/src/divina/DivinaNavigator.d.ts +0 -0
- package/types/src/divina/index.d.ts +0 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Publication } from "@readium/shared";
|
|
2
|
+
import { WebAudioEngine } from "./engine/WebAudioEngine";
|
|
3
|
+
|
|
4
|
+
export class AudioPoolManager {
|
|
5
|
+
private preloadedElements: Map<string, HTMLAudioElement> = new Map();
|
|
6
|
+
private _audioEngine: WebAudioEngine;
|
|
7
|
+
|
|
8
|
+
constructor(audioEngine: WebAudioEngine) {
|
|
9
|
+
this._audioEngine = audioEngine;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get audioEngine(): WebAudioEngine {
|
|
13
|
+
return this._audioEngine;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sets the current audio by href, using preloaded element if available or loading otherwise,
|
|
18
|
+
* and preloads adjacent tracks.
|
|
19
|
+
* @param href The URL of the audio resource.
|
|
20
|
+
* @param publication The publication containing the reading order.
|
|
21
|
+
* @param currentIndex The current track index.
|
|
22
|
+
* @param direction The navigation direction ('forward' or 'backward').
|
|
23
|
+
*/
|
|
24
|
+
setCurrentAudio(href: string, publication: Publication, currentIndex: number, direction: 'forward' | 'backward'): void {
|
|
25
|
+
// When Web Audio is active, preloaded elements lack crossOrigin="anonymous"
|
|
26
|
+
// and cannot be connected to MediaElementAudioSourceNode, so bypass the pool.
|
|
27
|
+
const preloadedElement = !this.audioEngine.isWebAudioActive ? this.get(href) : undefined;
|
|
28
|
+
if (preloadedElement) {
|
|
29
|
+
this.audioEngine.setMediaElement(preloadedElement);
|
|
30
|
+
this.clear(href);
|
|
31
|
+
} else {
|
|
32
|
+
this.clear(href);
|
|
33
|
+
this.audioEngine.loadAudio(href);
|
|
34
|
+
}
|
|
35
|
+
this.preloadAdjacent(publication, currentIndex, direction);
|
|
36
|
+
}
|
|
37
|
+
preload(href: string): void {
|
|
38
|
+
if (this.preloadedElements.has(href)) {
|
|
39
|
+
return; // Already preloaded
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const audioElement = document.createElement("audio");
|
|
43
|
+
audioElement.preload = "auto";
|
|
44
|
+
audioElement.src = href;
|
|
45
|
+
audioElement.load(); // Start buffering
|
|
46
|
+
|
|
47
|
+
this.preloadedElements.set(href, audioElement);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Retrieves a preloaded audio element by URL.
|
|
52
|
+
* @param href The URL of the audio resource.
|
|
53
|
+
* @returns The preloaded HTMLAudioElement, or undefined if not preloaded.
|
|
54
|
+
*/
|
|
55
|
+
get(href: string): HTMLAudioElement | undefined {
|
|
56
|
+
return this.preloadedElements.get(href);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Removes a preloaded element from the pool.
|
|
61
|
+
* @param href The URL of the audio resource.
|
|
62
|
+
*/
|
|
63
|
+
clear(href: string): void {
|
|
64
|
+
this.preloadedElements.delete(href);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Preloads the next track in the reading order.
|
|
69
|
+
* @param publication The publication containing the reading order.
|
|
70
|
+
* @param currentIndex The current track index.
|
|
71
|
+
*/
|
|
72
|
+
preloadNext(publication: Publication, currentIndex: number): void {
|
|
73
|
+
const nextIndex = currentIndex + 1;
|
|
74
|
+
if (nextIndex < publication.readingOrder.items.length) {
|
|
75
|
+
const nextLink = publication.readingOrder.items[nextIndex];
|
|
76
|
+
if (nextLink.href) {
|
|
77
|
+
this.preload(nextLink.href);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Preloads the previous track in the reading order.
|
|
84
|
+
* @param publication The publication containing the reading order.
|
|
85
|
+
* @param currentIndex The current track index.
|
|
86
|
+
*/
|
|
87
|
+
preloadPrevious(publication: Publication, currentIndex: number): void {
|
|
88
|
+
const prevIndex = currentIndex - 1;
|
|
89
|
+
if (prevIndex >= 0) {
|
|
90
|
+
const prevLink = publication.readingOrder.items[prevIndex];
|
|
91
|
+
if (prevLink.href) {
|
|
92
|
+
this.preload(prevLink.href);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Preloads adjacent tracks (previous and next) for smoother navigation.
|
|
99
|
+
* @param publication The publication containing the reading order.
|
|
100
|
+
* @param currentIndex The current track index.
|
|
101
|
+
* @param direction The navigation direction ('forward' or 'backward').
|
|
102
|
+
*/
|
|
103
|
+
preloadAdjacent(publication: Publication, currentIndex: number, direction: 'forward' | 'backward' = 'forward'): void {
|
|
104
|
+
if (direction === 'forward') {
|
|
105
|
+
this.preloadNext(publication, currentIndex);
|
|
106
|
+
this.preloadPrevious(publication, currentIndex);
|
|
107
|
+
} else {
|
|
108
|
+
this.preloadPrevious(publication, currentIndex);
|
|
109
|
+
this.preloadNext(publication, currentIndex);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Destroys the pool by stopping the engine and clearing all preloaded elements.
|
|
115
|
+
*/
|
|
116
|
+
destroy(): void {
|
|
117
|
+
this.audioEngine.stop();
|
|
118
|
+
this.preloadedElements.clear();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { Locator } from '@readium/shared';
|
|
2
|
-
import { Publication } from '@readium/shared';
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* Initial state of the audio engine playback.
|
|
6
3
|
*/
|
|
@@ -49,10 +46,11 @@ export interface AudioEngine {
|
|
|
49
46
|
playback: Playback;
|
|
50
47
|
|
|
51
48
|
/**
|
|
52
|
-
*
|
|
49
|
+
* Loads the audio resource at the given URL.
|
|
50
|
+
* @param url The URL of the audio resource.
|
|
53
51
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
loadAudio(url: string): void;
|
|
53
|
+
|
|
56
54
|
/**
|
|
57
55
|
* Adds an event listener to the audio engine.
|
|
58
56
|
* @param event The event name to listen.
|
|
@@ -66,12 +64,12 @@ export interface AudioEngine {
|
|
|
66
64
|
* @param callback Callback function to be removed.
|
|
67
65
|
*/
|
|
68
66
|
off(event: string, callback: (data: any) => void): void;
|
|
69
|
-
|
|
67
|
+
|
|
70
68
|
/**
|
|
71
|
-
*
|
|
72
|
-
* @param
|
|
69
|
+
* Sets the media element for playback, enabling use of preloaded elements from the pool.
|
|
70
|
+
* @param element The HTML audio element to use for playback.
|
|
73
71
|
*/
|
|
74
|
-
|
|
72
|
+
setMediaElement(element: HTMLAudioElement): void;
|
|
75
73
|
|
|
76
74
|
/**
|
|
77
75
|
* Plays the current audio resource.
|
|
@@ -97,6 +95,19 @@ export interface AudioEngine {
|
|
|
97
95
|
* Returns the duration of the audio resource.
|
|
98
96
|
*/
|
|
99
97
|
duration(): number;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Sets the volume of the audio resource.
|
|
101
|
+
* @param volume The volume to set, in the range [0, 1].
|
|
102
|
+
*/
|
|
103
|
+
setVolume(volume: number): void;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Sets the playback rate of the audio resource.
|
|
107
|
+
* @param rate The playback rate to set.
|
|
108
|
+
* @param preservePitch Whether to preserve pitch when changing playback rate.
|
|
109
|
+
*/
|
|
110
|
+
setPlaybackRate(rate: number, preservePitch: boolean): void;
|
|
100
111
|
|
|
101
112
|
/**
|
|
102
113
|
* Returns whether the audio resource is currently playing.
|
|
@@ -108,6 +119,11 @@ export interface AudioEngine {
|
|
|
108
119
|
*/
|
|
109
120
|
isPaused(): boolean;
|
|
110
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Returns the HTML media element used for playback.
|
|
124
|
+
*/
|
|
125
|
+
getMediaElement(): HTMLMediaElement;
|
|
126
|
+
|
|
111
127
|
/**
|
|
112
128
|
* Returns whether the audio resource is currently stopped.
|
|
113
129
|
*/
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// PreservePitchProcessor.js
|
|
2
|
+
// AudioWorklet processor for pitch preservation via pitch shifting
|
|
3
|
+
|
|
4
|
+
class PreservePitchProcessor extends AudioWorkletProcessor {
|
|
5
|
+
constructor() {
|
|
6
|
+
super();
|
|
7
|
+
this.bufferSize = 1024;
|
|
8
|
+
this.hopSize = 256;
|
|
9
|
+
this.overlap = this.bufferSize - this.hopSize;
|
|
10
|
+
this.inputBuffer = new Float32Array(this.bufferSize);
|
|
11
|
+
this.outputBuffer = new Float32Array(this.bufferSize);
|
|
12
|
+
this.window = new Float32Array(this.bufferSize);
|
|
13
|
+
this.bufferIndex = 0;
|
|
14
|
+
this.pitchFactor = 1.0;
|
|
15
|
+
|
|
16
|
+
// Hann window
|
|
17
|
+
for (let i = 0; i < this.bufferSize; i++) {
|
|
18
|
+
this.window[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / this.bufferSize));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.port.onmessage = (event) => {
|
|
22
|
+
if (event.data.type === 'setPitchFactor') {
|
|
23
|
+
this.pitchFactor = event.data.factor;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
process(inputs, outputs) {
|
|
29
|
+
const input = inputs[0];
|
|
30
|
+
const output = outputs[0];
|
|
31
|
+
|
|
32
|
+
if (!input || !output) return true;
|
|
33
|
+
|
|
34
|
+
const inputChannel = input[0];
|
|
35
|
+
const outputChannel = output[0];
|
|
36
|
+
|
|
37
|
+
// Accumulate input
|
|
38
|
+
for (let i = 0; i < inputChannel.length; i++) {
|
|
39
|
+
this.inputBuffer[this.bufferIndex] = inputChannel[i];
|
|
40
|
+
this.bufferIndex++;
|
|
41
|
+
|
|
42
|
+
if (this.bufferIndex >= this.bufferSize) {
|
|
43
|
+
// Process buffer
|
|
44
|
+
this.processBuffer();
|
|
45
|
+
// Output hopSize samples
|
|
46
|
+
for (let j = 0; j < this.hopSize; j++) {
|
|
47
|
+
outputChannel[j] = this.outputBuffer[j];
|
|
48
|
+
}
|
|
49
|
+
// Shift buffer
|
|
50
|
+
for (let j = 0; j < this.overlap; j++) {
|
|
51
|
+
this.inputBuffer[j] = this.inputBuffer[j + this.hopSize];
|
|
52
|
+
}
|
|
53
|
+
this.bufferIndex = this.overlap;
|
|
54
|
+
// Clear output buffer for next
|
|
55
|
+
this.outputBuffer.fill(0);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
processBuffer() {
|
|
63
|
+
// Apply window
|
|
64
|
+
let windowed = new Float32Array(this.bufferSize);
|
|
65
|
+
for (let i = 0; i < this.bufferSize; i++) {
|
|
66
|
+
windowed[i] = this.inputBuffer[i] * this.window[i];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// FFT
|
|
70
|
+
let fftResult = this.fft(windowed);
|
|
71
|
+
|
|
72
|
+
// Pitch shift
|
|
73
|
+
let shifted = this.pitchShift(fftResult, this.pitchFactor);
|
|
74
|
+
|
|
75
|
+
// IFFT
|
|
76
|
+
let ifftResult = this.ifft(shifted);
|
|
77
|
+
|
|
78
|
+
// Overlap-add
|
|
79
|
+
for (let i = 0; i < this.bufferSize; i++) {
|
|
80
|
+
this.outputBuffer[i] += ifftResult[i] * this.window[i];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pitchShift(fft, factor) {
|
|
85
|
+
let N = fft.length;
|
|
86
|
+
let result = new Array(N).fill(null).map(() => ({ real: 0, imag: 0 }));
|
|
87
|
+
for (let k = 0; k < N / 2; k++) {
|
|
88
|
+
let newK = Math.round(k * factor);
|
|
89
|
+
if (newK < N / 2) {
|
|
90
|
+
result[newK] = fft[k];
|
|
91
|
+
result[N - newK] = fft[N - k]; // symmetric for real input
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fft(input) {
|
|
98
|
+
let N = input.length;
|
|
99
|
+
if (N <= 1) return [{ real: input[0] || 0, imag: 0 }];
|
|
100
|
+
if ((N & (N - 1)) !== 0) throw new Error('N must be power of 2');
|
|
101
|
+
|
|
102
|
+
let even = this.fft(input.filter((_, i) => i % 2 === 0));
|
|
103
|
+
let odd = this.fft(input.filter((_, i) => i % 2 === 1));
|
|
104
|
+
|
|
105
|
+
let result = new Array(N);
|
|
106
|
+
for (let k = 0; k < N / 2; k++) {
|
|
107
|
+
let t = odd[k];
|
|
108
|
+
let angle = -2 * Math.PI * k / N;
|
|
109
|
+
let twiddle = { real: Math.cos(angle), imag: Math.sin(angle) };
|
|
110
|
+
let twiddled = {
|
|
111
|
+
real: t.real * twiddle.real - t.imag * twiddle.imag,
|
|
112
|
+
imag: t.real * twiddle.imag + t.imag * twiddle.real
|
|
113
|
+
};
|
|
114
|
+
result[k] = {
|
|
115
|
+
real: even[k].real + twiddled.real,
|
|
116
|
+
imag: even[k].imag + twiddled.imag
|
|
117
|
+
};
|
|
118
|
+
result[k + N / 2] = {
|
|
119
|
+
real: even[k].real - twiddled.real,
|
|
120
|
+
imag: even[k].imag - twiddled.imag
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
ifft(input) {
|
|
127
|
+
let N = input.length;
|
|
128
|
+
// Conjugate
|
|
129
|
+
let conj = input.map(c => ({ real: c.real, imag: -c.imag }));
|
|
130
|
+
// FFT
|
|
131
|
+
let fftConj = this.fft(conj.map(c => c.real)); // wait, fft expects real array
|
|
132
|
+
// FFT on complex is needed, but simplify
|
|
133
|
+
// For simplicity, implement IFFT similarly
|
|
134
|
+
let result = new Float32Array(N);
|
|
135
|
+
for (let n = 0; n < N; n++) {
|
|
136
|
+
let sumReal = 0, sumImag = 0;
|
|
137
|
+
for (let k = 0; k < N; k++) {
|
|
138
|
+
let angle = 2 * Math.PI * k * n / N;
|
|
139
|
+
let c = input[k];
|
|
140
|
+
sumReal += c.real * Math.cos(angle) - c.imag * Math.sin(angle);
|
|
141
|
+
sumImag += c.real * Math.sin(angle) + c.imag * Math.cos(angle);
|
|
142
|
+
}
|
|
143
|
+
result[n] = sumReal / N;
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
registerProcessor('preserve-pitch-processor', PreservePitchProcessor);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
interface PreservePitchWorkletOptions {
|
|
2
|
+
ctx: AudioContext;
|
|
3
|
+
mediaElement?: HTMLMediaElement;
|
|
4
|
+
pitchFactor?: number;
|
|
5
|
+
modulePath?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
import processorCode from './PreservePitchProcessor.js?raw';
|
|
9
|
+
|
|
10
|
+
export class PreservePitchWorklet {
|
|
11
|
+
mediaElement: HTMLMediaElement | null = null;
|
|
12
|
+
source: MediaElementAudioSourceNode | null = null;
|
|
13
|
+
ctx: AudioContext;
|
|
14
|
+
workletNode: AudioWorkletNode | null = null;
|
|
15
|
+
url: string | null = null;
|
|
16
|
+
|
|
17
|
+
static async createWorklet(options: PreservePitchWorkletOptions): Promise<PreservePitchWorklet> {
|
|
18
|
+
const { ctx, mediaElement, pitchFactor, modulePath } = options;
|
|
19
|
+
const worklet = new PreservePitchWorklet(ctx);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
if (modulePath) {
|
|
23
|
+
await ctx.audioWorklet.addModule(modulePath);
|
|
24
|
+
} else {
|
|
25
|
+
const blob = new Blob([processorCode], { type: 'text/javascript' });
|
|
26
|
+
worklet.url = URL.createObjectURL(blob);
|
|
27
|
+
await ctx.audioWorklet.addModule(worklet.url);
|
|
28
|
+
}
|
|
29
|
+
} catch (err) {
|
|
30
|
+
worklet.destroy();
|
|
31
|
+
throw new Error(`Error adding module: ${err}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
worklet.workletNode = new AudioWorkletNode(ctx, 'preserve-pitch-processor');
|
|
36
|
+
|
|
37
|
+
if (pitchFactor) {
|
|
38
|
+
worklet.updatePitchFactor(pitchFactor);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (mediaElement) {
|
|
42
|
+
const source = ctx.createMediaElementSource(mediaElement);
|
|
43
|
+
source.connect(worklet.workletNode);
|
|
44
|
+
worklet.mediaElement = mediaElement;
|
|
45
|
+
worklet.source = source;
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
worklet.destroy();
|
|
49
|
+
throw new Error(`Error creating worklet node: ${err}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return worklet;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
constructor(ctx: AudioContext) {
|
|
56
|
+
this.ctx = ctx;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
updatePitchFactor(factor: number): void {
|
|
60
|
+
if (this.workletNode) {
|
|
61
|
+
this.workletNode.port.postMessage({ type: 'setPitchFactor', factor });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
destroy(): void {
|
|
66
|
+
if (this.workletNode) {
|
|
67
|
+
this.workletNode.disconnect();
|
|
68
|
+
this.workletNode = null;
|
|
69
|
+
}
|
|
70
|
+
if (this.source) {
|
|
71
|
+
this.source.disconnect();
|
|
72
|
+
this.source = null;
|
|
73
|
+
}
|
|
74
|
+
if (this.url) {
|
|
75
|
+
URL.revokeObjectURL(this.url);
|
|
76
|
+
this.url = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|