@siteed/expo-audio-studio 2.4.0 → 2.5.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 (81) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +25 -0
  3. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +22 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +1501 -0
  5. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +10 -5
  6. package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +27 -25
  7. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +73 -71
  8. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +581 -255
  9. package/android/src/main/java/net/siteed/audiostream/Constants.kt +17 -1
  10. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +435 -158
  11. package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +65 -0
  12. package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +14 -5
  13. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +9 -1
  14. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  15. package/build/AudioDeviceManager.d.ts +107 -0
  16. package/build/AudioDeviceManager.d.ts.map +1 -0
  17. package/build/AudioDeviceManager.js +493 -0
  18. package/build/AudioDeviceManager.js.map +1 -0
  19. package/build/AudioRecorder.provider.d.ts.map +1 -1
  20. package/build/AudioRecorder.provider.js +3 -0
  21. package/build/AudioRecorder.provider.js.map +1 -1
  22. package/build/ExpoAudioStream.types.d.ts +90 -1
  23. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  24. package/build/ExpoAudioStream.types.js +7 -1
  25. package/build/ExpoAudioStream.types.js.map +1 -1
  26. package/build/ExpoAudioStream.web.d.ts +37 -0
  27. package/build/ExpoAudioStream.web.d.ts.map +1 -1
  28. package/build/ExpoAudioStream.web.js +399 -54
  29. package/build/ExpoAudioStream.web.js.map +1 -1
  30. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  31. package/build/ExpoAudioStreamModule.js +20 -0
  32. package/build/ExpoAudioStreamModule.js.map +1 -1
  33. package/build/WebRecorder.web.d.ts +63 -10
  34. package/build/WebRecorder.web.d.ts.map +1 -1
  35. package/build/WebRecorder.web.js +277 -68
  36. package/build/WebRecorder.web.js.map +1 -1
  37. package/build/hooks/useAudioDevices.d.ts +14 -0
  38. package/build/hooks/useAudioDevices.d.ts.map +1 -0
  39. package/build/hooks/useAudioDevices.js +151 -0
  40. package/build/hooks/useAudioDevices.js.map +1 -0
  41. package/build/index.d.ts +2 -0
  42. package/build/index.d.ts.map +1 -1
  43. package/build/index.js +4 -0
  44. package/build/index.js.map +1 -1
  45. package/build/useAudioRecorder.d.ts +1 -0
  46. package/build/useAudioRecorder.d.ts.map +1 -1
  47. package/build/useAudioRecorder.js +20 -1
  48. package/build/useAudioRecorder.js.map +1 -1
  49. package/build/utils/BlobFix.d.ts.map +1 -1
  50. package/build/utils/BlobFix.js +2 -2
  51. package/build/utils/BlobFix.js.map +1 -1
  52. package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
  53. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
  54. package/build/workers/InlineFeaturesExtractor.web.js +27 -26
  55. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
  56. package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
  57. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
  58. package/build/workers/inlineAudioWebWorker.web.js +25 -1
  59. package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
  60. package/ios/AudioDeviceManager.swift +654 -0
  61. package/ios/AudioStreamManager.swift +964 -760
  62. package/ios/ExpoAudioStreamModule.swift +174 -19
  63. package/ios/Features.swift +1 -1
  64. package/ios/ISSUE_IOS.md +45 -0
  65. package/ios/Logger.swift +13 -1
  66. package/ios/RecordingSettings.swift +12 -0
  67. package/package.json +2 -2
  68. package/src/AudioAnalysis/AudioAnalysis.types.ts +2 -2
  69. package/src/AudioDeviceManager.ts +571 -0
  70. package/src/AudioRecorder.provider.tsx +3 -0
  71. package/src/ExpoAudioStream.types.ts +97 -1
  72. package/src/ExpoAudioStream.web.ts +513 -63
  73. package/src/ExpoAudioStreamModule.ts +23 -0
  74. package/src/WebRecorder.web.ts +346 -81
  75. package/src/hooks/useAudioDevices.ts +180 -0
  76. package/src/index.ts +6 -0
  77. package/src/types/crc-32.d.ts +6 -6
  78. package/src/useAudioRecorder.tsx +27 -1
  79. package/src/utils/BlobFix.ts +6 -4
  80. package/src/workers/InlineFeaturesExtractor.web.tsx +27 -26
  81. package/src/workers/inlineAudioWebWorker.web.tsx +25 -1
@@ -8,7 +8,7 @@ interface AudioFeaturesEvent {
8
8
  };
9
9
  }
10
10
  export declare class WebRecorder {
11
- private audioContext;
11
+ audioContext: AudioContext;
12
12
  private audioWorkletNode;
13
13
  private featureExtractorWorker?;
14
14
  private source;
@@ -20,14 +20,25 @@ export declare class WebRecorder {
20
20
  private bitDepth;
21
21
  private exportBitDepth;
22
22
  private audioAnalysisData;
23
- private packetCount;
24
23
  private logger?;
25
24
  private compressedMediaRecorder;
26
25
  private compressedChunks;
27
26
  private compressedSize;
28
27
  private pendingCompressedChunk;
29
- private readonly wavMimeType;
30
28
  private dataPointIdCounter;
29
+ private deviceDisconnectionHandler;
30
+ private mediaStream;
31
+ private onInterruptionCallback?;
32
+ private _isDeviceDisconnected;
33
+ /**
34
+ * Flag to indicate whether this is the first audio chunk after a device switch
35
+ * Used to maintain proper duration counting
36
+ */
37
+ isFirstChunkAfterSwitch: boolean;
38
+ /**
39
+ * Gets whether the recording device has been disconnected
40
+ */
41
+ get isDeviceDisconnected(): boolean;
31
42
  /**
32
43
  * Initializes a new WebRecorder instance for audio recording and processing
33
44
  * @param audioContext - The AudioContext to use for recording
@@ -35,14 +46,20 @@ export declare class WebRecorder {
35
46
  * @param recordingConfig - Configuration options for the recording
36
47
  * @param emitAudioEventCallback - Callback function for audio data events
37
48
  * @param emitAudioAnalysisCallback - Callback function for audio analysis events
49
+ * @param onInterruption - Callback for recording interruptions
38
50
  * @param logger - Optional logger for debugging information
39
51
  */
40
- constructor({ audioContext, source, recordingConfig, emitAudioEventCallback, emitAudioAnalysisCallback, logger, }: {
52
+ constructor({ audioContext, source, recordingConfig, emitAudioEventCallback, emitAudioAnalysisCallback, onInterruption, logger, }: {
41
53
  audioContext: AudioContext;
42
54
  source: MediaStreamAudioSourceNode;
43
55
  recordingConfig: RecordingConfig;
44
56
  emitAudioEventCallback: EmitAudioEventFunction;
45
57
  emitAudioAnalysisCallback: EmitAudioAnalysisFunction;
58
+ onInterruption?: (event: {
59
+ reason: string;
60
+ isPaused: boolean;
61
+ timestamp: number;
62
+ }) => void;
46
63
  logger?: ConsoleLike;
47
64
  });
48
65
  /**
@@ -62,20 +79,32 @@ export declare class WebRecorder {
62
79
  */
63
80
  handleFeatureExtractorMessage(event: AudioFeaturesEvent): void;
64
81
  /**
65
- * Resets the data point ID counter
66
- * Used when starting a new recording
82
+ * Reset the data point counter to a specific value or zero
83
+ * @param startCounterFrom Optional value to start the counter from (for continuing from previous recordings)
67
84
  */
68
- resetDataPointCounter(): void;
85
+ resetDataPointCounter(startCounterFrom?: number): void;
86
+ /**
87
+ * Get the current data point counter value
88
+ * @returns The current value of the data point counter
89
+ */
90
+ getDataPointCounter(): number;
91
+ /**
92
+ * Prepares the recorder for continuity after device switch
93
+ * Sets up all necessary state to maintain proper recording continuity
94
+ */
95
+ prepareForDeviceSwitch(): void;
69
96
  /**
70
97
  * Starts the audio recording process
71
98
  * Connects the audio nodes and begins capturing audio data
99
+ * @param preserveCounters If true, do not reset the counter (used for device switching)
72
100
  */
73
- start(): void;
101
+ start(preserveCounters?: boolean): void;
74
102
  /**
75
103
  * Stops the audio recording process and returns the recorded data
104
+ * @param externalAudioChunks Optional array of Float32Array chunks from previous devices
76
105
  * @returns Promise resolving to an object containing PCM data and optional compressed blob
77
106
  */
78
- stop(): Promise<{
107
+ stop(externalAudioChunks?: Float32Array[]): Promise<{
79
108
  pcmData: Float32Array;
80
109
  compressedBlob?: Blob;
81
110
  }>;
@@ -83,7 +112,7 @@ export declare class WebRecorder {
83
112
  * Cleans up resources when recording is stopped
84
113
  * Closes audio context and disconnects nodes
85
114
  */
86
- private cleanup;
115
+ cleanup(): void;
87
116
  /**
88
117
  * Pauses the audio recording process
89
118
  * Disconnects audio nodes and pauses the media recorder
@@ -114,6 +143,30 @@ export declare class WebRecorder {
114
143
  * Processes features if enabled
115
144
  */
116
145
  processFeatures(chunk: Float32Array, sampleRate: number, chunkPosition: number, startPosition: number, endPosition: number, samples: number): void;
146
+ /**
147
+ * Sets up detection for device disconnection events
148
+ */
149
+ private setupDeviceDisconnectionDetection;
150
+ /**
151
+ * Explicitly set the position for continuous recording across device switches
152
+ * @param position The position in seconds to continue from
153
+ */
154
+ setPosition(position: number): void;
155
+ /**
156
+ * Get the current position in seconds
157
+ * @returns The current position
158
+ */
159
+ getPosition(): number;
160
+ /**
161
+ * Gets the current compressed chunks
162
+ * @returns Array of current compressed audio chunks
163
+ */
164
+ getCompressedChunks(): Blob[];
165
+ /**
166
+ * Sets the compressed chunks from a previous recorder
167
+ * @param chunks Array of compressed chunks from a previous recorder
168
+ */
169
+ setCompressedChunks(chunks: Blob[]): void;
117
170
  }
118
171
  export {};
119
172
  //# sourceMappingURL=WebRecorder.web.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"WebRecorder.web.d.ts","sourceRoot":"","sources":["../src/WebRecorder.web.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAA;AACnE,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AACtE,OAAO,EACH,yBAAyB,EACzB,sBAAsB,EACzB,MAAM,uBAAuB,CAAA;AAa9B,UAAU,kBAAkB;IACxB,IAAI,EAAE;QACF,OAAO,EAAE,MAAM,CAAA;QACf,MAAM,EAAE,aAAa,CAAA;KACxB,CAAA;CACJ;AASD,qBAAa,WAAW;IACpB,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,sBAAsB,CAAC,CAAQ;IACvC,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,sBAAsB,CAAwB;IACtD,OAAO,CAAC,yBAAyB,CAA2B;IAC5D,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,QAAQ,CAAY;IAC5B,OAAO,CAAC,gBAAgB,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,iBAAiB,CAAe;IACxC,OAAO,CAAC,WAAW,CAAY;IAC/B,OAAO,CAAC,MAAM,CAAC,CAAa;IAC5B,OAAO,CAAC,uBAAuB,CAA6B;IAC5D,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,cAAc,CAAY;IAClC,OAAO,CAAC,sBAAsB,CAAoB;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,kBAAkB,CAAY;IAEtC;;;;;;;;OAQG;gBACS,EACR,YAAY,EACZ,MAAM,EACN,eAAe,EACf,sBAAsB,EACtB,yBAAyB,EACzB,MAAM,GACT,EAAE;QACC,YAAY,EAAE,YAAY,CAAA;QAC1B,MAAM,EAAE,0BAA0B,CAAA;QAClC,eAAe,EAAE,eAAe,CAAA;QAChC,sBAAsB,EAAE,sBAAsB,CAAA;QAC9C,yBAAyB,EAAE,yBAAyB,CAAA;QACpD,MAAM,CAAC,EAAE,WAAW,CAAA;KACvB;IAmDD;;;OAGG;IACG,IAAI;IAqHV;;;OAGG;IACH,0BAA0B;IAuB1B;;;;OAIG;IACH,6BAA6B,CAAC,KAAK,EAAE,kBAAkB;IA+FvD;;;OAGG;IACH,qBAAqB;IAYrB;;;OAGG;IACH,KAAK;IAaL;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,YAAY,CAAC;QAAC,cAAc,CAAC,EAAE,IAAI,CAAA;KAAE,CAAC;IAqBvE;;;OAGG;IACH,OAAO,CAAC,OAAO;IAaf;;;OAGG;IACH,KAAK;IAOL;;;OAGG;IACH,qBAAqB;IAMrB;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAoB/B;;;OAGG;IACH,MAAM;IAON;;;OAGG;IACH,OAAO,CAAC,4BAA4B;IAkCpC;;OAEG;IACH,eAAe,CACX,KAAK,EAAE,YAAY,EACnB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM;CAsBtB"}
1
+ {"version":3,"file":"WebRecorder.web.d.ts","sourceRoot":"","sources":["../src/WebRecorder.web.ts"],"names":[],"mappings":"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"}
@@ -20,14 +20,27 @@ export class WebRecorder {
20
20
  bitDepth; // Bit depth of the audio
21
21
  exportBitDepth; // Bit depth of the audio
22
22
  audioAnalysisData; // Keep updating the full audio analysis data with latest events
23
- packetCount = 0;
24
23
  logger;
25
24
  compressedMediaRecorder = null;
26
25
  compressedChunks = [];
27
26
  compressedSize = 0;
28
27
  pendingCompressedChunk = null;
29
- wavMimeType = 'audio/wav';
30
28
  dataPointIdCounter = 0; // Add this property to track the counter
29
+ deviceDisconnectionHandler = null;
30
+ mediaStream = null;
31
+ onInterruptionCallback;
32
+ _isDeviceDisconnected = false;
33
+ /**
34
+ * Flag to indicate whether this is the first audio chunk after a device switch
35
+ * Used to maintain proper duration counting
36
+ */
37
+ isFirstChunkAfterSwitch = false;
38
+ /**
39
+ * Gets whether the recording device has been disconnected
40
+ */
41
+ get isDeviceDisconnected() {
42
+ return this._isDeviceDisconnected;
43
+ }
31
44
  /**
32
45
  * Initializes a new WebRecorder instance for audio recording and processing
33
46
  * @param audioContext - The AudioContext to use for recording
@@ -35,9 +48,10 @@ export class WebRecorder {
35
48
  * @param recordingConfig - Configuration options for the recording
36
49
  * @param emitAudioEventCallback - Callback function for audio data events
37
50
  * @param emitAudioAnalysisCallback - Callback function for audio analysis events
51
+ * @param onInterruption - Callback for recording interruptions
38
52
  * @param logger - Optional logger for debugging information
39
53
  */
40
- constructor({ audioContext, source, recordingConfig, emitAudioEventCallback, emitAudioAnalysisCallback, logger, }) {
54
+ constructor({ audioContext, source, recordingConfig, emitAudioEventCallback, emitAudioAnalysisCallback, onInterruption, logger, }) {
41
55
  this.audioContext = audioContext;
42
56
  this.source = source;
43
57
  this.emitAudioEventCallback = emitAudioEventCallback;
@@ -80,6 +94,10 @@ export class WebRecorder {
80
94
  if (recordingConfig.compression?.enabled) {
81
95
  this.initializeCompressedRecorder();
82
96
  }
97
+ this.mediaStream = source.mediaStream;
98
+ this.onInterruptionCallback = onInterruption;
99
+ // Setup device disconnection detection
100
+ this.setupDeviceDisconnectionDetection();
83
101
  }
84
102
  /**
85
103
  * Initializes the audio worklet using an inline script
@@ -107,12 +125,16 @@ export class WebRecorder {
107
125
  const chunkSize = this.audioContext.sampleRate * 2; // Reduce to 2 seconds chunks
108
126
  const sampleRate = event.data.sampleRate ?? this.audioContext.sampleRate;
109
127
  const duration = pcmBufferFloat.length / sampleRate;
128
+ // Use incoming position if provided by worklet, otherwise use our tracked position
129
+ const incomingPosition = typeof event.data.position === 'number'
130
+ ? event.data.position
131
+ : this.position;
110
132
  // Calculate bytes per sample based on bit depth
111
133
  const bytesPerSample = this.bitDepth / 8;
112
134
  // Emit chunks without storing them
113
135
  for (let i = 0; i < pcmBufferFloat.length; i += chunkSize) {
114
136
  const chunk = pcmBufferFloat.slice(i, i + chunkSize);
115
- const chunkPosition = this.position + i / sampleRate;
137
+ const chunkPosition = incomingPosition + i / sampleRate;
116
138
  // Calculate byte positions and samples
117
139
  const startPosition = Math.floor(i * bytesPerSample);
118
140
  const endPosition = Math.floor((i + chunk.length) * bytesPerSample);
@@ -153,10 +175,11 @@ export class WebRecorder {
153
175
  : undefined,
154
176
  });
155
177
  }
156
- this.position += duration;
178
+ // Update our position based on the worklet's position if provided
179
+ this.position = incomingPosition + duration;
157
180
  this.pendingCompressedChunk = null;
158
181
  };
159
- this.logger?.debug(`WebRecorder initialized -- recordSampleRate=${this.audioContext.sampleRate}`, this.config);
182
+ this.logger?.debug(`WebRecorder initialized -- recordSampleRate=${this.audioContext.sampleRate}, startPosition=${this.position}`, this.config);
160
183
  this.audioWorkletNode.port.postMessage({
161
184
  command: 'init',
162
185
  recordSampleRate: this.audioContext.sampleRate,
@@ -165,6 +188,7 @@ export class WebRecorder {
165
188
  exportBitDepth: this.exportBitDepth,
166
189
  channels: this.numberOfChannels,
167
190
  interval: this.config.interval ?? DEFAULT_WEB_INTERVAL,
191
+ position: this.position, // Pass the current position to the processor
168
192
  // enableLogging: !!this.logger,
169
193
  });
170
194
  // Connect the source to the AudioWorkletNode and start recording
@@ -191,6 +215,14 @@ export class WebRecorder {
191
215
  this.featureExtractorWorker.onerror = (error) => {
192
216
  console.error(`[${TAG}] Feature extractor worker error:`, error);
193
217
  };
218
+ // Initialize worker with counter if needed
219
+ if (this.dataPointIdCounter > 0) {
220
+ this.featureExtractorWorker.postMessage({
221
+ command: 'resetCounter',
222
+ value: this.dataPointIdCounter,
223
+ });
224
+ this.logger?.debug(`Initialized worker with counter value ${this.dataPointIdCounter}`);
225
+ }
194
226
  this.logger?.log('Feature extractor worker initialized successfully');
195
227
  }
196
228
  catch (error) {
@@ -205,29 +237,33 @@ export class WebRecorder {
205
237
  handleFeatureExtractorMessage(event) {
206
238
  if (event.data.command === 'features') {
207
239
  const segmentResult = event.data.result;
208
- // Update the dataPointIdCounter based on the last ID received
209
- if (segmentResult.dataPoints &&
210
- segmentResult.dataPoints.length > 0) {
211
- const lastDataPoint = segmentResult.dataPoints[segmentResult.dataPoints.length - 1];
240
+ // Track existing IDs to prevent duplicates
241
+ const existingIds = new Set(this.audioAnalysisData.dataPoints.map((dp) => dp.id));
242
+ // Filter out datapoints with duplicate IDs
243
+ const uniqueNewDataPoints = segmentResult.dataPoints.filter((dp) => {
244
+ return !existingIds.has(dp.id);
245
+ });
246
+ // Log filtered duplicates if any
247
+ if (uniqueNewDataPoints.length < segmentResult.dataPoints.length &&
248
+ this.logger?.warn) {
249
+ this.logger.warn(`Filtered ${segmentResult.dataPoints.length - uniqueNewDataPoints.length} duplicate datapoints`);
250
+ }
251
+ // Update counter based on the highest ID seen
252
+ if (uniqueNewDataPoints.length > 0) {
253
+ const lastDataPoint = uniqueNewDataPoints[uniqueNewDataPoints.length - 1];
212
254
  if (lastDataPoint && typeof lastDataPoint.id === 'number') {
213
- this.dataPointIdCounter = Math.max(this.dataPointIdCounter, lastDataPoint.id + 1);
255
+ const nextIdValue = lastDataPoint.id + 1;
256
+ if (nextIdValue > this.dataPointIdCounter) {
257
+ this.dataPointIdCounter = nextIdValue;
258
+ this.logger?.debug(`Counter updated to ${this.dataPointIdCounter}`);
259
+ }
214
260
  }
215
261
  }
216
- this.logger?.debug('[WebRecorder] Raw segment result:', {
217
- dataPointsLength: segmentResult.dataPoints.length,
218
- durationMs: segmentResult.durationMs,
219
- sampleRate: segmentResult.sampleRate,
220
- amplitudeRange: segmentResult.amplitudeRange,
221
- });
222
- // Ensure consistent sample rate in the result
223
- segmentResult.sampleRate =
224
- this.config.sampleRate || this.audioContext.sampleRate;
225
- // Update the full audio analysis data with proper range merging
226
- this.audioAnalysisData.dataPoints.push(...segmentResult.dataPoints);
262
+ // Add unique data points to our analysis data
263
+ this.audioAnalysisData.dataPoints.push(...uniqueNewDataPoints);
227
264
  this.audioAnalysisData.durationMs += segmentResult.durationMs;
228
- // Make sure the sample rate is consistent
229
265
  this.audioAnalysisData.sampleRate = segmentResult.sampleRate;
230
- // Properly merge amplitude ranges
266
+ // Merge amplitude ranges
231
267
  if (segmentResult.amplitudeRange) {
232
268
  if (!this.audioAnalysisData.amplitudeRange) {
233
269
  this.audioAnalysisData.amplitudeRange = {
@@ -241,7 +277,7 @@ export class WebRecorder {
241
277
  };
242
278
  }
243
279
  }
244
- // Properly merge RMS ranges
280
+ // Merge RMS ranges
245
281
  if (segmentResult.rmsRange) {
246
282
  if (!this.audioAnalysisData.rmsRange) {
247
283
  this.audioAnalysisData.rmsRange = {
@@ -255,61 +291,98 @@ export class WebRecorder {
255
291
  };
256
292
  }
257
293
  }
258
- this.logger?.debug('features event segmentResult', segmentResult);
259
- this.logger?.debug(`features event audioAnalysisData duration=${this.audioAnalysisData.durationMs}`, this.audioAnalysisData);
260
- this.emitAudioAnalysisCallback(segmentResult);
261
- this.logger?.debug('[WebRecorder] Updated audioAnalysisData:', {
262
- dataPointsLength: this.audioAnalysisData.dataPoints.length,
263
- durationMs: this.audioAnalysisData.durationMs,
264
- sampleRate: this.audioAnalysisData.sampleRate,
265
- amplitudeRange: this.audioAnalysisData.amplitudeRange,
266
- });
294
+ // Send filtered result to avoid duplicate IDs
295
+ const filteredSegmentResult = {
296
+ ...segmentResult,
297
+ dataPoints: uniqueNewDataPoints,
298
+ };
299
+ this.emitAudioAnalysisCallback(filteredSegmentResult);
267
300
  }
268
301
  }
269
302
  /**
270
- * Resets the data point ID counter
271
- * Used when starting a new recording
303
+ * Reset the data point counter to a specific value or zero
304
+ * @param startCounterFrom Optional value to start the counter from (for continuing from previous recordings)
272
305
  */
273
- resetDataPointCounter() {
274
- this.dataPointIdCounter = 0;
275
- // Reset the counter in the worker
306
+ resetDataPointCounter(startCounterFrom) {
307
+ // Set the counter with the passed value or 0
308
+ this.dataPointIdCounter =
309
+ startCounterFrom !== undefined ? startCounterFrom : 0;
310
+ this.logger?.debug(`Reset data point counter to ${this.dataPointIdCounter}`);
311
+ // Update worker counter if available
276
312
  if (this.featureExtractorWorker) {
277
313
  this.featureExtractorWorker.postMessage({
278
314
  command: 'resetCounter',
279
- startCounterFrom: 0,
315
+ value: this.dataPointIdCounter,
280
316
  });
281
317
  }
318
+ else {
319
+ this.logger?.warn('No feature extractor worker available to update counter');
320
+ }
321
+ }
322
+ /**
323
+ * Get the current data point counter value
324
+ * @returns The current value of the data point counter
325
+ */
326
+ getDataPointCounter() {
327
+ return this.dataPointIdCounter;
328
+ }
329
+ /**
330
+ * Prepares the recorder for continuity after device switch
331
+ * Sets up all necessary state to maintain proper recording continuity
332
+ */
333
+ prepareForDeviceSwitch() {
334
+ this.isFirstChunkAfterSwitch = true;
335
+ this.logger?.debug(`Prepared for device switch at position ${this.position}s`);
282
336
  }
283
337
  /**
284
338
  * Starts the audio recording process
285
339
  * Connects the audio nodes and begins capturing audio data
340
+ * @param preserveCounters If true, do not reset the counter (used for device switching)
286
341
  */
287
- start() {
342
+ start(preserveCounters = false) {
288
343
  this.source.connect(this.audioWorkletNode);
289
344
  this.audioWorkletNode.connect(this.audioContext.destination);
290
- this.packetCount = 0;
291
- // Reset the counter when starting a new recording
292
- this.resetDataPointCounter();
345
+ // Only reset the counter when not preserving state (e.g., for a fresh recording)
346
+ if (!preserveCounters) {
347
+ this.logger?.debug('Starting fresh recording, resetting counter to 0');
348
+ this.resetDataPointCounter(0); // Explicitly reset to 0 for new recordings
349
+ this.isFirstChunkAfterSwitch = false;
350
+ }
351
+ else {
352
+ this.logger?.debug(`Preserving counter at ${this.dataPointIdCounter} during device switch`);
353
+ }
293
354
  if (this.compressedMediaRecorder) {
294
355
  this.compressedMediaRecorder.start(this.config.interval ?? 1000);
295
356
  }
296
357
  }
297
358
  /**
298
359
  * Stops the audio recording process and returns the recorded data
360
+ * @param externalAudioChunks Optional array of Float32Array chunks from previous devices
299
361
  * @returns Promise resolving to an object containing PCM data and optional compressed blob
300
362
  */
301
- async stop() {
363
+ async stop(externalAudioChunks) {
302
364
  try {
303
- if (this.compressedMediaRecorder) {
365
+ // Log what's happening for debugging
366
+ this.logger?.debug('Stopping recording and collecting final data');
367
+ // Stop any compressed recording first
368
+ if (this.compressedMediaRecorder &&
369
+ this.compressedMediaRecorder.state !== 'inactive') {
304
370
  this.compressedMediaRecorder.stop();
305
- return {
306
- pcmData: new Float32Array(), // Return empty array since we're streaming
307
- compressedBlob: new Blob(this.compressedChunks, {
308
- type: 'audio/webm;codecs=opus',
309
- }),
310
- };
311
371
  }
312
- return { pcmData: new Float32Array() };
372
+ // Wait for any pending compressed chunks to be processed
373
+ if (this.compressedMediaRecorder) {
374
+ // Small delay to ensure all data is processed
375
+ await new Promise((resolve) => setTimeout(resolve, 100));
376
+ }
377
+ // Return the compressed blob if available
378
+ return {
379
+ pcmData: new Float32Array(), // Return empty array since we're streaming
380
+ compressedBlob: this.compressedChunks.length > 0
381
+ ? new Blob(this.compressedChunks, {
382
+ type: 'audio/webm;codecs=opus',
383
+ })
384
+ : undefined,
385
+ };
313
386
  }
314
387
  finally {
315
388
  this.cleanup();
@@ -324,26 +397,63 @@ export class WebRecorder {
324
397
  * Closes audio context and disconnects nodes
325
398
  */
326
399
  cleanup() {
327
- if (this.audioContext) {
328
- this.audioContext.close();
400
+ // Remove device disconnection handler
401
+ if (this.deviceDisconnectionHandler) {
402
+ this.deviceDisconnectionHandler();
403
+ this.deviceDisconnectionHandler = null;
329
404
  }
405
+ // Check if AudioContext is already closed before attempting to close it
406
+ if (this.audioContext && this.audioContext.state !== 'closed') {
407
+ try {
408
+ this.audioContext.close();
409
+ }
410
+ catch (e) {
411
+ // Ignore closure errors - this happens if already closed
412
+ }
413
+ }
414
+ // Safely disconnect audioWorkletNode if it exists
330
415
  if (this.audioWorkletNode) {
331
- this.audioWorkletNode.disconnect();
416
+ try {
417
+ this.audioWorkletNode.disconnect();
418
+ }
419
+ catch (e) {
420
+ // Ignore disconnection errors - node might be already disconnected
421
+ }
332
422
  }
423
+ // Safely disconnect source if it exists
333
424
  if (this.source) {
334
- this.source.disconnect();
425
+ try {
426
+ this.source.disconnect();
427
+ }
428
+ catch (e) {
429
+ // Ignore disconnection errors - source might be already disconnected
430
+ }
335
431
  }
432
+ // Always stop media stream tracks to release hardware resources
336
433
  this.stopMediaStreamTracks();
434
+ // Mark as disconnected to prevent future errors
435
+ this._isDeviceDisconnected = true;
337
436
  }
338
437
  /**
339
438
  * Pauses the audio recording process
340
439
  * Disconnects audio nodes and pauses the media recorder
341
440
  */
342
441
  pause() {
343
- this.source.disconnect(this.audioWorkletNode); // Disconnect the source from the AudioWorkletNode
344
- this.audioWorkletNode.disconnect(this.audioContext.destination); // Disconnect the AudioWorkletNode from the destination
345
- this.audioWorkletNode.port.postMessage({ command: 'pause' });
346
- this.compressedMediaRecorder?.pause();
442
+ try {
443
+ // Note: We're just pausing, not disconnecting the device
444
+ // Simply disconnect nodes temporarily without marking device as disconnected
445
+ this.source.disconnect(this.audioWorkletNode);
446
+ this.audioWorkletNode.disconnect(this.audioContext.destination);
447
+ this.audioWorkletNode.port.postMessage({ command: 'pause' });
448
+ if (this.compressedMediaRecorder?.state === 'recording') {
449
+ this.compressedMediaRecorder.pause();
450
+ }
451
+ this.logger?.debug('Recording paused successfully');
452
+ }
453
+ catch (error) {
454
+ this.logger?.error('Error in pause(): ', error);
455
+ // Already disconnected, just ignore and continue
456
+ }
347
457
  }
348
458
  /**
349
459
  * Stops all media stream tracks to release hardware resources
@@ -351,8 +461,14 @@ export class WebRecorder {
351
461
  */
352
462
  stopMediaStreamTracks() {
353
463
  // Stop all audio tracks to stop the recording icon
354
- const tracks = this.source.mediaStream.getTracks();
355
- tracks.forEach((track) => track.stop());
464
+ if (this.mediaStream) {
465
+ const tracks = this.mediaStream.getTracks();
466
+ tracks.forEach((track) => track.stop());
467
+ }
468
+ else if (this.source?.mediaStream) {
469
+ const tracks = this.source.mediaStream.getTracks();
470
+ tracks.forEach((track) => track.stop());
471
+ }
356
472
  }
357
473
  /**
358
474
  * Determines the audio format capabilities of the current audio context
@@ -377,10 +493,20 @@ export class WebRecorder {
377
493
  * Reconnects audio nodes and resumes the media recorder
378
494
  */
379
495
  resume() {
380
- this.source.connect(this.audioWorkletNode);
381
- this.audioWorkletNode.connect(this.audioContext.destination);
382
- this.audioWorkletNode.port.postMessage({ command: 'resume' });
383
- this.compressedMediaRecorder?.resume();
496
+ // If device was disconnected, we can't resume
497
+ if (this._isDeviceDisconnected) {
498
+ this.logger?.warn('Cannot resume recording: device disconnected');
499
+ return;
500
+ }
501
+ try {
502
+ this.source.connect(this.audioWorkletNode);
503
+ this.audioWorkletNode.connect(this.audioContext.destination);
504
+ this.audioWorkletNode.port.postMessage({ command: 'resume' });
505
+ this.compressedMediaRecorder?.resume();
506
+ }
507
+ catch (error) {
508
+ this.logger?.error('Error in resume(): ', error);
509
+ }
384
510
  }
385
511
  /**
386
512
  * Initializes the compressed media recorder if compression is enabled
@@ -428,9 +554,92 @@ export class WebRecorder {
428
554
  startPosition,
429
555
  endPosition,
430
556
  samples,
431
- startCounterFrom: this.dataPointIdCounter, // Pass the current counter value
432
557
  });
433
558
  }
434
559
  }
560
+ /**
561
+ * Sets up detection for device disconnection events
562
+ */
563
+ setupDeviceDisconnectionDetection() {
564
+ if (!this.mediaStream)
565
+ return;
566
+ // Function to handle track ending (which happens on device disconnection)
567
+ const handleTrackEnded = () => {
568
+ this.logger?.warn('Audio track ended - device disconnected');
569
+ this._isDeviceDisconnected = true;
570
+ // Use the callback to notify parent component about device disconnection
571
+ if (this.onInterruptionCallback) {
572
+ this.onInterruptionCallback({
573
+ reason: 'deviceDisconnected',
574
+ isPaused: true,
575
+ timestamp: Date.now(),
576
+ });
577
+ this.logger?.debug('Notified about device disconnection');
578
+ }
579
+ // Ensure we disconnect nodes to prevent zombie recordings
580
+ if (this.audioWorkletNode) {
581
+ this.audioWorkletNode.port.postMessage({
582
+ command: 'deviceDisconnected',
583
+ });
584
+ try {
585
+ this.source.disconnect(this.audioWorkletNode);
586
+ this.audioWorkletNode.disconnect();
587
+ }
588
+ catch (e) {
589
+ // Ignore disconnection errors as the track might already be gone
590
+ }
591
+ }
592
+ };
593
+ // Add listeners to all audio tracks
594
+ const tracks = this.mediaStream.getAudioTracks();
595
+ tracks.forEach((track) => {
596
+ track.addEventListener('ended', handleTrackEnded);
597
+ });
598
+ // Store the handler for cleanup
599
+ this.deviceDisconnectionHandler = () => {
600
+ tracks.forEach((track) => {
601
+ track.removeEventListener('ended', handleTrackEnded);
602
+ });
603
+ };
604
+ }
605
+ /**
606
+ * Explicitly set the position for continuous recording across device switches
607
+ * @param position The position in seconds to continue from
608
+ */
609
+ setPosition(position) {
610
+ if (position >= 0) {
611
+ this.position = position;
612
+ this.logger?.debug(`Position explicitly set to ${position} seconds`);
613
+ }
614
+ else {
615
+ this.logger?.warn(`Invalid position value: ${position}, ignoring`);
616
+ }
617
+ }
618
+ /**
619
+ * Get the current position in seconds
620
+ * @returns The current position
621
+ */
622
+ getPosition() {
623
+ return this.position;
624
+ }
625
+ /**
626
+ * Gets the current compressed chunks
627
+ * @returns Array of current compressed audio chunks
628
+ */
629
+ getCompressedChunks() {
630
+ return [...this.compressedChunks];
631
+ }
632
+ /**
633
+ * Sets the compressed chunks from a previous recorder
634
+ * @param chunks Array of compressed chunks from a previous recorder
635
+ */
636
+ setCompressedChunks(chunks) {
637
+ if (chunks && chunks.length > 0) {
638
+ this.logger?.debug(`Adding ${chunks.length} compressed chunks from previous device`);
639
+ this.compressedChunks = [...chunks, ...this.compressedChunks];
640
+ // Update size
641
+ this.compressedSize = this.compressedChunks.reduce((size, chunk) => size + chunk.size, 0);
642
+ }
643
+ }
435
644
  }
436
645
  //# sourceMappingURL=WebRecorder.web.js.map