@ain1084/audio-worklet-stream 0.1.8 → 1.0.0

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 (57) hide show
  1. package/README.md +12 -27
  2. package/dist/events.d.ts +6 -6
  3. package/dist/events.js +8 -8
  4. package/dist/events.js.map +1 -1
  5. package/dist/frame-buffer/buffer-config.d.ts +67 -0
  6. package/dist/frame-buffer/buffer-config.js +64 -0
  7. package/dist/frame-buffer/buffer-config.js.map +1 -0
  8. package/dist/frame-buffer/buffer-factory.d.ts +1 -2
  9. package/dist/frame-buffer/buffer-factory.js +6 -3
  10. package/dist/frame-buffer/buffer-factory.js.map +1 -1
  11. package/dist/frame-buffer/buffer-reader.d.ts +29 -10
  12. package/dist/frame-buffer/buffer-reader.js +37 -16
  13. package/dist/frame-buffer/buffer-reader.js.map +1 -1
  14. package/dist/frame-buffer/buffer-utils.js +2 -2
  15. package/dist/frame-buffer/buffer-utils.js.map +1 -1
  16. package/dist/frame-buffer/buffer-writer.d.ts +31 -12
  17. package/dist/frame-buffer/buffer-writer.js +39 -18
  18. package/dist/frame-buffer/buffer-writer.js.map +1 -1
  19. package/dist/frame-buffer/buffer.d.ts +33 -22
  20. package/dist/frame-buffer/buffer.js +50 -40
  21. package/dist/frame-buffer/buffer.js.map +1 -1
  22. package/dist/output-message.d.ts +9 -9
  23. package/dist/output-stream-node.d.ts +13 -19
  24. package/dist/output-stream-node.js +20 -26
  25. package/dist/output-stream-node.js.map +1 -1
  26. package/dist/output-stream-processor.js +25 -18
  27. package/dist/output-stream-processor.js.map +1 -1
  28. package/dist/stream-node-factory.d.ts +10 -0
  29. package/dist/stream-node-factory.js +27 -12
  30. package/dist/stream-node-factory.js.map +1 -1
  31. package/dist/write-strategy/manual.d.ts +1 -1
  32. package/dist/write-strategy/manual.js +1 -1
  33. package/dist/write-strategy/manual.js.map +1 -1
  34. package/dist/write-strategy/timed.d.ts +1 -1
  35. package/dist/write-strategy/timed.js +1 -1
  36. package/dist/write-strategy/timed.js.map +1 -1
  37. package/dist/write-strategy/worker/message.d.ts +1 -1
  38. package/dist/write-strategy/worker/strategy.d.ts +1 -1
  39. package/dist/write-strategy/worker/worker.js +1 -1
  40. package/dist/write-strategy/worker/worker.js.map +1 -1
  41. package/package.json +9 -9
  42. package/src/events.ts +4 -4
  43. package/src/frame-buffer/buffer-config.ts +129 -0
  44. package/src/frame-buffer/buffer-reader.ts +37 -16
  45. package/src/frame-buffer/buffer-writer.ts +39 -18
  46. package/src/frame-buffer/buffer.ts +51 -40
  47. package/src/output-message.ts +9 -9
  48. package/src/output-stream-node.ts +23 -29
  49. package/src/output-stream-processor.ts +25 -18
  50. package/src/stream-node-factory.ts +33 -15
  51. package/src/write-strategy/manual.ts +1 -1
  52. package/src/write-strategy/timed.ts +1 -1
  53. package/src/write-strategy/worker/message.ts +1 -1
  54. package/src/write-strategy/worker/strategy.ts +1 -1
  55. package/src/write-strategy/worker/worker.ts +1 -1
  56. package/src/frame-buffer/buffer-factory.ts +0 -115
  57. package/src/frame-buffer/buffer-utils.ts +0 -48
@@ -5,7 +5,7 @@
5
5
  export class FrameBuffer {
6
6
  private readonly _buffer: Float32Array
7
7
  private readonly _samplesPerFrame: number
8
- private readonly _length: number
8
+ private readonly _frameCount: number
9
9
 
10
10
  /**
11
11
  * Creates an instance of FrameBuffer.
@@ -15,54 +15,65 @@ export class FrameBuffer {
15
15
  public constructor(buffer: Float32Array, samplesPerFrame: number) {
16
16
  this._buffer = buffer
17
17
  this._samplesPerFrame = samplesPerFrame
18
- this._length = Math.floor(buffer.length / samplesPerFrame)
18
+ this._frameCount = Math.floor(buffer.length / samplesPerFrame)
19
19
  }
20
20
 
21
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.
22
+ * Gets the count of the buffer in frames.
23
+ * @returns The count of the buffer in frames.
29
24
  */
30
- public setFrames(index: number, samples: Float32Array, sampleStart: number = 0, sampleCount?: number): number {
31
- index *= this._samplesPerFrame
32
- const sampleEnd = (sampleCount !== undefined) ? Math.min(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
25
+ public get frameCount(): number {
26
+ return this._frameCount
41
27
  }
42
28
 
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
- })
29
+ private getFrames(index: number, count: number): Float32Array {
30
+ return this._buffer.subarray(index * this._samplesPerFrame, (index + count) * this._samplesPerFrame)
59
31
  }
60
32
 
61
33
  /**
62
- * Gets the length of the buffer in frames.
63
- * @returns The length of the buffer in frames.
34
+ * Processes sections of a Float32Array buffer using a callback function.
35
+ * This function handles one or more segments within the ring buffer and invokes
36
+ * the provided callback for each segment. It is intended for internal use only.
37
+ *
38
+ * @param startIndex - The starting index in the buffer from where processing should begin.
39
+ * @param availableFrames - The total number of frames available to process in the buffer.
40
+ * @param processFrameSegment - The callback function invoked for each segment
41
+ * of the ring buffer during enumeration. It receives:
42
+ * 1. `buffer`: A Float32Array representing the segment to process. The buffer is an array
43
+ * of samples but is always provided in frame-sized segments.
44
+ * 2. `offset`: The cumulative number of frames processed so far, used as the starting index
45
+ * for the current segment relative to the entire data.
46
+ * The callback must return the number of frames it successfully processed.
47
+ * If the callback processes fewer frames than available in the current segment,
48
+ * processing will stop early.
49
+ *
50
+ * @returns An object containing:
51
+ * - totalProcessedFrames: The number of frames successfully processed.
52
+ * - nextIndex: The index in the buffer for the next processing cycle.
53
+ *
54
+ * @throws RangeError - If the processFrameSegment callback returns a processed length greater than the available section length.
55
+ *
56
+ * @remarks The buffer is always provided in frame-sized segments, meaning that the buffer contains complete frames.
57
+ * You must process the buffer in frame-sized chunks based on the structure of the frames.
64
58
  */
65
- public get length(): number {
66
- return this._length
59
+ public enumFrameSegments(startIndex: number, availableFrames: number,
60
+ processFrameSegment: (buffer: Float32Array, offset: number) => number): { totalProcessedFrames: number, nextIndex: number } {
61
+ let totalProcessedFrames = 0
62
+ while (totalProcessedFrames < availableFrames) {
63
+ // Determine the length of the current section to process
64
+ const sectionFrames = Math.min(this.frameCount - startIndex, availableFrames - totalProcessedFrames)
65
+ // Process the current section using the frameCallback function
66
+ const processedFrames = processFrameSegment(this.getFrames(startIndex, sectionFrames), totalProcessedFrames)
67
+ // Ensure the processed length does not exceed the section length
68
+ if (processedFrames > sectionFrames) {
69
+ throw new RangeError(`Processed frames (${processedFrames}) exceeds section frames (${sectionFrames})`)
70
+ }
71
+ totalProcessedFrames += processedFrames
72
+ startIndex = (startIndex + processedFrames) % this.frameCount
73
+ if (processedFrames < sectionFrames) {
74
+ break
75
+ }
76
+ }
77
+ return { totalProcessedFrames, nextIndex: startIndex }
67
78
  }
68
79
  }
@@ -4,21 +4,21 @@
4
4
  * @typeParam type - The type of message. Can be 'stop' or 'underrun'.
5
5
  *
6
6
  * For 'stop' messages:
7
- * @property frames - The total frames processed when stopped.
7
+ * @property totalProcessedFrames - The total frames processed when stopped.
8
8
  *
9
9
  * For 'underrun' messages:
10
- * @property frames - The number of frames that have been underrun.
10
+ * @property underrunFrameCount - The number of frames that have been underrun.
11
11
  *
12
12
  * Example:
13
13
  * ```typescript
14
- * const message: MessageToAudioNode = { type: 'stop', frames: 1000n };
14
+ * const message: MessageToAudioNode = { type: 'stop', totalProcessedFrames: 1000n };
15
15
  * // or
16
- * const message: MessageToAudioNode = { type: 'underrun', frames: 256 };
16
+ * const message: MessageToAudioNode = { type: 'underrun', underrunFrameCount: 256 };
17
17
  * ```
18
18
  */
19
19
  export type MessageToAudioNode =
20
- | { type: 'stop', frames: bigint }
21
- | { type: 'underrun', frames: number }
20
+ | { type: 'stop', totalProcessedFrames: bigint }
21
+ | { type: 'underrun', underrunFrameCount: number }
22
22
 
23
23
  /**
24
24
  * Messages sent from the main thread to the processor.
@@ -26,12 +26,12 @@ export type MessageToAudioNode =
26
26
  * @typeParam type - The type of message. Can be 'stop'.
27
27
  *
28
28
  * For 'stop' messages:
29
- * @property frames - The position in frames to stop at.
29
+ * @property framePosition - The position in frames to stop at.
30
30
  *
31
31
  * Example:
32
32
  * ```typescript
33
- * const message: MessageToProcessor = { type: 'stop', frames: 1000n };
33
+ * const message: MessageToProcessor = { type: 'stop', framePosition: 1000n };
34
34
  * ```
35
35
  */
36
36
  export type MessageToProcessor =
37
- | { type: 'stop', frames: bigint }
37
+ | { type: 'stop', framePosition: bigint }
@@ -3,7 +3,7 @@ import type { OutputStreamProcessorOptions } from './output-stream-processor'
3
3
  import { PROCESSOR_NAME } from './constants'
4
4
  import { StopEvent, UnderrunEvent } from './events'
5
5
  import { BufferWriteStrategy } from './write-strategy/strategy'
6
- import { FrameBufferConfig } from './frame-buffer/buffer-factory'
6
+ import { FrameBufferConfig } from './frame-buffer/buffer-config'
7
7
 
8
8
  /**
9
9
  * Stream state
@@ -36,7 +36,7 @@ export class OutputStreamNode extends AudioWorkletNode {
36
36
  * @param bufferConfig - The configuration for the buffer.
37
37
  * @param strategy - The strategy for writing to the buffer.
38
38
  */
39
- protected constructor(
39
+ private constructor(
40
40
  baseAudioContext: BaseAudioContext,
41
41
  bufferConfig: FrameBufferConfig,
42
42
  strategy: BufferWriteStrategy,
@@ -55,6 +55,21 @@ export class OutputStreamNode extends AudioWorkletNode {
55
55
  this.port.onmessage = this.handleMessage.bind(this)
56
56
  }
57
57
 
58
+ /**
59
+ * Creates an instance of OutputStreamNode.
60
+ * @param audioContext - The audio context to use.
61
+ * @param info - The configuration for the buffer.
62
+ * @param strategy - The strategy for writing to the buffer.
63
+ * @returns A promise that resolves to an instance of OutputStreamNode.
64
+ */
65
+ public static async create(audioContext: BaseAudioContext, info: FrameBufferConfig, strategy: BufferWriteStrategy): Promise<OutputStreamNode> {
66
+ const node = new OutputStreamNode(audioContext, info, strategy)
67
+ if (!(await strategy.onInit(node))) {
68
+ throw new Error('Failed to onInit.')
69
+ }
70
+ return node
71
+ }
72
+
58
73
  /**
59
74
  * Start playback.
60
75
  * The node must be connected before starting playback using connect() method.
@@ -84,17 +99,17 @@ export class OutputStreamNode extends AudioWorkletNode {
84
99
  * Stop the node processing at a given frame position.
85
100
  * Returns a Promise that resolves when the node has completely stopped.
86
101
  * 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,
102
+ * @param framePosition - The frame position at which to stop the processing, in frames.
103
+ * If framePosition is 0 or if the current playback frame position has already passed the specified value,
89
104
  * the node will stop immediately.
90
105
  * @returns A promise that resolves when the node has stopped.
91
106
  */
92
- public stop(frames: bigint = BigInt(0)): Promise<void> {
107
+ public stop(framePosition: bigint = BigInt(0)): Promise<void> {
93
108
  switch (this._state) {
94
109
  case 'started':
95
110
  return new Promise((resolve) => {
96
111
  this._state = 'stopping'
97
- const message: MessageToProcessor = { type: 'stop', frames }
112
+ const message: MessageToProcessor = { type: 'stop', framePosition: framePosition }
98
113
  this.port.postMessage(message)
99
114
  this.addEventListener(StopEvent.type, () => {
100
115
  resolve()
@@ -121,10 +136,10 @@ export class OutputStreamNode extends AudioWorkletNode {
121
136
  switch (event.data.type) {
122
137
  case 'stop':
123
138
  this.handleStopped()
124
- this.dispatchEvent(new StopEvent(event.data.frames))
139
+ this.dispatchEvent(new StopEvent(event.data.totalProcessedFrames))
125
140
  break
126
141
  case 'underrun':
127
- this.dispatchEvent(new UnderrunEvent(event.data.frames))
142
+ this.dispatchEvent(new UnderrunEvent(event.data.underrunFrameCount))
128
143
  break
129
144
  default:
130
145
  throw new Error(`Unexpected event value: ${event.data}`)
@@ -178,24 +193,3 @@ export class OutputStreamNode extends AudioWorkletNode {
178
193
  return Atomics.load(this._totalWriteFrames, 0)
179
194
  }
180
195
  }
181
-
182
- /**
183
- * OutputStreamNodeFactory class
184
- * Factory class to create instances of OutputStreamNode.
185
- */
186
- export class OutputStreamNodeFactory extends OutputStreamNode {
187
- /**
188
- * Creates an instance of OutputStreamNodeFactory.
189
- * @param audioContext - The audio context to use.
190
- * @param info - The configuration for the buffer.
191
- * @param strategy - The strategy for writing to the buffer.
192
- * @returns A promise that resolves to an instance of OutputStreamNodeFactory.
193
- */
194
- public static async create(audioContext: BaseAudioContext, info: FrameBufferConfig, strategy: BufferWriteStrategy): Promise<OutputStreamNode> {
195
- const node = new OutputStreamNodeFactory(audioContext, info, strategy)
196
- if (!(await strategy.onInit(node))) {
197
- throw new Error('Failed to onPrepare.')
198
- }
199
- return node
200
- }
201
- }
@@ -32,8 +32,8 @@ const createFrameBufferReader = (options: OutputStreamProcessorOptions) => {
32
32
  class OutputStreamProcessor extends AudioWorkletProcessor {
33
33
  private readonly _frameReader: FrameBufferReader
34
34
  private _shouldStop = false
35
- private _stopFrames: bigint | undefined
36
- private _underrunFrames = 0
35
+ private _stopFramePosition: bigint | undefined
36
+ private _underrunFrameCount = 0
37
37
 
38
38
  /**
39
39
  * Creates an instance of OutputStreamProcessor.
@@ -53,12 +53,12 @@ class OutputStreamProcessor extends AudioWorkletProcessor {
53
53
  if (event.data.type !== 'stop') {
54
54
  throw new Error(`Unexpected message type: ${event.data.type}`)
55
55
  }
56
- const frames = event.data.frames
57
- if (frames <= 0) {
56
+ const framePosition = event.data.framePosition
57
+ if (framePosition <= 0) {
58
58
  this._shouldStop = true
59
59
  }
60
60
  else {
61
- this._stopFrames = frames
61
+ this._stopFramePosition = framePosition
62
62
  }
63
63
  }
64
64
 
@@ -67,18 +67,18 @@ class OutputStreamProcessor extends AudioWorkletProcessor {
67
67
  * If underrunFrames is provided and not zero, it adds to the current underrun frame count.
68
68
  * If underrunFrames is 0, it indicates that the underrun state has been resolved,
69
69
  * and the total underrun frame count is sent to the main thread before being reset.
70
- * @param underrunFrames - The number of underrun frames to add (default is 0).
70
+ * @param underrunFrameCount - The number of underrun frames to add (default is 0).
71
71
  */
72
- private updateUnderrun(underrunFrames: number = 0): void {
73
- if (underrunFrames !== 0) {
74
- this._underrunFrames += underrunFrames
72
+ private updateUnderrun(underrunFrameCount: number = 0): void {
73
+ if (underrunFrameCount !== 0) {
74
+ this._underrunFrameCount += underrunFrameCount
75
75
  return
76
76
  }
77
- if (this._underrunFrames === 0) {
77
+ if (this._underrunFrameCount === 0) {
78
78
  return
79
79
  }
80
- this.port.postMessage({ type: 'underrun', frames: this._underrunFrames } as MessageToAudioNode)
81
- this._underrunFrames = 0
80
+ this.port.postMessage({ type: 'underrun', underrunFrameCount: this._underrunFrameCount } as MessageToAudioNode)
81
+ this._underrunFrameCount = 0
82
82
  }
83
83
 
84
84
  /**
@@ -86,7 +86,7 @@ class OutputStreamProcessor extends AudioWorkletProcessor {
86
86
  * @param totalFrames - The total number of frames processed so far.
87
87
  */
88
88
  private checkStopCondition(totalFrames: bigint) {
89
- if (this._stopFrames !== undefined && totalFrames >= this._stopFrames) {
89
+ if (this._stopFramePosition !== undefined && totalFrames >= this._stopFramePosition) {
90
90
  this._shouldStop = true
91
91
  }
92
92
  }
@@ -99,16 +99,23 @@ class OutputStreamProcessor extends AudioWorkletProcessor {
99
99
  */
100
100
  process(_inputs: Float32Array[][], outputs: Float32Array[][]): boolean {
101
101
  const output = outputs[0]
102
- const readFrames = this._frameReader.read((frame, offset) => {
103
- const frames = Math.min(frame.frames, output[0].length - offset)
104
- frame.buffer.convertToOutput(frame.index, frames, output, offset)
105
- return frames
102
+ const samplesPerFrame = output.length
103
+ const readFrames = this._frameReader.read((buffer, offset) => {
104
+ const bufferFrameCount = buffer.length / samplesPerFrame
105
+ const frameCount = Math.min(bufferFrameCount, output[0].length - offset)
106
+ // Deinterleaves interleaved audio frame data and writes it to the output.
107
+ output.forEach((outputChannel, channelIndex) => {
108
+ for (let i = channelIndex, j = 0; j < frameCount; i += samplesPerFrame, ++j) {
109
+ outputChannel[offset + j] = buffer[i]
110
+ }
111
+ })
112
+ return frameCount
106
113
  })
107
114
  const totalFrames = this._frameReader.totalFrames
108
115
  this.checkStopCondition(totalFrames)
109
116
  if (this._shouldStop) {
110
117
  this.updateUnderrun()
111
- this.port.postMessage({ type: 'stop', frames: totalFrames } as MessageToAudioNode)
118
+ this.port.postMessage({ type: 'stop', totalProcessedFrames: totalFrames } as MessageToAudioNode)
112
119
  this.port.close()
113
120
  return false
114
121
  }
@@ -1,7 +1,7 @@
1
1
  import processor from './output-stream-processor?worker&url'
2
2
  import { ManualBufferWriteStrategy } from './write-strategy/manual'
3
- import { FrameBufferFactory } from './frame-buffer/buffer-factory'
4
- import type { OutputStreamNode, OutputStreamNodeFactory } from './output-stream-node'
3
+ import { createFillerFrameBufferConfig, createFrameBufferConfig } from './frame-buffer/buffer-config'
4
+ import type { OutputStreamNode } from './output-stream-node'
5
5
  import { TimedBufferWriteStrategy } from './write-strategy/timed'
6
6
  import type { FrameBufferFiller } from './frame-buffer/buffer-filler'
7
7
  import { FrameBufferWriter } from './frame-buffer/buffer-writer'
@@ -47,14 +47,14 @@ export type WorkerBufferNodeParams<T> = TimedBufferNodeParams & Readonly<{
47
47
  */
48
48
  export class StreamNodeFactory {
49
49
  private _audioContext: AudioContext
50
- private readonly _outputStreamNodeFactory: typeof OutputStreamNodeFactory
50
+ private readonly _outputStreamNodeFactory: typeof OutputStreamNode
51
51
 
52
52
  /**
53
53
  * Constructor for StreamNodeFactory.
54
54
  * @param context - The AudioContext to use.
55
55
  * @param outputStreamNodeFactory - The factory for creating OutputStreamNode instances.
56
56
  */
57
- private constructor(context: AudioContext, outputStreamNodeFactory: typeof OutputStreamNodeFactory) {
57
+ private constructor(context: AudioContext, outputStreamNodeFactory: typeof OutputStreamNode) {
58
58
  this._audioContext = context
59
59
  this._outputStreamNodeFactory = outputStreamNodeFactory
60
60
  }
@@ -72,7 +72,7 @@ export class StreamNodeFactory {
72
72
  context.audioWorklet.addModule(processor),
73
73
  import('./output-stream-node'),
74
74
  ])
75
- return new StreamNodeFactory(context, loadResults[1].OutputStreamNodeFactory)
75
+ return new StreamNodeFactory(context, loadResults[1].OutputStreamNode)
76
76
  }
77
77
  catch (error) {
78
78
  throw new Error('Failed to load modules: ' + error)
@@ -91,9 +91,9 @@ export class StreamNodeFactory {
91
91
  if (params.frameBufferSize <= 0 || !Number.isInteger(params.frameBufferSize)) {
92
92
  throw new Error('Invalid frameBufferSize: must be a positive integer.')
93
93
  }
94
- const bufferConfig = FrameBufferFactory.createFrameBufferConfig({ ...params })
95
- const strategy = new ManualBufferWriteStrategy(bufferConfig)
96
- return [await this._outputStreamNodeFactory.create(this.audioContext, bufferConfig, strategy), strategy.writer]
94
+ const config = createFrameBufferConfig({ ...params })
95
+ const strategy = new ManualBufferWriteStrategy(config)
96
+ return [await this._outputStreamNodeFactory.create(this.audioContext, config, strategy), strategy.writer]
97
97
  }
98
98
 
99
99
  /**
@@ -105,13 +105,14 @@ export class StreamNodeFactory {
105
105
  public async createTimedBufferNode(
106
106
  frameFiller: FrameBufferFiller, params: TimedBufferNodeParams,
107
107
  ): Promise<OutputStreamNode> {
108
- StreamNodeFactory.validateTimedBufferNodeParams(params)
109
108
  if (typeof frameFiller !== 'object' || frameFiller === null) {
110
109
  throw new Error('Invalid frameFiller: must be an object.')
111
110
  }
112
- const config = FrameBufferFactory.createFillerFrameBufferConfig(this._audioContext.sampleRate, { ...params })
113
- const strategy = new TimedBufferWriteStrategy(config, frameFiller)
114
- return this._outputStreamNodeFactory.create(this.audioContext, config, strategy)
111
+ StreamNodeFactory.validateTimedBufferNodeParams(params)
112
+ const paramsWithSampleRate = StreamNodeFactory.applySampleRateToParams(params, this._audioContext.sampleRate)
113
+ const config = createFillerFrameBufferConfig(paramsWithSampleRate)
114
+ return this._outputStreamNodeFactory.create(this.audioContext, config,
115
+ new TimedBufferWriteStrategy(config, frameFiller))
115
116
  }
116
117
 
117
118
  /**
@@ -124,9 +125,10 @@ export class StreamNodeFactory {
124
125
  worker: new () => Worker, params: WorkerBufferNodeParams<FillerParams>,
125
126
  ): Promise<OutputStreamNode> {
126
127
  StreamNodeFactory.validateTimedBufferNodeParams(params)
127
- const config = FrameBufferFactory.createFillerFrameBufferConfig(this._audioContext.sampleRate, { ...params })
128
- const strategy = new WorkerBufferWriteStrategy<FillerParams>(config, worker, params.fillerParams)
129
- return this._outputStreamNodeFactory.create(this._audioContext, config, strategy)
128
+ const paramsWithSampleRate = StreamNodeFactory.applySampleRateToParams(params, this._audioContext.sampleRate)
129
+ const config = createFillerFrameBufferConfig(paramsWithSampleRate)
130
+ return this._outputStreamNodeFactory.create(this._audioContext, config,
131
+ new WorkerBufferWriteStrategy<FillerParams>(config, worker, params.fillerParams))
130
132
  }
131
133
 
132
134
  /**
@@ -151,6 +153,22 @@ export class StreamNodeFactory {
151
153
  }
152
154
  }
153
155
 
156
+ /**
157
+ * Applies a default sample rate to the given parameters if the sampleRate is undefined.
158
+ * This function creates a new TimedBufferNodeParams object with the specified sampleRate,
159
+ * while keeping other properties unchanged.
160
+ *
161
+ * @param params - The original parameters for the TimedBufferNode.
162
+ * @param defaultSampleRate - The default sample rate to use if params.sampleRate is undefined.
163
+ * @returns A new TimedBufferNodeParams object with the sample rate applied.
164
+ */
165
+ private static applySampleRateToParams(params: TimedBufferNodeParams, defaultSampleRate: number): TimedBufferNodeParams {
166
+ return {
167
+ ...params,
168
+ sampleRate: params.sampleRate ?? defaultSampleRate,
169
+ }
170
+ }
171
+
154
172
  /**
155
173
  * Get the AudioContext associated with this factory.
156
174
  * @returns The AudioContext instance.
@@ -1,5 +1,5 @@
1
1
  import { BufferWriteStrategy } from './strategy'
2
- import { createFrameBufferWriter, FrameBufferConfig } from '../frame-buffer/buffer-factory'
2
+ import { createFrameBufferWriter, FrameBufferConfig } from '../frame-buffer/buffer-config'
3
3
  import { FrameBufferWriter } from '../frame-buffer/buffer-writer'
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { BufferWriteStrategy } from './strategy'
2
- import { createFrameBufferWriter, FillerFrameBufferConfig } from '../frame-buffer/buffer-factory'
2
+ import { createFrameBufferWriter, FillerFrameBufferConfig } from '../frame-buffer/buffer-config'
3
3
  import { FrameBufferWriter } from '../frame-buffer/buffer-writer'
4
4
  import { FrameBufferFiller } from '../frame-buffer/buffer-filler'
5
5
  import { OutputStreamNode } from '../output-stream-node'
@@ -1,4 +1,4 @@
1
- import type { FillerFrameBufferConfig } from '../../frame-buffer/buffer-factory'
1
+ import type { FillerFrameBufferConfig } from '../../frame-buffer/buffer-config'
2
2
 
3
3
  /**
4
4
  * Message sent from the main thread to the worker.
@@ -1,5 +1,5 @@
1
1
  import type { BufferWriteStrategy } from '../strategy'
2
- import type { FillerFrameBufferConfig } from '../../frame-buffer/buffer-factory'
2
+ import type { FillerFrameBufferConfig } from '../../frame-buffer/buffer-config'
3
3
  import type { MessageToStrategy, MessageToWorker } from './message'
4
4
  import type { OutputStreamNode } from '../../output-stream-node'
5
5
 
@@ -1,4 +1,4 @@
1
- import { createFrameBufferWriter, FillerFrameBufferConfig } from '../../frame-buffer/buffer-factory'
1
+ import { createFrameBufferWriter, FillerFrameBufferConfig } from '../../frame-buffer/buffer-config'
2
2
  import type { FrameBufferFiller } from '../../frame-buffer/buffer-filler'
3
3
  import type { FrameBufferWriter } from '../../frame-buffer/buffer-writer'
4
4
  import { MessageToStrategy, MessageToWorker } from './message'
@@ -1,115 +0,0 @@
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
- }
@@ -1,48 +0,0 @@
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
- }