@ain1084/audio-worklet-stream 0.1.2
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/LICENSE +222 -0
- package/README.md +243 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +15 -0
- package/dist/constants.js.map +1 -0
- package/dist/events.d.ts +37 -0
- package/dist/events.js +35 -0
- package/dist/events.js.map +1 -0
- package/dist/frame-buffer/buffer-factory.d.ts +77 -0
- package/dist/frame-buffer/buffer-factory.js +52 -0
- package/dist/frame-buffer/buffer-factory.js.map +1 -0
- package/dist/frame-buffer/buffer-filler.d.ts +13 -0
- package/dist/frame-buffer/buffer-filler.js +2 -0
- package/dist/frame-buffer/buffer-filler.js.map +1 -0
- package/dist/frame-buffer/buffer-reader.d.ts +37 -0
- package/dist/frame-buffer/buffer-reader.js +51 -0
- package/dist/frame-buffer/buffer-reader.js.map +1 -0
- package/dist/frame-buffer/buffer-utils.d.ts +34 -0
- package/dist/frame-buffer/buffer-utils.js +34 -0
- package/dist/frame-buffer/buffer-utils.js.map +1 -0
- package/dist/frame-buffer/buffer-writer.d.ts +37 -0
- package/dist/frame-buffer/buffer-writer.js +51 -0
- package/dist/frame-buffer/buffer-writer.js.map +1 -0
- package/dist/frame-buffer/buffer.d.ts +40 -0
- package/dist/frame-buffer/buffer.js +65 -0
- package/dist/frame-buffer/buffer.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/output-message.d.ts +42 -0
- package/dist/output-message.js +2 -0
- package/dist/output-message.js.map +1 -0
- package/dist/output-stream-node.d.ts +93 -0
- package/dist/output-stream-node.js +166 -0
- package/dist/output-stream-node.js.map +1 -0
- package/dist/output-stream-processor.d.ts +13 -0
- package/dist/output-stream-processor.js +99 -0
- package/dist/output-stream-processor.js.map +1 -0
- package/dist/stream-node-factory.d.ts +87 -0
- package/dist/stream-node-factory.js +113 -0
- package/dist/stream-node-factory.js.map +1 -0
- package/dist/write-strategy/manual.d.ts +36 -0
- package/dist/write-strategy/manual.js +43 -0
- package/dist/write-strategy/manual.js.map +1 -0
- package/dist/write-strategy/strategy.d.ts +23 -0
- package/dist/write-strategy/strategy.js +2 -0
- package/dist/write-strategy/strategy.js.map +1 -0
- package/dist/write-strategy/timed.d.ts +36 -0
- package/dist/write-strategy/timed.js +92 -0
- package/dist/write-strategy/timed.js.map +1 -0
- package/dist/write-strategy/worker/message.d.ts +54 -0
- package/dist/write-strategy/worker/message.js +2 -0
- package/dist/write-strategy/worker/message.js.map +1 -0
- package/dist/write-strategy/worker/strategy.d.ts +34 -0
- package/dist/write-strategy/worker/strategy.js +125 -0
- package/dist/write-strategy/worker/strategy.js.map +1 -0
- package/dist/write-strategy/worker/worker.d.ts +35 -0
- package/dist/write-strategy/worker/worker.js +135 -0
- package/dist/write-strategy/worker/worker.js.map +1 -0
- package/package.json +54 -0
- package/src/constants.ts +18 -0
- package/src/events.ts +43 -0
- package/src/frame-buffer/buffer-factory.ts +115 -0
- package/src/frame-buffer/buffer-filler.ts +14 -0
- package/src/frame-buffer/buffer-reader.ts +56 -0
- package/src/frame-buffer/buffer-utils.ts +48 -0
- package/src/frame-buffer/buffer-writer.ts +56 -0
- package/src/frame-buffer/buffer.ts +68 -0
- package/src/index.ts +9 -0
- package/src/output-message.ts +37 -0
- package/src/output-stream-node.ts +197 -0
- package/src/output-stream-processor.ts +124 -0
- package/src/stream-node-factory.ts +161 -0
- package/src/write-strategy/manual.ts +50 -0
- package/src/write-strategy/strategy.ts +26 -0
- package/src/write-strategy/timed.ts +103 -0
- package/src/write-strategy/worker/message.ts +48 -0
- package/src/write-strategy/worker/strategy.ts +154 -0
- package/src/write-strategy/worker/worker.ts +149 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { FrameBuffer } from './buffer'
|
|
2
|
+
import { FrameBufferWriter } from './buffer-writer'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parameters for creating a FrameBuffer.
|
|
6
|
+
* @property frameBufferSize - The size of the frame buffer.
|
|
7
|
+
* @property channelCount - The number of audio channels.
|
|
8
|
+
*/
|
|
9
|
+
export type FrameBufferParams = Readonly<{
|
|
10
|
+
frameBufferSize: number
|
|
11
|
+
channelCount: number
|
|
12
|
+
}>
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parameters for creating a FillerFrameBuffer.
|
|
16
|
+
* @property channelCount - The number of audio channels.
|
|
17
|
+
* @property fillInterval - The interval in milliseconds for filling the buffer.
|
|
18
|
+
* @property sampleRate - The sample rate of the audio context.
|
|
19
|
+
* @property frameBufferChunks - The number of chunks in the frame buffer.
|
|
20
|
+
*/
|
|
21
|
+
export type FillerFrameBufferParams = Readonly<{
|
|
22
|
+
channelCount: number
|
|
23
|
+
fillInterval?: number
|
|
24
|
+
sampleRate?: number
|
|
25
|
+
frameBufferChunks?: number
|
|
26
|
+
}>
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Configuration for a FrameBuffer.
|
|
30
|
+
* This configuration is returned by the createFrameBufferConfig function.
|
|
31
|
+
* @property sampleBuffer - The shared buffer for audio data frames.
|
|
32
|
+
* @property samplesPerFrame - The number of samples per frame.
|
|
33
|
+
* @property usedFramesInBuffer - The usage count of the frames in the buffer.
|
|
34
|
+
* @property totalReadFrames - The total frames read from the buffer.
|
|
35
|
+
* @property totalWriteFrames - The total frames written to the buffer.
|
|
36
|
+
*/
|
|
37
|
+
export type FrameBufferConfig = Readonly<{
|
|
38
|
+
sampleBuffer: Float32Array
|
|
39
|
+
samplesPerFrame: number
|
|
40
|
+
usedFramesInBuffer: Uint32Array
|
|
41
|
+
totalReadFrames: BigUint64Array
|
|
42
|
+
totalWriteFrames: BigUint64Array
|
|
43
|
+
}>
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Configuration for a FillerFrameBuffer.
|
|
47
|
+
* This configuration is returned by the createFillerFrameBufferConfig function.
|
|
48
|
+
* @property sampleRate - The sample rate of the audio context.
|
|
49
|
+
* @property fillInterval - The interval in milliseconds for filling the buffer.
|
|
50
|
+
*/
|
|
51
|
+
export type FillerFrameBufferConfig = FrameBufferConfig & Readonly<{
|
|
52
|
+
sampleRate: number
|
|
53
|
+
fillInterval: number
|
|
54
|
+
}>
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates a FrameBufferWriter instance.
|
|
58
|
+
* @param config - The configuration for the FrameBuffer.
|
|
59
|
+
* @returns A new instance of FrameBufferWriter.
|
|
60
|
+
*/
|
|
61
|
+
export const createFrameBufferWriter = (config: FrameBufferConfig): FrameBufferWriter => {
|
|
62
|
+
return new FrameBufferWriter(
|
|
63
|
+
new FrameBuffer(config.sampleBuffer, config.samplesPerFrame),
|
|
64
|
+
config.usedFramesInBuffer, config.totalWriteFrames,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* FrameBufferFactory class
|
|
70
|
+
* Provides static methods to create frame buffer configurations and writers.
|
|
71
|
+
*/
|
|
72
|
+
export class FrameBufferFactory {
|
|
73
|
+
public static readonly DEFAULT_FILL_INTERVAL_MS = 20
|
|
74
|
+
public static readonly DEFAULT_FRAME_BUFFER_CHUNKS = 5
|
|
75
|
+
public static readonly PROCESS_UNIT = 128
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Creates a FrameBufferConfig instance.
|
|
79
|
+
* @param params - The parameters for the FrameBuffer.
|
|
80
|
+
* @returns A new instance of FrameBufferConfig.
|
|
81
|
+
*/
|
|
82
|
+
public static createFrameBufferConfig(params: FrameBufferParams): FrameBufferConfig {
|
|
83
|
+
return {
|
|
84
|
+
sampleBuffer: new Float32Array(
|
|
85
|
+
new SharedArrayBuffer(params.frameBufferSize * params.channelCount * Float32Array.BYTES_PER_ELEMENT),
|
|
86
|
+
),
|
|
87
|
+
samplesPerFrame: params.channelCount,
|
|
88
|
+
usedFramesInBuffer: new Uint32Array(new SharedArrayBuffer(Uint32Array.BYTES_PER_ELEMENT)),
|
|
89
|
+
totalReadFrames: new BigUint64Array(new SharedArrayBuffer(BigUint64Array.BYTES_PER_ELEMENT)),
|
|
90
|
+
totalWriteFrames: new BigUint64Array(new SharedArrayBuffer(BigUint64Array.BYTES_PER_ELEMENT)),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Creates a FillerFrameBufferConfig instance.
|
|
96
|
+
* @param defaultSampleRate - The sample rate of the audio context.
|
|
97
|
+
* @param params - The parameters for the FillerFrameBuffer.
|
|
98
|
+
* @returns A new instance of FillerFrameBufferConfig.
|
|
99
|
+
*/
|
|
100
|
+
public static createFillerFrameBufferConfig(defaultSampleRate: number, params: FillerFrameBufferParams): FillerFrameBufferConfig {
|
|
101
|
+
const sampleRate = params.sampleRate ?? defaultSampleRate
|
|
102
|
+
const intervalMillisecond = params.fillInterval ?? FrameBufferFactory.DEFAULT_FILL_INTERVAL_MS
|
|
103
|
+
const frameBufferSize = Math.floor(
|
|
104
|
+
sampleRate * intervalMillisecond / 1000 + (FrameBufferFactory.PROCESS_UNIT - 1),
|
|
105
|
+
) & ~(FrameBufferFactory.PROCESS_UNIT - 1)
|
|
106
|
+
const frameBufferChunkCount = params.frameBufferChunks ?? FrameBufferFactory.DEFAULT_FRAME_BUFFER_CHUNKS
|
|
107
|
+
const config = FrameBufferFactory.createFrameBufferConfig(
|
|
108
|
+
{ frameBufferSize: frameBufferSize * frameBufferChunkCount, channelCount: params.channelCount })
|
|
109
|
+
return {
|
|
110
|
+
...config,
|
|
111
|
+
sampleRate,
|
|
112
|
+
fillInterval: intervalMillisecond,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FrameBufferWriter } from './buffer-writer'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FrameBufferFiller interface
|
|
5
|
+
* This interface defines a method to fill audio frames into a buffer.
|
|
6
|
+
*/
|
|
7
|
+
export interface FrameBufferFiller {
|
|
8
|
+
/**
|
|
9
|
+
* Fill the buffer with audio frames using the provided writer.
|
|
10
|
+
* @param writer - An instance of FrameBufferWriter used to write audio frames to the buffer.
|
|
11
|
+
* @returns A boolean indicating whether to continue playback.
|
|
12
|
+
*/
|
|
13
|
+
fill(writer: FrameBufferWriter): boolean
|
|
14
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { enumFrames, type FrameCallback } from './buffer-utils'
|
|
2
|
+
import { FrameBuffer } from './buffer'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FrameBufferReader class
|
|
6
|
+
* This class reads audio frame data from a shared Float32Array buffer and processes it.
|
|
7
|
+
* The buffer usage is tracked using a Uint32Array.
|
|
8
|
+
*/
|
|
9
|
+
export class FrameBufferReader {
|
|
10
|
+
private readonly _frameBuffer: FrameBuffer
|
|
11
|
+
private readonly _usedFrameInBuffer: Uint32Array
|
|
12
|
+
private readonly _totalFrames: BigUint64Array
|
|
13
|
+
private _index: number = 0
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates an instance of FrameBufferReader.
|
|
17
|
+
* @param frameBuffer - The shared buffer to read from.
|
|
18
|
+
* @param usedFrameInBuffer - The Uint32Array tracking the usage of the buffer.
|
|
19
|
+
* @param totalFrames - The BigUint64Array tracking the total frames read from the buffer.
|
|
20
|
+
*/
|
|
21
|
+
constructor(frameBuffer: FrameBuffer, usedFrameInBuffer: Uint32Array, totalFrames: BigUint64Array) {
|
|
22
|
+
this._frameBuffer = frameBuffer
|
|
23
|
+
this._usedFrameInBuffer = usedFrameInBuffer
|
|
24
|
+
this._totalFrames = totalFrames
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the number of available frames in the buffer.
|
|
29
|
+
* @returns The number of available frames in the buffer.
|
|
30
|
+
*/
|
|
31
|
+
public get available(): number {
|
|
32
|
+
return Atomics.load(this._usedFrameInBuffer, 0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the total number of frames read from the buffer.
|
|
37
|
+
* @returns The total number of frames read.
|
|
38
|
+
*/
|
|
39
|
+
public get totalFrames(): bigint {
|
|
40
|
+
return this._totalFrames[0]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reads audio frame data from the buffer and processes it using the provided callback.
|
|
45
|
+
* @param frameCallback - The callback function to process each section of the buffer.
|
|
46
|
+
* @returns The number of frames processed.
|
|
47
|
+
* @throws RangeError - If the processed length exceeds the part length.
|
|
48
|
+
*/
|
|
49
|
+
public read(frameCallback: FrameCallback): number {
|
|
50
|
+
const result = enumFrames(this._frameBuffer, this._index, this.available, frameCallback)
|
|
51
|
+
this._index = result.nextIndex
|
|
52
|
+
Atomics.sub(this._usedFrameInBuffer, 0, result.frames)
|
|
53
|
+
Atomics.add(this._totalFrames, 0, BigInt(result.frames))
|
|
54
|
+
return result.frames
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { FrameBuffer } from './buffer'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type definition for the callback function used in enumFrames.
|
|
5
|
+
* The callback processes each section of the frame buffer.
|
|
6
|
+
* @param frame - An object containing:
|
|
7
|
+
* - buffer: The FrameBuffer instance.
|
|
8
|
+
* - index: The starting index in the buffer.
|
|
9
|
+
* - frames: The number of frames in the section.
|
|
10
|
+
* @param offset - The offset in the buffer from the start of processing.
|
|
11
|
+
* @returns The number of frames processed.
|
|
12
|
+
*/
|
|
13
|
+
export type FrameCallback = (frame: { buffer: FrameBuffer, index: number, frames: number }, offset: number) => number
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Processes sections of a Float32Array buffer using a callback function.
|
|
17
|
+
* This function is intended for internal use only.
|
|
18
|
+
*
|
|
19
|
+
* @param buffer - The FrameBuffer to process. This buffer is expected to be shared.
|
|
20
|
+
* @param startIndex - The starting index in the buffer from where processing should begin.
|
|
21
|
+
* @param availableFrames - The total number of frames available to process in the buffer.
|
|
22
|
+
* @param frameCallback - The callback function to process each section of the buffer.
|
|
23
|
+
* It should return the number of frames processed.
|
|
24
|
+
* @returns An object containing:
|
|
25
|
+
* - frames: The number of frames successfully processed.
|
|
26
|
+
* - nextIndex: The index in the buffer for the next processing cycle.
|
|
27
|
+
* @throws RangeError - If the frameCallback returns a processed length greater than the part length.
|
|
28
|
+
*/
|
|
29
|
+
export const enumFrames = (buffer: FrameBuffer, startIndex: number, availableFrames: number, frameCallback: FrameCallback):
|
|
30
|
+
{ frames: number, nextIndex: number } => {
|
|
31
|
+
let totalFrames = 0
|
|
32
|
+
while (totalFrames < availableFrames) {
|
|
33
|
+
// Determine the length of the current section to process
|
|
34
|
+
const sectionFrames = Math.min(buffer.length - startIndex, availableFrames - totalFrames)
|
|
35
|
+
// Process the current section using the frameCallback function
|
|
36
|
+
const processedFrames = frameCallback({ buffer, index: startIndex, frames: sectionFrames }, totalFrames)
|
|
37
|
+
// Ensure the processed length does not exceed the section length
|
|
38
|
+
if (processedFrames > sectionFrames) {
|
|
39
|
+
throw new RangeError(`Processed frames (${processedFrames}) exceeds section frames (${sectionFrames})`)
|
|
40
|
+
}
|
|
41
|
+
totalFrames += processedFrames
|
|
42
|
+
startIndex = (startIndex + processedFrames) % buffer.length
|
|
43
|
+
if (processedFrames < sectionFrames) {
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { frames: totalFrames, nextIndex: startIndex }
|
|
48
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { enumFrames, type FrameCallback } from './buffer-utils'
|
|
2
|
+
import { FrameBuffer } from './buffer'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FrameBufferWriter class
|
|
6
|
+
* This class writes audio frame data to a shared Float32Array buffer.
|
|
7
|
+
* The buffer usage is tracked using a Uint32Array.
|
|
8
|
+
*/
|
|
9
|
+
export class FrameBufferWriter {
|
|
10
|
+
private readonly _frameBuffer: FrameBuffer
|
|
11
|
+
private readonly _usedFrameInBuffer: Uint32Array
|
|
12
|
+
private readonly _totalFrames: BigUint64Array
|
|
13
|
+
private _index: number = 0
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates an instance of FrameBufferWriter.
|
|
17
|
+
* @param frameBuffer - The shared buffer to write to.
|
|
18
|
+
* @param usedFrameInBuffer - The Uint32Array tracking the usage of the buffer.
|
|
19
|
+
* @param totalFrames - The BigUint64Array tracking the total frames written to the buffer.
|
|
20
|
+
*/
|
|
21
|
+
constructor(frameBuffer: FrameBuffer, usedFrameInBuffer: Uint32Array, totalFrames: BigUint64Array) {
|
|
22
|
+
this._frameBuffer = frameBuffer
|
|
23
|
+
this._usedFrameInBuffer = usedFrameInBuffer
|
|
24
|
+
this._totalFrames = totalFrames
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the number of available spaces in the buffer.
|
|
29
|
+
* @returns The number of available spaces in the buffer.
|
|
30
|
+
*/
|
|
31
|
+
public get available(): number {
|
|
32
|
+
return this._frameBuffer.length - Atomics.load(this._usedFrameInBuffer, 0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the total number of frames written to the buffer.
|
|
37
|
+
* @returns The total number of frames written.
|
|
38
|
+
*/
|
|
39
|
+
public get totalFrames(): bigint {
|
|
40
|
+
return this._totalFrames[0]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Write audio frame data to the buffer using the provided frameCallback.
|
|
45
|
+
* @param frameCallback - The frameCallback function to process each section of the buffer.
|
|
46
|
+
* @returns The number of frames processed.
|
|
47
|
+
* @throws RangeError - If the processed length exceeds the part length.
|
|
48
|
+
*/
|
|
49
|
+
public write(frameCallback: FrameCallback): number {
|
|
50
|
+
const result = enumFrames(this._frameBuffer, this._index, this.available, frameCallback)
|
|
51
|
+
this._index = result.nextIndex
|
|
52
|
+
Atomics.add(this._usedFrameInBuffer, 0, result.frames)
|
|
53
|
+
Atomics.add(this._totalFrames, 0, BigInt(result.frames))
|
|
54
|
+
return result.frames
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FrameBuffer class
|
|
3
|
+
* This class manages a buffer of audio frames.
|
|
4
|
+
*/
|
|
5
|
+
export class FrameBuffer {
|
|
6
|
+
private readonly _buffer: Float32Array
|
|
7
|
+
private readonly _samplesPerFrame: number
|
|
8
|
+
private readonly _length: number
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates an instance of FrameBuffer.
|
|
12
|
+
* @param buffer - The Float32Array buffer to manage.
|
|
13
|
+
* @param samplesPerFrame - The number of samples per frame.
|
|
14
|
+
*/
|
|
15
|
+
public constructor(buffer: Float32Array, samplesPerFrame: number) {
|
|
16
|
+
this._buffer = buffer
|
|
17
|
+
this._samplesPerFrame = samplesPerFrame
|
|
18
|
+
this._length = Math.floor(buffer.length / samplesPerFrame)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sets the frame data in the buffer.
|
|
23
|
+
* @param index - The starting index in the buffer.
|
|
24
|
+
* @param samples - The samples to set in the buffer.
|
|
25
|
+
* @param sampleStart - The starting position in the samples array (default is 0).
|
|
26
|
+
* @param sampleCount - The number of samples to set (default is the length of the samples array).
|
|
27
|
+
* @returns A number of written frames.
|
|
28
|
+
* @throws Error - If the number of samples per frame does not match the specified number of samples.
|
|
29
|
+
*/
|
|
30
|
+
public setFrames(index: number, samples: Float32Array, sampleStart: number = 0, sampleCount?: number): number {
|
|
31
|
+
index *= this._samplesPerFrame
|
|
32
|
+
const sampleEnd = Math.min((sampleCount !== undefined) ? (sampleStart + sampleCount) : samples.length, samples.length)
|
|
33
|
+
const frames = (sampleEnd - sampleStart) / this._samplesPerFrame
|
|
34
|
+
if (!Number.isInteger(frames)) {
|
|
35
|
+
throw new Error(`Error: The number of samples per frame does not match the specified number of samples. Expected samples per frame: ${this._samplesPerFrame}, but got: ${sampleEnd - sampleStart}.`)
|
|
36
|
+
}
|
|
37
|
+
for (let sampleIndex = sampleStart; sampleIndex < sampleEnd; ++sampleIndex, ++index) {
|
|
38
|
+
this._buffer[index] = samples[sampleIndex]
|
|
39
|
+
}
|
|
40
|
+
return frames
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Converts the frame data to output.
|
|
45
|
+
* This method is intended to be called from within the process method of the AudioWorkletProcessor.
|
|
46
|
+
* It converts the interleaved frame data to the structure expected by the process method's outputs.
|
|
47
|
+
* @param frameIndex - The index of the frame to convert.
|
|
48
|
+
* @param frames - The number of frames to convert.
|
|
49
|
+
* @param output - The output array to store the converted data.
|
|
50
|
+
* @param outputOffset - The offset in the output array at which to start storing the data.
|
|
51
|
+
*/
|
|
52
|
+
public convertToOutput(frameIndex: number, frames: number, output: Float32Array[], outputOffset: number): void {
|
|
53
|
+
const samplesPerFrame = this._samplesPerFrame
|
|
54
|
+
output.forEach((outputChannel, channelNumber) => {
|
|
55
|
+
for (let i = channelNumber, j = 0; j < frames; i += samplesPerFrame, ++j) {
|
|
56
|
+
outputChannel[outputOffset + j] = this._buffer[frameIndex + i]
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Gets the length of the buffer in frames.
|
|
63
|
+
* @returns The length of the buffer in frames.
|
|
64
|
+
*/
|
|
65
|
+
public get length(): number {
|
|
66
|
+
return this._length
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { StopEvent, UnderrunEvent } from './events'
|
|
2
|
+
export type { OutputStreamNode } from './output-stream-node'
|
|
3
|
+
export type { FrameBufferWriter } from './frame-buffer/buffer-writer'
|
|
4
|
+
export type { FrameBufferFiller } from './frame-buffer/buffer-filler'
|
|
5
|
+
export { StreamNodeFactory,
|
|
6
|
+
type ManualBufferNodeParams,
|
|
7
|
+
type TimedBufferNodeParams,
|
|
8
|
+
} from './stream-node-factory'
|
|
9
|
+
export { BufferFillWorker } from './write-strategy/worker/worker'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messages sent from the processor to the main thread.
|
|
3
|
+
*
|
|
4
|
+
* @typeParam type - The type of message. Can be 'stop' or 'underrun'.
|
|
5
|
+
*
|
|
6
|
+
* For 'stop' messages:
|
|
7
|
+
* @property frames - The total frames processed when stopped.
|
|
8
|
+
*
|
|
9
|
+
* For 'underrun' messages:
|
|
10
|
+
* @property frames - The number of frames that have been underrun.
|
|
11
|
+
*
|
|
12
|
+
* Example:
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const message: MessageToAudioNode = { type: 'stop', frames: 1000n };
|
|
15
|
+
* // or
|
|
16
|
+
* const message: MessageToAudioNode = { type: 'underrun', frames: 256 };
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export type MessageToAudioNode =
|
|
20
|
+
| { type: 'stop', frames: bigint }
|
|
21
|
+
| { type: 'underrun', frames: number }
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Messages sent from the main thread to the processor.
|
|
25
|
+
*
|
|
26
|
+
* @typeParam type - The type of message. Can be 'stop'.
|
|
27
|
+
*
|
|
28
|
+
* For 'stop' messages:
|
|
29
|
+
* @property frames - The position in frames to stop at.
|
|
30
|
+
*
|
|
31
|
+
* Example:
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const message: MessageToProcessor = { type: 'stop', frames: 1000n };
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export type MessageToProcessor =
|
|
37
|
+
| { type: 'stop', frames: bigint }
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { MessageToProcessor, MessageToAudioNode } from './output-message'
|
|
2
|
+
import type { OutputStreamProcessorOptions } from './output-stream-processor'
|
|
3
|
+
import { PROCESSOR_NAME } from './constants'
|
|
4
|
+
import { StopEvent, UnderrunEvent } from './events'
|
|
5
|
+
import { BufferWriteStrategy } from './write-strategy/strategy'
|
|
6
|
+
import { FrameBufferConfig } from './frame-buffer/buffer-factory'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Stream state
|
|
10
|
+
* Represents the different states of the stream.
|
|
11
|
+
*/
|
|
12
|
+
export type StreamState =
|
|
13
|
+
// Initial state where playback can start
|
|
14
|
+
| 'ready'
|
|
15
|
+
// State where playback has started
|
|
16
|
+
| 'started'
|
|
17
|
+
// State where playback is stopping
|
|
18
|
+
| 'stopping'
|
|
19
|
+
// State where playback has stopped
|
|
20
|
+
| 'stopped'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* OutputStreamNode class
|
|
24
|
+
* This class extends AudioWorkletNode to handle audio processing.
|
|
25
|
+
* It manages a buffer using a FrameBufferWriter instance and tracks the current frame position.
|
|
26
|
+
*/
|
|
27
|
+
export class OutputStreamNode extends AudioWorkletNode {
|
|
28
|
+
private readonly _totalWriteFrames: BigUint64Array
|
|
29
|
+
private readonly _totalReadFrames: BigUint64Array
|
|
30
|
+
private readonly _strategy: BufferWriteStrategy
|
|
31
|
+
private _state: StreamState = 'ready'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates an instance of OutputStreamNode.
|
|
35
|
+
* @param baseAudioContext - The audio context to use.
|
|
36
|
+
* @param bufferConfig - The configuration for the buffer.
|
|
37
|
+
* @param strategy - The strategy for writing to the buffer.
|
|
38
|
+
*/
|
|
39
|
+
protected constructor(
|
|
40
|
+
baseAudioContext: BaseAudioContext,
|
|
41
|
+
bufferConfig: FrameBufferConfig,
|
|
42
|
+
strategy: BufferWriteStrategy,
|
|
43
|
+
) {
|
|
44
|
+
const processorOptions: OutputStreamProcessorOptions = {
|
|
45
|
+
...bufferConfig,
|
|
46
|
+
totalFrames: bufferConfig.totalReadFrames,
|
|
47
|
+
}
|
|
48
|
+
super(baseAudioContext, PROCESSOR_NAME, {
|
|
49
|
+
outputChannelCount: [bufferConfig.samplesPerFrame],
|
|
50
|
+
processorOptions,
|
|
51
|
+
})
|
|
52
|
+
this._strategy = strategy
|
|
53
|
+
this._totalWriteFrames = bufferConfig.totalWriteFrames
|
|
54
|
+
this._totalReadFrames = bufferConfig.totalReadFrames
|
|
55
|
+
this.port.onmessage = this.handleMessage.bind(this)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Start playback.
|
|
60
|
+
* The node must be connected before starting playback using connect() method.
|
|
61
|
+
* Output samples must be written to the buffer before starting.
|
|
62
|
+
* Playback can only be started once. Once stopped, it cannot be restarted.
|
|
63
|
+
* @returns A boolean indicating whether the playback started successfully.
|
|
64
|
+
*/
|
|
65
|
+
public start(): boolean {
|
|
66
|
+
if (this.numberOfOutputs === 0) {
|
|
67
|
+
throw new Error('Cannot start playback. Node is not connected.')
|
|
68
|
+
}
|
|
69
|
+
switch (this._state) {
|
|
70
|
+
case 'ready':
|
|
71
|
+
this._state = 'started'
|
|
72
|
+
if (!this._strategy.onStart(this)) {
|
|
73
|
+
this.stop(this.totalWriteFrames)
|
|
74
|
+
}
|
|
75
|
+
return true
|
|
76
|
+
case 'started':
|
|
77
|
+
return false
|
|
78
|
+
default:
|
|
79
|
+
throw new Error(`Cannot start playback. Current state: ${this._state}`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Stop the node processing at a given frame position.
|
|
85
|
+
* Returns a Promise that resolves when the node has completely stopped.
|
|
86
|
+
* The node is disconnected once stopping is complete.
|
|
87
|
+
* @param frames - The frame position at which to stop the processing, in frames.
|
|
88
|
+
* If frames is 0 or if the current playback frame position has already passed the specified value,
|
|
89
|
+
* the node will stop immediately.
|
|
90
|
+
* @returns A promise that resolves when the node has stopped.
|
|
91
|
+
*/
|
|
92
|
+
public stop(frames: bigint = BigInt(0)): Promise<void> {
|
|
93
|
+
switch (this._state) {
|
|
94
|
+
case 'started':
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
this._state = 'stopping'
|
|
97
|
+
const message: MessageToProcessor = { type: 'stop', frames }
|
|
98
|
+
this.port.postMessage(message)
|
|
99
|
+
this.addEventListener(StopEvent.type, () => {
|
|
100
|
+
resolve()
|
|
101
|
+
}, { once: true })
|
|
102
|
+
})
|
|
103
|
+
case 'ready':
|
|
104
|
+
this.handleStopped()
|
|
105
|
+
break
|
|
106
|
+
default:
|
|
107
|
+
throw new Error(`Cannot stop playback. Current state: ${this._state}`)
|
|
108
|
+
}
|
|
109
|
+
return Promise.resolve()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Handles incoming messages from the audio processor.
|
|
114
|
+
* @param event - The message event from the processor.
|
|
115
|
+
*/
|
|
116
|
+
private handleMessage(event: MessageEvent<MessageToAudioNode>): void {
|
|
117
|
+
switch (event.data.type) {
|
|
118
|
+
case 'stop':
|
|
119
|
+
this.handleStopped()
|
|
120
|
+
this.dispatchEvent(new StopEvent(event.data.frames))
|
|
121
|
+
break
|
|
122
|
+
case 'underrun':
|
|
123
|
+
this.dispatchEvent(new UnderrunEvent(event.data.frames))
|
|
124
|
+
break
|
|
125
|
+
default:
|
|
126
|
+
throw new Error(`Unexpected event value: ${event.data}`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Handles the stopping of the node.
|
|
132
|
+
* Disconnects the node and closes the port.
|
|
133
|
+
*/
|
|
134
|
+
private handleStopped() {
|
|
135
|
+
this._strategy.onStopped()
|
|
136
|
+
this.disconnect()
|
|
137
|
+
this.port.close()
|
|
138
|
+
this.port.onmessage = null
|
|
139
|
+
this._state = 'stopped'
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Checks if playback has started.
|
|
144
|
+
* @returns A boolean indicating if playback has started.
|
|
145
|
+
*/
|
|
146
|
+
public get isStart(): boolean {
|
|
147
|
+
return this._state === 'started'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get the current frame position since the start of playback.
|
|
152
|
+
* The position is in frames and increases by 1 for each frame.
|
|
153
|
+
* It represents the total number of frames processed by the AudioWorkletProcessor.
|
|
154
|
+
* - AudioWorkletProcessor has read this total number of frames from the buffer.
|
|
155
|
+
* It closely indicates the playback position in frames.
|
|
156
|
+
* - totalReadFrames will never exceed the number of frames written to the buffer,
|
|
157
|
+
* ensuring it is always less than or equal to totalWriteFrames.
|
|
158
|
+
* - The difference between totalWriteFrames and totalReadFrames represents the delay
|
|
159
|
+
* in samples before playback (excluding processing beyond the AudioNode).
|
|
160
|
+
* @returns The current frame position as a bigint.
|
|
161
|
+
*/
|
|
162
|
+
public get totalReadFrames(): bigint {
|
|
163
|
+
return Atomics.load(this._totalReadFrames, 0)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get the total number of frames written to the buffer.
|
|
168
|
+
* This reflects the total frames written by the BufferWriteStrategy.
|
|
169
|
+
* Note: The method of writing to the buffer is implemented by the BufferWriteStrategy.
|
|
170
|
+
* The OutputStreamNode itself does not modify this value.
|
|
171
|
+
* @returns The total number of frames written as a bigint.
|
|
172
|
+
*/
|
|
173
|
+
public get totalWriteFrames(): bigint {
|
|
174
|
+
return Atomics.load(this._totalWriteFrames, 0)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* OutputStreamNodeFactory class
|
|
180
|
+
* Factory class to create instances of OutputStreamNode.
|
|
181
|
+
*/
|
|
182
|
+
export class OutputStreamNodeFactory extends OutputStreamNode {
|
|
183
|
+
/**
|
|
184
|
+
* Creates an instance of OutputStreamNodeFactory.
|
|
185
|
+
* @param audioContext - The audio context to use.
|
|
186
|
+
* @param info - The configuration for the buffer.
|
|
187
|
+
* @param strategy - The strategy for writing to the buffer.
|
|
188
|
+
* @returns A promise that resolves to an instance of OutputStreamNodeFactory.
|
|
189
|
+
*/
|
|
190
|
+
public static async create(audioContext: BaseAudioContext, info: FrameBufferConfig, strategy: BufferWriteStrategy): Promise<OutputStreamNode> {
|
|
191
|
+
const node = new OutputStreamNodeFactory(audioContext, info, strategy)
|
|
192
|
+
if (!(await strategy.onInit(node))) {
|
|
193
|
+
throw new Error('Failed to onPrepare.')
|
|
194
|
+
}
|
|
195
|
+
return node
|
|
196
|
+
}
|
|
197
|
+
}
|