@siteed/expo-audio-studio 2.4.1 → 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.
- package/CHANGELOG.md +10 -1
- package/README.md +25 -0
- package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +22 -0
- package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +1501 -0
- package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +10 -5
- package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +27 -25
- package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +73 -71
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +576 -252
- package/android/src/main/java/net/siteed/audiostream/Constants.kt +17 -1
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +419 -155
- package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +65 -0
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +9 -1
- package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/AudioDeviceManager.d.ts +107 -0
- package/build/AudioDeviceManager.d.ts.map +1 -0
- package/build/AudioDeviceManager.js +493 -0
- package/build/AudioDeviceManager.js.map +1 -0
- package/build/AudioRecorder.provider.d.ts.map +1 -1
- package/build/AudioRecorder.provider.js +3 -0
- package/build/AudioRecorder.provider.js.map +1 -1
- package/build/ExpoAudioStream.types.d.ts +90 -1
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js +7 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStream.web.d.ts +37 -0
- package/build/ExpoAudioStream.web.d.ts.map +1 -1
- package/build/ExpoAudioStream.web.js +399 -54
- package/build/ExpoAudioStream.web.js.map +1 -1
- package/build/ExpoAudioStreamModule.d.ts.map +1 -1
- package/build/ExpoAudioStreamModule.js +20 -0
- package/build/ExpoAudioStreamModule.js.map +1 -1
- package/build/WebRecorder.web.d.ts +63 -10
- package/build/WebRecorder.web.d.ts.map +1 -1
- package/build/WebRecorder.web.js +277 -68
- package/build/WebRecorder.web.js.map +1 -1
- package/build/hooks/useAudioDevices.d.ts +14 -0
- package/build/hooks/useAudioDevices.d.ts.map +1 -0
- package/build/hooks/useAudioDevices.js +151 -0
- package/build/hooks/useAudioDevices.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -0
- package/build/index.js.map +1 -1
- package/build/useAudioRecorder.d.ts +1 -0
- package/build/useAudioRecorder.d.ts.map +1 -1
- package/build/useAudioRecorder.js +20 -1
- package/build/useAudioRecorder.js.map +1 -1
- package/build/utils/BlobFix.d.ts.map +1 -1
- package/build/utils/BlobFix.js +2 -2
- package/build/utils/BlobFix.js.map +1 -1
- package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
- package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
- package/build/workers/InlineFeaturesExtractor.web.js +27 -26
- package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
- package/build/workers/inlineAudioWebWorker.web.js +25 -1
- package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
- package/ios/AudioDeviceManager.swift +654 -0
- package/ios/AudioStreamManager.swift +964 -760
- package/ios/ExpoAudioStreamModule.swift +174 -19
- package/ios/Features.swift +1 -1
- package/ios/ISSUE_IOS.md +45 -0
- package/ios/Logger.swift +13 -1
- package/ios/RecordingSettings.swift +12 -0
- package/package.json +2 -2
- package/src/AudioAnalysis/AudioAnalysis.types.ts +2 -2
- package/src/AudioDeviceManager.ts +571 -0
- package/src/AudioRecorder.provider.tsx +3 -0
- package/src/ExpoAudioStream.types.ts +97 -1
- package/src/ExpoAudioStream.web.ts +513 -63
- package/src/ExpoAudioStreamModule.ts +23 -0
- package/src/WebRecorder.web.ts +346 -81
- package/src/hooks/useAudioDevices.ts +180 -0
- package/src/index.ts +6 -0
- package/src/types/crc-32.d.ts +6 -6
- package/src/useAudioRecorder.tsx +27 -1
- package/src/utils/BlobFix.ts +6 -4
- package/src/workers/InlineFeaturesExtractor.web.tsx +27 -26
- 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
|
-
|
|
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
|
-
*
|
|
66
|
-
*
|
|
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
|
-
|
|
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;
|
|
1
|
+
{"version":3,"file":"WebRecorder.web.d.ts","sourceRoot":"","sources":["../src/WebRecorder.web.ts"],"names":[],"mappings":"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"}
|
package/build/WebRecorder.web.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
*
|
|
271
|
-
*
|
|
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
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|