whisper.rn 0.5.0-rc.1 → 0.5.0-rc.3

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 (106) hide show
  1. package/README.md +119 -50
  2. package/android/src/main/java/com/rnwhisper/RNWhisper.java +26 -0
  3. package/android/src/main/java/com/rnwhisper/WhisperContext.java +25 -0
  4. package/android/src/main/jni.cpp +81 -0
  5. package/android/src/newarch/java/com/rnwhisper/RNWhisperModule.java +5 -0
  6. package/android/src/oldarch/java/com/rnwhisper/RNWhisperModule.java +5 -0
  7. package/ios/RNWhisper.mm +11 -0
  8. package/ios/RNWhisperContext.h +1 -0
  9. package/ios/RNWhisperContext.mm +46 -0
  10. package/lib/commonjs/AudioSessionIos.js +2 -1
  11. package/lib/commonjs/AudioSessionIos.js.map +1 -1
  12. package/lib/commonjs/NativeRNWhisper.js.map +1 -1
  13. package/lib/commonjs/index.js +26 -0
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/jest-mock.js +126 -0
  16. package/lib/commonjs/jest-mock.js.map +1 -0
  17. package/lib/commonjs/realtime-transcription/RealtimeTranscriber.js +831 -0
  18. package/lib/commonjs/realtime-transcription/RealtimeTranscriber.js.map +1 -0
  19. package/lib/commonjs/realtime-transcription/SliceManager.js +233 -0
  20. package/lib/commonjs/realtime-transcription/SliceManager.js.map +1 -0
  21. package/lib/commonjs/realtime-transcription/adapters/AudioPcmStreamAdapter.js +133 -0
  22. package/lib/commonjs/realtime-transcription/adapters/AudioPcmStreamAdapter.js.map +1 -0
  23. package/lib/commonjs/realtime-transcription/adapters/JestAudioStreamAdapter.js +201 -0
  24. package/lib/commonjs/realtime-transcription/adapters/JestAudioStreamAdapter.js.map +1 -0
  25. package/lib/commonjs/realtime-transcription/adapters/SimulateFileAudioStreamAdapter.js +309 -0
  26. package/lib/commonjs/realtime-transcription/adapters/SimulateFileAudioStreamAdapter.js.map +1 -0
  27. package/lib/commonjs/realtime-transcription/index.js +27 -0
  28. package/lib/commonjs/realtime-transcription/index.js.map +1 -0
  29. package/lib/commonjs/realtime-transcription/types.js +114 -0
  30. package/lib/commonjs/realtime-transcription/types.js.map +1 -0
  31. package/lib/commonjs/utils/WavFileReader.js +158 -0
  32. package/lib/commonjs/utils/WavFileReader.js.map +1 -0
  33. package/lib/commonjs/utils/WavFileWriter.js +181 -0
  34. package/lib/commonjs/utils/WavFileWriter.js.map +1 -0
  35. package/lib/commonjs/utils/common.js +25 -0
  36. package/lib/commonjs/utils/common.js.map +1 -0
  37. package/lib/module/AudioSessionIos.js +2 -1
  38. package/lib/module/AudioSessionIos.js.map +1 -1
  39. package/lib/module/NativeRNWhisper.js.map +1 -1
  40. package/lib/module/index.js +24 -0
  41. package/lib/module/index.js.map +1 -1
  42. package/lib/module/jest-mock.js +124 -0
  43. package/lib/module/jest-mock.js.map +1 -0
  44. package/lib/module/realtime-transcription/RealtimeTranscriber.js +825 -0
  45. package/lib/module/realtime-transcription/RealtimeTranscriber.js.map +1 -0
  46. package/lib/module/realtime-transcription/SliceManager.js +226 -0
  47. package/lib/module/realtime-transcription/SliceManager.js.map +1 -0
  48. package/lib/module/realtime-transcription/adapters/AudioPcmStreamAdapter.js +124 -0
  49. package/lib/module/realtime-transcription/adapters/AudioPcmStreamAdapter.js.map +1 -0
  50. package/lib/module/realtime-transcription/adapters/JestAudioStreamAdapter.js +194 -0
  51. package/lib/module/realtime-transcription/adapters/JestAudioStreamAdapter.js.map +1 -0
  52. package/lib/module/realtime-transcription/adapters/SimulateFileAudioStreamAdapter.js +302 -0
  53. package/lib/module/realtime-transcription/adapters/SimulateFileAudioStreamAdapter.js.map +1 -0
  54. package/lib/module/realtime-transcription/index.js +8 -0
  55. package/lib/module/realtime-transcription/index.js.map +1 -0
  56. package/lib/module/realtime-transcription/types.js +107 -0
  57. package/lib/module/realtime-transcription/types.js.map +1 -0
  58. package/lib/module/utils/WavFileReader.js +151 -0
  59. package/lib/module/utils/WavFileReader.js.map +1 -0
  60. package/lib/module/utils/WavFileWriter.js +174 -0
  61. package/lib/module/utils/WavFileWriter.js.map +1 -0
  62. package/lib/module/utils/common.js +18 -0
  63. package/lib/module/utils/common.js.map +1 -0
  64. package/lib/typescript/AudioSessionIos.d.ts +1 -1
  65. package/lib/typescript/AudioSessionIos.d.ts.map +1 -1
  66. package/lib/typescript/NativeRNWhisper.d.ts +1 -0
  67. package/lib/typescript/NativeRNWhisper.d.ts.map +1 -1
  68. package/lib/typescript/index.d.ts +4 -0
  69. package/lib/typescript/index.d.ts.map +1 -1
  70. package/lib/typescript/jest-mock.d.ts +2 -0
  71. package/lib/typescript/jest-mock.d.ts.map +1 -0
  72. package/lib/typescript/realtime-transcription/RealtimeTranscriber.d.ts +165 -0
  73. package/lib/typescript/realtime-transcription/RealtimeTranscriber.d.ts.map +1 -0
  74. package/lib/typescript/realtime-transcription/SliceManager.d.ts +72 -0
  75. package/lib/typescript/realtime-transcription/SliceManager.d.ts.map +1 -0
  76. package/lib/typescript/realtime-transcription/adapters/AudioPcmStreamAdapter.d.ts +22 -0
  77. package/lib/typescript/realtime-transcription/adapters/AudioPcmStreamAdapter.d.ts.map +1 -0
  78. package/lib/typescript/realtime-transcription/adapters/JestAudioStreamAdapter.d.ts +44 -0
  79. package/lib/typescript/realtime-transcription/adapters/JestAudioStreamAdapter.d.ts.map +1 -0
  80. package/lib/typescript/realtime-transcription/adapters/SimulateFileAudioStreamAdapter.d.ts +75 -0
  81. package/lib/typescript/realtime-transcription/adapters/SimulateFileAudioStreamAdapter.d.ts.map +1 -0
  82. package/lib/typescript/realtime-transcription/index.d.ts +6 -0
  83. package/lib/typescript/realtime-transcription/index.d.ts.map +1 -0
  84. package/lib/typescript/realtime-transcription/types.d.ts +216 -0
  85. package/lib/typescript/realtime-transcription/types.d.ts.map +1 -0
  86. package/lib/typescript/utils/WavFileReader.d.ts +61 -0
  87. package/lib/typescript/utils/WavFileReader.d.ts.map +1 -0
  88. package/lib/typescript/utils/WavFileWriter.d.ts +57 -0
  89. package/lib/typescript/utils/WavFileWriter.d.ts.map +1 -0
  90. package/lib/typescript/utils/common.d.ts +9 -0
  91. package/lib/typescript/utils/common.d.ts.map +1 -0
  92. package/package.json +18 -6
  93. package/src/AudioSessionIos.ts +3 -2
  94. package/src/NativeRNWhisper.ts +2 -0
  95. package/src/index.ts +34 -0
  96. package/{jest/mock.js → src/jest-mock.ts} +2 -2
  97. package/src/realtime-transcription/RealtimeTranscriber.ts +983 -0
  98. package/src/realtime-transcription/SliceManager.ts +252 -0
  99. package/src/realtime-transcription/adapters/AudioPcmStreamAdapter.ts +143 -0
  100. package/src/realtime-transcription/adapters/JestAudioStreamAdapter.ts +251 -0
  101. package/src/realtime-transcription/adapters/SimulateFileAudioStreamAdapter.ts +378 -0
  102. package/src/realtime-transcription/index.ts +34 -0
  103. package/src/realtime-transcription/types.ts +277 -0
  104. package/src/utils/WavFileReader.ts +202 -0
  105. package/src/utils/WavFileWriter.ts +206 -0
  106. package/src/utils/common.ts +17 -0
@@ -0,0 +1,252 @@
1
+ import type { AudioSlice, MemoryUsage } from './types'
2
+
3
+ export class SliceManager {
4
+ private slices: AudioSlice[] = []
5
+
6
+ private currentSliceIndex = 0
7
+
8
+ private transcribeSliceIndex = 0
9
+
10
+ private maxSlicesInMemory: number
11
+
12
+ private sliceDurationSec: number
13
+
14
+ private sampleRate: number
15
+
16
+ constructor(
17
+ sliceDurationSec = 30,
18
+ maxSlicesInMemory = 1,
19
+ sampleRate = 16000,
20
+ ) {
21
+ this.sliceDurationSec = sliceDurationSec
22
+ this.maxSlicesInMemory = maxSlicesInMemory
23
+ this.sampleRate = sampleRate
24
+ }
25
+
26
+ /**
27
+ * Add audio data to the current slice
28
+ */
29
+ addAudioData(audioData: Uint8Array): {
30
+ slice?: AudioSlice
31
+ } {
32
+ // Get or create current slice
33
+ const currentSlice = this.getCurrentSlice()
34
+
35
+ // Calculate bytes per slice (2 bytes per sample for 16-bit PCM)
36
+ const bytesPerSlice = this.sliceDurationSec * this.sampleRate * 2
37
+
38
+ // Check if adding this data would exceed slice capacity
39
+ if (currentSlice.sampleCount + audioData.length > bytesPerSlice) {
40
+ // Finalize current slice and create new one
41
+ this.finalizeCurrentSlice()
42
+ this.currentSliceIndex += 1
43
+ return this.addAudioData(audioData) // Recursively add to new slice
44
+ }
45
+
46
+ // Append data to current slice
47
+ const newData = new Uint8Array(currentSlice.sampleCount + audioData.length)
48
+ newData.set(currentSlice.data.subarray(0, currentSlice.sampleCount))
49
+ newData.set(audioData, currentSlice.sampleCount)
50
+
51
+ currentSlice.data = newData
52
+ currentSlice.sampleCount += audioData.length
53
+ currentSlice.endTime = Date.now()
54
+
55
+ // Check if slice is complete
56
+ const isSliceComplete = currentSlice.sampleCount >= bytesPerSlice * 0.8 // 80% full
57
+
58
+ if (isSliceComplete) {
59
+ this.finalizeCurrentSlice()
60
+ }
61
+
62
+ return { slice: currentSlice }
63
+ }
64
+
65
+ /**
66
+ * Get the current slice being built
67
+ */
68
+ private getCurrentSlice(): AudioSlice {
69
+ let slice = this.slices.find((s) => s.index === this.currentSliceIndex)
70
+
71
+ if (!slice) {
72
+ const bytesPerSlice = this.sliceDurationSec * this.sampleRate * 2 // 2 bytes per sample
73
+ slice = {
74
+ index: this.currentSliceIndex,
75
+ data: new Uint8Array(bytesPerSlice),
76
+ sampleCount: 0,
77
+ startTime: Date.now(),
78
+ endTime: Date.now(),
79
+ isProcessed: false,
80
+ isReleased: false,
81
+ }
82
+ this.slices.push(slice)
83
+
84
+ // Clean up old slices if we have too many
85
+ this.cleanupOldSlices()
86
+ }
87
+
88
+ return slice
89
+ }
90
+
91
+ /**
92
+ * Finalize the current slice
93
+ */
94
+ private finalizeCurrentSlice(): void {
95
+ const slice = this.slices.find((s) => s.index === this.currentSliceIndex)
96
+ if (slice && slice.sampleCount > 0) {
97
+ // Trim the data array to actual size
98
+ slice.data = slice.data.subarray(0, slice.sampleCount)
99
+ slice.endTime = Date.now()
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get a slice for transcription
105
+ */
106
+ getSliceForTranscription(): AudioSlice | null {
107
+ const slice = this.slices.find(
108
+ (s) => s.index === this.transcribeSliceIndex && !s.isProcessed,
109
+ )
110
+
111
+ if (slice && slice.sampleCount > 0) {
112
+ return slice
113
+ }
114
+
115
+ return null
116
+ }
117
+
118
+ /**
119
+ * Mark a slice as processed
120
+ */
121
+ markSliceAsProcessed(sliceIndex: number): void {
122
+ const slice = this.slices.find((s) => s.index === sliceIndex)
123
+ if (slice) {
124
+ slice.isProcessed = true
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Move to the next slice for transcription
130
+ */
131
+ moveToNextTranscribeSlice(): void {
132
+ this.transcribeSliceIndex += 1
133
+ }
134
+
135
+ /**
136
+ * Get audio data for transcription (base64 encoded)
137
+ */
138
+ getAudioDataForTranscription(sliceIndex: number): Uint8Array | null {
139
+ const slice = this.slices.find((s) => s.index === sliceIndex)
140
+ if (!slice || slice.sampleCount === 0) {
141
+ return null
142
+ }
143
+
144
+ return slice.data.subarray(0, slice.sampleCount)
145
+ }
146
+
147
+ /**
148
+ * Get a slice by index
149
+ */
150
+ getSliceByIndex(sliceIndex: number): AudioSlice | null {
151
+ return this.slices.find((s) => s.index === sliceIndex) || null
152
+ }
153
+
154
+ /**
155
+ * Clean up old slices to manage memory
156
+ */
157
+ private cleanupOldSlices(): void {
158
+ if (this.slices.length <= this.maxSlicesInMemory) {
159
+ return
160
+ }
161
+
162
+ // Sort slices by index
163
+ this.slices.sort((a, b) => a.index - b.index)
164
+
165
+ // Keep only the most recent slices
166
+ const slicesToKeep = this.slices.slice(-this.maxSlicesInMemory)
167
+ const slicesToRemove = this.slices.slice(0, -this.maxSlicesInMemory)
168
+
169
+ // Release old slices
170
+ slicesToRemove.forEach((slice) => {
171
+ if (!slice.isReleased) {
172
+ slice.isReleased = true
173
+ // Clear the audio data to free memory
174
+ slice.data = new Uint8Array(0)
175
+ }
176
+ })
177
+
178
+ this.slices = slicesToKeep
179
+ }
180
+
181
+ /**
182
+ * Get memory usage statistics
183
+ */
184
+ getMemoryUsage(): MemoryUsage {
185
+ const activeSlices = this.slices.filter((s) => !s.isReleased)
186
+ const totalBytes = activeSlices.reduce(
187
+ (sum, slice) => sum + slice.sampleCount,
188
+ 0,
189
+ )
190
+
191
+ // Estimate memory usage (Uint8Array = 1 byte per sample)
192
+ const estimatedMB = totalBytes / (1024 * 1024)
193
+
194
+ return {
195
+ slicesInMemory: activeSlices.length,
196
+ totalSamples: totalBytes / 2, // Convert bytes to samples (2 bytes per sample)
197
+ estimatedMB: Math.round(estimatedMB * 100) / 100, // Round to 2 decimal places
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Reset all slices and indices
203
+ */
204
+ reset(): void {
205
+ // Release all slices
206
+ this.slices.forEach((slice) => {
207
+ slice.isReleased = true
208
+ // Clear the audio data to free memory
209
+ slice.data = new Uint8Array(0)
210
+ })
211
+
212
+ // Reset state
213
+ this.slices = []
214
+ this.currentSliceIndex = 0
215
+ this.transcribeSliceIndex = 0
216
+ }
217
+
218
+ /**
219
+ * Get current slice information
220
+ */
221
+ getCurrentSliceInfo() {
222
+ return {
223
+ currentSliceIndex: this.currentSliceIndex,
224
+ transcribeSliceIndex: this.transcribeSliceIndex,
225
+ totalSlices: this.slices.length,
226
+ memoryUsage: this.getMemoryUsage(),
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Force move to the next slice, finalizing the current one regardless of capacity
232
+ */
233
+ forceNextSlice(): { slice?: AudioSlice } {
234
+ const currentSlice = this.slices.find(
235
+ (s) => s.index === this.currentSliceIndex,
236
+ )
237
+
238
+ if (currentSlice && currentSlice.sampleCount > 0) {
239
+ // Finalize current slice
240
+ this.finalizeCurrentSlice()
241
+
242
+ // Move to next slice
243
+ this.currentSliceIndex += 1
244
+
245
+ return { slice: currentSlice }
246
+ }
247
+
248
+ // If no current slice or it's empty, just move to next index
249
+ this.currentSliceIndex += 1
250
+ return {}
251
+ }
252
+ }
@@ -0,0 +1,143 @@
1
+ /* eslint-disable import/no-extraneous-dependencies */
2
+ // @ts-ignore
3
+ import LiveAudioStream from '@fugood/react-native-audio-pcm-stream'
4
+ import type { AudioStreamInterface, AudioStreamConfig, AudioStreamData } from '../types'
5
+ import { base64ToUint8Array } from '../../utils/common'
6
+
7
+ export class AudioPcmStreamAdapter implements AudioStreamInterface {
8
+ private isInitialized = false
9
+
10
+ private recording = false
11
+
12
+ private config: AudioStreamConfig | null = null
13
+
14
+ private dataCallback?: (data: AudioStreamData) => void
15
+
16
+ private errorCallback?: (error: string) => void
17
+
18
+ private statusCallback?: (isRecording: boolean) => void
19
+
20
+ async initialize(config: AudioStreamConfig): Promise<void> {
21
+ if (this.isInitialized) {
22
+ await this.release()
23
+ }
24
+
25
+ try {
26
+ this.config = config || null
27
+
28
+ // Initialize LiveAudioStream
29
+ LiveAudioStream.init({
30
+ sampleRate: config.sampleRate || 16000,
31
+ channels: config.channels || 1,
32
+ bitsPerSample: config.bitsPerSample || 16,
33
+ audioSource: config.audioSource || 6,
34
+ bufferSize: config.bufferSize || 16 * 1024,
35
+ wavFile: '', // We handle file writing separately
36
+ })
37
+
38
+ // Set up data listener
39
+ LiveAudioStream.on('data', this.handleAudioData.bind(this))
40
+
41
+ this.isInitialized = true
42
+ } catch (error) {
43
+ const errorMessage = error instanceof Error ? error.message : 'Unknown initialization error'
44
+ this.errorCallback?.(errorMessage)
45
+ throw new Error(`Failed to initialize LiveAudioStream: ${errorMessage}`)
46
+ }
47
+ }
48
+
49
+ async start(): Promise<void> {
50
+ if (!this.isInitialized) {
51
+ throw new Error('AudioStream not initialized')
52
+ }
53
+
54
+ if (this.recording) {
55
+ return
56
+ }
57
+
58
+ try {
59
+ LiveAudioStream.start()
60
+ this.recording = true
61
+ this.statusCallback?.(true)
62
+ } catch (error) {
63
+ const errorMessage = error instanceof Error ? error.message : 'Unknown start error'
64
+ this.errorCallback?.(errorMessage)
65
+ throw new Error(`Failed to start recording: ${errorMessage}`)
66
+ }
67
+ }
68
+
69
+ async stop(): Promise<void> {
70
+ if (!this.recording) {
71
+ return
72
+ }
73
+
74
+ try {
75
+ await LiveAudioStream.stop()
76
+ this.recording = false
77
+ this.statusCallback?.(false)
78
+ } catch (error) {
79
+ const errorMessage = error instanceof Error ? error.message : 'Unknown stop error'
80
+ this.errorCallback?.(errorMessage)
81
+ throw new Error(`Failed to stop recording: ${errorMessage}`)
82
+ }
83
+ }
84
+
85
+ isRecording(): boolean {
86
+ return this.recording
87
+ }
88
+
89
+ onData(callback: (data: AudioStreamData) => void): void {
90
+ this.dataCallback = callback
91
+ }
92
+
93
+ onError(callback: (error: string) => void): void {
94
+ this.errorCallback = callback
95
+ }
96
+
97
+ onStatusChange(callback: (isRecording: boolean) => void): void {
98
+ this.statusCallback = callback
99
+ }
100
+
101
+ async release(): Promise<void> {
102
+ if (this.recording) {
103
+ await this.stop()
104
+ }
105
+
106
+ try {
107
+ // LiveAudioStream doesn't have an explicit release method
108
+ // But we should remove listeners and reset state
109
+ this.isInitialized = false
110
+ this.config = null
111
+ this.dataCallback = undefined
112
+ this.errorCallback = undefined
113
+ this.statusCallback = undefined
114
+ } catch (error) {
115
+ console.warn('Error during LiveAudioStream release:', error)
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Handle incoming audio data from LiveAudioStream
121
+ */
122
+ private handleAudioData(base64Data: string): void {
123
+ if (!this.dataCallback) {
124
+ return
125
+ }
126
+
127
+ try {
128
+ const audioData = base64ToUint8Array(base64Data)
129
+
130
+ const streamData: AudioStreamData = {
131
+ data: audioData,
132
+ sampleRate: this.config?.sampleRate || 16000,
133
+ channels: this.config?.channels || 1,
134
+ timestamp: Date.now(),
135
+ }
136
+
137
+ this.dataCallback(streamData)
138
+ } catch (error) {
139
+ const errorMessage = error instanceof Error ? error.message : 'Audio processing error'
140
+ this.errorCallback?.(errorMessage)
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,251 @@
1
+ import type { AudioStreamInterface, AudioStreamConfig, AudioStreamData } from '../types'
2
+
3
+ export interface JestAudioStreamAdapterOptions {
4
+ sampleRate?: number
5
+ channels?: number
6
+ bitsPerSample?: number
7
+ simulateLatency?: number // milliseconds
8
+ simulateErrors?: boolean
9
+ simulateStartErrorOnly?: boolean // only simulate errors on start, not initialize
10
+ chunkSize?: number // bytes per chunk
11
+ chunkInterval?: number // milliseconds between chunks
12
+ maxChunks?: number // maximum number of chunks to send
13
+ audioData?: Uint8Array // pre-defined audio data to stream
14
+ generateSilence?: boolean // generate silence if no audioData provided
15
+ }
16
+
17
+ export class JestAudioStreamAdapter implements AudioStreamInterface {
18
+ private config: AudioStreamConfig | null = null
19
+
20
+ private options: JestAudioStreamAdapterOptions
21
+
22
+ private isInitialized = false
23
+
24
+ private recording = false
25
+
26
+ private dataCallback?: (data: AudioStreamData) => void
27
+
28
+ private errorCallback?: (error: string) => void
29
+
30
+ private statusCallback?: (isRecording: boolean) => void
31
+
32
+ private streamInterval?: ReturnType<typeof setTimeout>
33
+
34
+ private chunksSent = 0
35
+
36
+ private startTime = 0
37
+
38
+ constructor(options: JestAudioStreamAdapterOptions = {}) {
39
+ this.options = {
40
+ sampleRate: 16000,
41
+ channels: 1,
42
+ bitsPerSample: 16,
43
+ simulateLatency: 0,
44
+ simulateErrors: false,
45
+ chunkSize: 3200, // 100ms at 16kHz, 16-bit, mono
46
+ chunkInterval: 100, // 100ms
47
+ maxChunks: -1, // unlimited
48
+ generateSilence: true,
49
+ ...options,
50
+ }
51
+ }
52
+
53
+ async initialize(config: AudioStreamConfig): Promise<void> {
54
+ if (this.isInitialized) {
55
+ await this.release()
56
+ }
57
+
58
+ if (this.options.simulateLatency! > 0) {
59
+ await JestAudioStreamAdapter.delay(this.options.simulateLatency!)
60
+ }
61
+
62
+ if (this.options.simulateErrors && !this.options.simulateStartErrorOnly) {
63
+ throw new Error('Simulated initialization error')
64
+ }
65
+
66
+ this.config = config
67
+ this.isInitialized = true
68
+ }
69
+
70
+ async start(): Promise<void> {
71
+ if (!this.isInitialized) {
72
+ throw new Error('AudioStream not initialized')
73
+ }
74
+
75
+ if (this.recording) {
76
+ return
77
+ }
78
+
79
+ if (this.options.simulateLatency! > 0) {
80
+ await JestAudioStreamAdapter.delay(this.options.simulateLatency!)
81
+ }
82
+
83
+ if (this.options.simulateErrors) {
84
+ throw new Error('Simulated start error')
85
+ }
86
+
87
+ this.recording = true
88
+ this.chunksSent = 0
89
+ this.startTime = Date.now()
90
+ this.statusCallback?.(true)
91
+ this.startStreaming()
92
+ }
93
+
94
+ async stop(): Promise<void> {
95
+ if (!this.recording) {
96
+ return
97
+ }
98
+
99
+ if (this.options.simulateLatency! > 0) {
100
+ await JestAudioStreamAdapter.delay(this.options.simulateLatency!)
101
+ }
102
+
103
+ this.recording = false
104
+ this.statusCallback?.(false)
105
+
106
+ if (this.streamInterval) {
107
+ clearTimeout(this.streamInterval)
108
+ this.streamInterval = undefined
109
+ }
110
+ }
111
+
112
+ isRecording(): boolean {
113
+ return this.recording
114
+ }
115
+
116
+ onData(callback: (data: AudioStreamData) => void): void {
117
+ this.dataCallback = callback
118
+ }
119
+
120
+ onError(callback: (error: string) => void): void {
121
+ this.errorCallback = callback
122
+ }
123
+
124
+ onStatusChange(callback: (isRecording: boolean) => void): void {
125
+ this.statusCallback = callback
126
+ }
127
+
128
+ async release(): Promise<void> {
129
+ if (this.recording) {
130
+ await this.stop()
131
+ }
132
+
133
+ this.isInitialized = false
134
+ this.config = null
135
+ this.dataCallback = undefined
136
+ this.errorCallback = undefined
137
+ this.statusCallback = undefined
138
+ this.chunksSent = 0
139
+ }
140
+
141
+ // Test helper methods
142
+ simulateError(error: string): void {
143
+ this.errorCallback?.(error)
144
+ }
145
+
146
+ simulateDataChunk(data: Uint8Array): void {
147
+ if (!this.dataCallback || !this.config) {
148
+ return
149
+ }
150
+
151
+ const streamData: AudioStreamData = {
152
+ data,
153
+ sampleRate: this.config.sampleRate || this.options.sampleRate!,
154
+ channels: this.config.channels || this.options.channels!,
155
+ timestamp: Date.now(),
156
+ }
157
+
158
+ this.dataCallback(streamData)
159
+ }
160
+
161
+ getChunksSent(): number {
162
+ return this.chunksSent
163
+ }
164
+
165
+ getTotalBytesStreamed(): number {
166
+ return this.chunksSent * this.options.chunkSize!
167
+ }
168
+
169
+ getStreamDuration(): number {
170
+ return this.recording ? Date.now() - this.startTime : 0
171
+ }
172
+
173
+ private startStreaming(): void {
174
+ if (!this.dataCallback || !this.config) {
175
+ return
176
+ }
177
+
178
+ const streamChunk = () => {
179
+ if (!this.recording) {
180
+ return
181
+ }
182
+
183
+ // Check if we've reached the maximum chunks
184
+ if (this.options.maxChunks! > 0 && this.chunksSent >= this.options.maxChunks!) {
185
+ this.stop()
186
+ return
187
+ }
188
+
189
+ // Generate or use provided audio data
190
+ const audioData = this.generateAudioChunk()
191
+
192
+ if (audioData) {
193
+ this.simulateDataChunk(audioData)
194
+ this.chunksSent += 1
195
+ }
196
+
197
+ // Schedule next chunk if still recording
198
+ if (this.recording) {
199
+ this.streamInterval = setTimeout(streamChunk, this.options.chunkInterval!)
200
+ }
201
+ }
202
+
203
+ // Start streaming after a short delay
204
+ this.streamInterval = setTimeout(streamChunk, this.options.chunkInterval!)
205
+ }
206
+
207
+ private generateAudioChunk(): Uint8Array | null {
208
+ // If we have pre-defined audio data, use it
209
+ if (this.options.audioData) {
210
+ const startByte = this.chunksSent * this.options.chunkSize!
211
+ const endByte = Math.min(startByte + this.options.chunkSize!, this.options.audioData.length)
212
+
213
+ if (startByte >= this.options.audioData.length) {
214
+ return null // No more data
215
+ }
216
+
217
+ return this.options.audioData.subarray(startByte, endByte)
218
+ }
219
+
220
+ // Generate silence or simple tone
221
+ const chunkSize = this.options.chunkSize!
222
+ const audioData = new Uint8Array(chunkSize)
223
+
224
+ if (this.options.generateSilence) {
225
+ // Generate silence (all zeros)
226
+ audioData.fill(0)
227
+ } else {
228
+ // Generate a simple sine wave tone for testing
229
+ const sampleRate = this.options.sampleRate!
230
+ const frequency = 440 // A4 note
231
+ const samplesPerChunk = chunkSize / 2 // 16-bit samples
232
+ const timeOffset = (this.chunksSent * samplesPerChunk) / sampleRate
233
+
234
+ for (let i = 0; i < samplesPerChunk; i += 1) {
235
+ const time = timeOffset + i / sampleRate
236
+ const amplitude = Math.sin(2 * Math.PI * frequency * time) * 0.5
237
+ const sample = Math.round(amplitude * 32767) // 16-bit signed sample
238
+
239
+ // Convert to little-endian bytes
240
+ audioData[i * 2] = sample % 256
241
+ audioData[i * 2 + 1] = Math.floor(sample / 256) % 256
242
+ }
243
+ }
244
+
245
+ return audioData
246
+ }
247
+
248
+ private static delay(ms: number): Promise<void> {
249
+ return new Promise(resolve => setTimeout(resolve, ms))
250
+ }
251
+ }