@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.
Files changed (79) hide show
  1. package/LICENSE +222 -0
  2. package/README.md +243 -0
  3. package/dist/constants.d.ts +12 -0
  4. package/dist/constants.js +15 -0
  5. package/dist/constants.js.map +1 -0
  6. package/dist/events.d.ts +37 -0
  7. package/dist/events.js +35 -0
  8. package/dist/events.js.map +1 -0
  9. package/dist/frame-buffer/buffer-factory.d.ts +77 -0
  10. package/dist/frame-buffer/buffer-factory.js +52 -0
  11. package/dist/frame-buffer/buffer-factory.js.map +1 -0
  12. package/dist/frame-buffer/buffer-filler.d.ts +13 -0
  13. package/dist/frame-buffer/buffer-filler.js +2 -0
  14. package/dist/frame-buffer/buffer-filler.js.map +1 -0
  15. package/dist/frame-buffer/buffer-reader.d.ts +37 -0
  16. package/dist/frame-buffer/buffer-reader.js +51 -0
  17. package/dist/frame-buffer/buffer-reader.js.map +1 -0
  18. package/dist/frame-buffer/buffer-utils.d.ts +34 -0
  19. package/dist/frame-buffer/buffer-utils.js +34 -0
  20. package/dist/frame-buffer/buffer-utils.js.map +1 -0
  21. package/dist/frame-buffer/buffer-writer.d.ts +37 -0
  22. package/dist/frame-buffer/buffer-writer.js +51 -0
  23. package/dist/frame-buffer/buffer-writer.js.map +1 -0
  24. package/dist/frame-buffer/buffer.d.ts +40 -0
  25. package/dist/frame-buffer/buffer.js +65 -0
  26. package/dist/frame-buffer/buffer.js.map +1 -0
  27. package/dist/index.d.ts +6 -0
  28. package/dist/index.js +4 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/output-message.d.ts +42 -0
  31. package/dist/output-message.js +2 -0
  32. package/dist/output-message.js.map +1 -0
  33. package/dist/output-stream-node.d.ts +93 -0
  34. package/dist/output-stream-node.js +166 -0
  35. package/dist/output-stream-node.js.map +1 -0
  36. package/dist/output-stream-processor.d.ts +13 -0
  37. package/dist/output-stream-processor.js +99 -0
  38. package/dist/output-stream-processor.js.map +1 -0
  39. package/dist/stream-node-factory.d.ts +87 -0
  40. package/dist/stream-node-factory.js +113 -0
  41. package/dist/stream-node-factory.js.map +1 -0
  42. package/dist/write-strategy/manual.d.ts +36 -0
  43. package/dist/write-strategy/manual.js +43 -0
  44. package/dist/write-strategy/manual.js.map +1 -0
  45. package/dist/write-strategy/strategy.d.ts +23 -0
  46. package/dist/write-strategy/strategy.js +2 -0
  47. package/dist/write-strategy/strategy.js.map +1 -0
  48. package/dist/write-strategy/timed.d.ts +36 -0
  49. package/dist/write-strategy/timed.js +92 -0
  50. package/dist/write-strategy/timed.js.map +1 -0
  51. package/dist/write-strategy/worker/message.d.ts +54 -0
  52. package/dist/write-strategy/worker/message.js +2 -0
  53. package/dist/write-strategy/worker/message.js.map +1 -0
  54. package/dist/write-strategy/worker/strategy.d.ts +34 -0
  55. package/dist/write-strategy/worker/strategy.js +125 -0
  56. package/dist/write-strategy/worker/strategy.js.map +1 -0
  57. package/dist/write-strategy/worker/worker.d.ts +35 -0
  58. package/dist/write-strategy/worker/worker.js +135 -0
  59. package/dist/write-strategy/worker/worker.js.map +1 -0
  60. package/package.json +54 -0
  61. package/src/constants.ts +18 -0
  62. package/src/events.ts +43 -0
  63. package/src/frame-buffer/buffer-factory.ts +115 -0
  64. package/src/frame-buffer/buffer-filler.ts +14 -0
  65. package/src/frame-buffer/buffer-reader.ts +56 -0
  66. package/src/frame-buffer/buffer-utils.ts +48 -0
  67. package/src/frame-buffer/buffer-writer.ts +56 -0
  68. package/src/frame-buffer/buffer.ts +68 -0
  69. package/src/index.ts +9 -0
  70. package/src/output-message.ts +37 -0
  71. package/src/output-stream-node.ts +197 -0
  72. package/src/output-stream-processor.ts +124 -0
  73. package/src/stream-node-factory.ts +161 -0
  74. package/src/write-strategy/manual.ts +50 -0
  75. package/src/write-strategy/strategy.ts +26 -0
  76. package/src/write-strategy/timed.ts +103 -0
  77. package/src/write-strategy/worker/message.ts +48 -0
  78. package/src/write-strategy/worker/strategy.ts +154 -0
  79. 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
+ }