@siteed/audio-studio 3.2.1-beta.0 → 3.2.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -1
- package/README.md +41 -1
- package/android/src/main/java/net/siteed/audiostudio/AudioRecorderManager.kt +130 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +1 -0
- package/android/src/main/java/net/siteed/audiostudio/Constants.kt +2 -1
- package/android/src/main/java/net/siteed/audiostudio/RecordingConfig.kt +5 -1
- package/build/cjs/AudioStudio.types.js.map +1 -1
- package/build/cjs/AudioStudio.web.js +125 -13
- package/build/cjs/AudioStudio.web.js.map +1 -1
- package/build/cjs/AudioStudioModule.js +6 -1
- package/build/cjs/AudioStudioModule.js.map +1 -1
- package/build/cjs/events.js +4 -0
- package/build/cjs/events.js.map +1 -1
- package/build/cjs/index.js +3 -1
- package/build/cjs/index.js.map +1 -1
- package/build/cjs/useAudioRecorder.js +187 -30
- package/build/cjs/useAudioRecorder.js.map +1 -1
- package/build/cjs/utils/nativeRecordingOptions.js +13 -0
- package/build/cjs/utils/nativeRecordingOptions.js.map +1 -0
- package/build/cjs/utils/nativeRecordingOptions.test.js +30 -0
- package/build/cjs/utils/nativeRecordingOptions.test.js.map +1 -0
- package/build/esm/AudioStudio.types.js.map +1 -1
- package/build/esm/AudioStudio.web.js +125 -13
- package/build/esm/AudioStudio.web.js.map +1 -1
- package/build/esm/AudioStudioModule.js +6 -1
- package/build/esm/AudioStudioModule.js.map +1 -1
- package/build/esm/events.js +3 -0
- package/build/esm/events.js.map +1 -1
- package/build/esm/index.js +1 -0
- package/build/esm/index.js.map +1 -1
- package/build/esm/useAudioRecorder.js +188 -31
- package/build/esm/useAudioRecorder.js.map +1 -1
- package/build/esm/utils/nativeRecordingOptions.js +10 -0
- package/build/esm/utils/nativeRecordingOptions.js.map +1 -0
- package/build/esm/utils/nativeRecordingOptions.test.js +28 -0
- package/build/esm/utils/nativeRecordingOptions.test.js.map +1 -0
- package/build/types/AudioStudio.types.d.ts +58 -1
- package/build/types/AudioStudio.types.d.ts.map +1 -1
- package/build/types/AudioStudio.web.d.ts +17 -1
- package/build/types/AudioStudio.web.d.ts.map +1 -1
- package/build/types/AudioStudioModule.d.ts.map +1 -1
- package/build/types/events.d.ts +2 -1
- package/build/types/events.d.ts.map +1 -1
- package/build/types/index.d.ts +1 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/useAudioRecorder.d.ts +4 -1
- package/build/types/useAudioRecorder.d.ts.map +1 -1
- package/build/types/utils/nativeRecordingOptions.d.ts +28 -0
- package/build/types/utils/nativeRecordingOptions.d.ts.map +1 -0
- package/build/types/utils/nativeRecordingOptions.test.d.ts +2 -0
- package/build/types/utils/nativeRecordingOptions.test.d.ts.map +1 -0
- package/ios/AudioStreamManager.swift +103 -9
- package/ios/AudioStreamManagerDelegate.swift +1 -0
- package/ios/AudioStudioModule.swift +6 -0
- package/ios/RecordingSettings.swift +48 -43
- package/package.json +1 -1
- package/src/AudioStudio.types.ts +70 -1
- package/src/AudioStudio.web.ts +152 -13
- package/src/AudioStudioModule.ts +6 -1
- package/src/events.ts +13 -1
- package/src/index.ts +1 -0
- package/src/useAudioRecorder.tsx +260 -45
- package/src/utils/nativeRecordingOptions.test.ts +29 -0
- package/src/utils/nativeRecordingOptions.ts +20 -0
package/build/esm/events.js
CHANGED
package/build/esm/events.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/events.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC,OAAO,EAAE,kBAAkB,EAA0B,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/events.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC,OAAO,EAAE,kBAAkB,EAA0B,MAAM,mBAAmB,CAAA;AAO9E,OAAO,iBAAiB,MAAM,qBAAqB,CAAA;AAEnD,MAAM,OAAO,GAAG,IAAI,kBAAkB,CAAC,iBAAiB,CAAC,CAAA;AAwBzD,MAAM,UAAU,qBAAqB,CACjC,QAAqD;IAErD,OAAO,OAAO,CAAC,WAAW,CAAoB,WAAW,EAAE,QAAQ,CAAC,CAAA;AACxE,CAAC;AAKD,MAAM,UAAU,wBAAwB,CACpC,QAAsD;IAEtD,OAAO,OAAO,CAAC,WAAW,CAAqB,eAAe,EAAE,QAAQ,CAAC,CAAA;AAC7E,CAAC;AAED,MAAM,UAAU,gCAAgC,CAC5C,QAAqD;IAErD,oBAAoB;IACpB,OAAO,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAA;IAEvD,MAAM,YAAY,GAAG,OAAO,CAAC,WAAW,CACpC,wBAAwB,EAAE,+CAA+C;IACzE,CAAC,KAAK,EAAE,EAAE;QACN,OAAO,CAAC,KAAK,CAAC,wCAAwC,EAAE,KAAK,CAAC,CAAA;QAC9D,QAAQ,CAAC,KAAK,CAAC,CAAA;IACnB,CAAC,CACJ,CAAA;IAED,OAAO,YAAY,CAAA;AACvB,CAAC;AAED,MAAM,UAAU,6BAA6B,CACzC,QAAkD;IAElD,OAAO,OAAO,CAAC,WAAW,CACtB,oBAAoB,EACpB,QAAQ,CACX,CAAA;AACL,CAAC","sourcesContent":["// packages/audio-studio/src/events.ts\n\nimport { LegacyEventEmitter, type EventSubscription } from 'expo-modules-core'\n\nimport { AudioAnalysis } from './AudioAnalysis/AudioAnalysis.types'\nimport type {\n MaxDurationReachedEvent,\n RecordingInterruptionEvent,\n} from './AudioStudio.types'\nimport AudioStudioModule from './AudioStudioModule'\n\nconst emitter = new LegacyEventEmitter(AudioStudioModule)\n\n// Internal event payload from native module\nexport interface AudioEventPayload {\n encoded?: string\n /** Float32 samples in [-1, 1] — sent by native when streamFormat='float32'.\n * Android new arch delivers Float32Array; iOS delivers number[]. */\n pcmFloat32?: Float32Array | number[]\n buffer?: Float32Array\n fileUri: string\n lastEmittedSize: number\n position: number\n deltaSize: number\n totalSize: number\n mimeType: string\n streamUuid: string\n compression?: {\n data?: string | Blob // Base64 (native) or Float32Array (web) encoded compressed data chunk\n position: number\n eventDataSize: number\n totalSize: number\n }\n}\n\nexport function addAudioEventListener(\n listener: (event: AudioEventPayload) => Promise<void>\n): EventSubscription {\n return emitter.addListener<AudioEventPayload>('AudioData', listener)\n}\n\n// Only aliasing the AudioAnalysis type for the event payload\nexport interface AudioAnalysisEvent extends AudioAnalysis {}\n\nexport function addAudioAnalysisListener(\n listener: (event: AudioAnalysisEvent) => Promise<void>\n): EventSubscription {\n return emitter.addListener<AudioAnalysisEvent>('AudioAnalysis', listener)\n}\n\nexport function addRecordingInterruptionListener(\n listener: (event: RecordingInterruptionEvent) => void\n): EventSubscription {\n // Add debug logging\n console.debug('Adding recording interruption listener')\n\n const subscription = emitter.addListener<RecordingInterruptionEvent>(\n 'onRecordingInterrupted', // Make sure this matches the native event name\n (event) => {\n console.debug('Recording interruption event received:', event)\n listener(event)\n }\n )\n\n return subscription\n}\n\nexport function addMaxDurationReachedListener(\n listener: (event: MaxDurationReachedEvent) => void\n): EventSubscription {\n return emitter.addListener<MaxDurationReachedEvent>(\n 'MaxDurationReached',\n listener\n )\n}\n"]}
|
package/build/esm/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import AudioStudioModule from './AudioStudioModule';
|
|
|
9
9
|
import { getAudioDecodeCapabilities, streamAudioData, } from './streamAudioData';
|
|
10
10
|
import { trimAudio } from './trimAudio';
|
|
11
11
|
import { useAudioRecorder } from './useAudioRecorder';
|
|
12
|
+
export { addMaxDurationReachedListener } from './events';
|
|
12
13
|
export * from './utils/convertPCMToFloat32';
|
|
13
14
|
export * from './utils/getWavFileInfo';
|
|
14
15
|
export * from './utils/writeWavHeader';
|
package/build/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,eAAe;AAEf,OAAO,EACH,qBAAqB,EACrB,oBAAoB,GACvB,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,EACH,qBAAqB,EACrB,eAAe,GAClB,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AAC/D,OAAO,EACH,oBAAoB,EACpB,mBAAmB,GACtB,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EACH,qBAAqB,EACrB,sBAAsB,GACzB,MAAM,0BAA0B,CAAA;AACjC,OAAO,iBAAiB,MAAM,qBAAqB,CAAA;AACnD,OAAO,EACH,0BAA0B,EAC1B,eAAe,GAClB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,eAAe;AAEf,OAAO,EACH,qBAAqB,EACrB,oBAAoB,GACvB,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,EACH,qBAAqB,EACrB,eAAe,GAClB,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AAC/D,OAAO,EACH,oBAAoB,EACpB,mBAAmB,GACtB,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EACH,qBAAqB,EACrB,sBAAsB,GACzB,MAAM,0BAA0B,CAAA;AACjC,OAAO,iBAAiB,MAAM,qBAAqB,CAAA;AACnD,OAAO,EACH,0BAA0B,EAC1B,eAAe,GAClB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,6BAA6B,EAAE,MAAM,UAAU,CAAA;AAExD,cAAc,6BAA6B,CAAA;AAC3C,cAAc,wBAAwB,CAAA;AACtC,cAAc,wBAAwB,CAAA;AAEtC,+BAA+B;AAC/B,OAAO,EACH,uBAAuB,EACvB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,uBAAuB,GAE1B,MAAM,iCAAiC,CAAA;AAExC,4BAA4B;AAC5B,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAE7E,8BAA8B;AAC9B,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAEzD,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAA;AAEvE,OAAO,EACH,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,EACpB,cAAc,EACd,SAAS,EACT,gBAAgB,EAChB,eAAe,EACf,0BAA0B,EAC1B,qBAAqB,EACrB,oBAAoB,EACpB,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,sBAAsB,GACzB,CAAA;AAED,OAAO,EACH,oBAAoB,EACpB,kBAAkB,GACrB,MAAM,+BAA+B,CAAA;AAMtC,OAAO,EACH,gBAAgB,EAChB,cAAc,GACjB,MAAM,2BAA2B,CAAA;AAmBlC,gDAAgD;AAChD,MAAM,CAAC,MAAM,qBAAqB,GAAG,iBAAiB,CAAA","sourcesContent":["// src/index.ts\n\nimport {\n extractRawWavAnalysis,\n extractAudioAnalysis,\n} from './AudioAnalysis/extractAudioAnalysis'\nimport { extractAudioData } from './AudioAnalysis/extractAudioData'\nimport {\n extractMelSpectrogram,\n MAX_DURATION_MS,\n} from './AudioAnalysis/extractMelSpectrogram'\nimport { extractPreview } from './AudioAnalysis/extractPreview'\nimport {\n initMelStreamingWasm,\n computeMelFrameWasm,\n} from './AudioAnalysis/melSpectrogramWasm'\nimport {\n AudioRecorderProvider,\n useSharedAudioRecorder,\n} from './AudioRecorder.provider'\nimport AudioStudioModule from './AudioStudioModule'\nimport {\n getAudioDecodeCapabilities,\n streamAudioData,\n} from './streamAudioData'\nimport { trimAudio } from './trimAudio'\nimport { useAudioRecorder } from './useAudioRecorder'\nexport { addMaxDurationReachedListener } from './events'\n\nexport * from './utils/convertPCMToFloat32'\nexport * from './utils/getWavFileInfo'\nexport * from './utils/writeWavHeader'\n\n// Export platform capabilities\nexport {\n getPlatformCapabilities,\n isEncodingSupported,\n isBitDepthSupported,\n getFallbackEncoding,\n getFallbackBitDepth,\n validateRecordingConfig,\n type PlatformCapabilities,\n} from './constants/platformLimitations'\n\n// Export AudioDeviceManager\nexport { AudioDeviceManager, audioDeviceManager } from './AudioDeviceManager'\n\n// Export useAudioDevices hook\nexport { useAudioDevices } from './hooks/useAudioDevices'\n\nexport { setMelSpectrogramWasmUrl } from './AudioAnalysis/wasmConfig'\nexport { extractPreviewBars } from './AudioAnalysis/extractPreviewBars'\n\nexport {\n AudioRecorderProvider,\n AudioStudioModule,\n extractRawWavAnalysis,\n extractAudioAnalysis,\n extractPreview,\n trimAudio,\n extractAudioData,\n streamAudioData,\n getAudioDecodeCapabilities,\n extractMelSpectrogram,\n initMelStreamingWasm,\n computeMelFrameWasm,\n MAX_DURATION_MS,\n useAudioRecorder,\n useSharedAudioRecorder,\n}\n\nexport {\n AudioExtractionError,\n mapExtractionError,\n} from './errors/AudioExtractionError'\nexport type {\n AudioExtractionErrorCode,\n AudioExtractionErrorPayload,\n} from './errors/AudioExtractionError'\n\nexport {\n AudioStreamError,\n mapStreamError,\n} from './errors/AudioStreamError'\nexport type {\n AudioStreamErrorCode,\n AudioStreamErrorPayload,\n} from './errors/AudioStreamError'\n\nexport type {\n StreamAudioDataOptions,\n StreamAudioDataChunk,\n StreamAudioDataProgress,\n StreamAudioDataResult,\n StreamAudioDataCallbacks,\n AudioDecodeCapabilities,\n} from './streamAudioData'\n\n// Export all types\nexport type * from './AudioAnalysis/AudioAnalysis.types'\nexport type * from './AudioStudio.types'\n\n/** @deprecated Use AudioStudioModule instead */\nexport const ExpoAudioStreamModule = AudioStudioModule\n"]}
|
|
@@ -4,8 +4,8 @@ import { useCallback, useEffect, useReducer, useRef, useId } from 'react';
|
|
|
4
4
|
import { audioDeviceManager } from './AudioDeviceManager';
|
|
5
5
|
import AudioStudioModule from './AudioStudioModule';
|
|
6
6
|
import { validateRecordingConfig } from './constants/platformLimitations';
|
|
7
|
-
import { addAudioAnalysisListener, addAudioEventListener, addRecordingInterruptionListener, } from './events';
|
|
8
|
-
import {
|
|
7
|
+
import { addAudioAnalysisListener, addAudioEventListener, addMaxDurationReachedListener, addRecordingInterruptionListener, } from './events';
|
|
8
|
+
import { createNativeRecordingOptions } from './utils/nativeRecordingOptions';
|
|
9
9
|
const defaultAnalysis = {
|
|
10
10
|
segmentDurationMs: 100,
|
|
11
11
|
bitDepth: 32,
|
|
@@ -24,6 +24,31 @@ const defaultAnalysis = {
|
|
|
24
24
|
},
|
|
25
25
|
extractionTimeMs: 0,
|
|
26
26
|
};
|
|
27
|
+
function finiteOrZero(value) {
|
|
28
|
+
return Number.isFinite(value) ? value : 0;
|
|
29
|
+
}
|
|
30
|
+
function sanitizeSerializableValue(value) {
|
|
31
|
+
if (typeof value === 'number') {
|
|
32
|
+
return finiteOrZero(value);
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
return value.map((item) => sanitizeSerializableValue(item));
|
|
36
|
+
}
|
|
37
|
+
if (value && typeof value === 'object') {
|
|
38
|
+
const sanitized = {};
|
|
39
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
40
|
+
sanitized[key] = sanitizeSerializableValue(nestedValue);
|
|
41
|
+
}
|
|
42
|
+
return sanitized;
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function createSerializableAnalysis(analysis) {
|
|
47
|
+
return sanitizeSerializableValue(analysis);
|
|
48
|
+
}
|
|
49
|
+
function createRecordingSnapshot(recording) {
|
|
50
|
+
return sanitizeSerializableValue(recording);
|
|
51
|
+
}
|
|
27
52
|
function audioRecorderReducer(state, action) {
|
|
28
53
|
switch (action.type) {
|
|
29
54
|
case 'START':
|
|
@@ -35,6 +60,9 @@ function audioRecorderReducer(state, action) {
|
|
|
35
60
|
size: 0,
|
|
36
61
|
compression: undefined,
|
|
37
62
|
analysisData: defaultAnalysis,
|
|
63
|
+
maxDurationMs: undefined,
|
|
64
|
+
maxDurationReached: false,
|
|
65
|
+
lastRecordingReason: undefined,
|
|
38
66
|
};
|
|
39
67
|
case 'STOP':
|
|
40
68
|
return {
|
|
@@ -45,6 +73,9 @@ function audioRecorderReducer(state, action) {
|
|
|
45
73
|
size: 0,
|
|
46
74
|
compression: undefined,
|
|
47
75
|
analysisData: undefined,
|
|
76
|
+
lastRecordingReason: action.payload.reason,
|
|
77
|
+
// Preserve max-duration state after stop so UI and agentic
|
|
78
|
+
// validation can explain why recording ended. START resets it.
|
|
48
79
|
};
|
|
49
80
|
case 'PAUSE':
|
|
50
81
|
return { ...state, isPaused: true, isRecording: false };
|
|
@@ -69,9 +100,17 @@ function audioRecorderReducer(state, action) {
|
|
|
69
100
|
format: action.payload.compression.format,
|
|
70
101
|
}
|
|
71
102
|
: undefined,
|
|
103
|
+
maxDurationMs: action.payload.maxDurationMs,
|
|
104
|
+
maxDurationReached: action.payload.maxDurationReached,
|
|
72
105
|
};
|
|
73
106
|
return newState;
|
|
74
107
|
}
|
|
108
|
+
case 'MAX_DURATION_REACHED':
|
|
109
|
+
return {
|
|
110
|
+
...state,
|
|
111
|
+
maxDurationMs: action.payload.maxDurationMs,
|
|
112
|
+
maxDurationReached: true,
|
|
113
|
+
};
|
|
75
114
|
case 'UPDATE_ANALYSIS':
|
|
76
115
|
return {
|
|
77
116
|
...state,
|
|
@@ -96,6 +135,9 @@ export function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl,
|
|
|
96
135
|
size: 0,
|
|
97
136
|
compression: undefined,
|
|
98
137
|
analysisData: undefined,
|
|
138
|
+
maxDurationMs: undefined,
|
|
139
|
+
maxDurationReached: false,
|
|
140
|
+
lastRecordingReason: undefined,
|
|
99
141
|
});
|
|
100
142
|
const startResultRef = useRef(null);
|
|
101
143
|
const analysisListenerRef = useRef(null);
|
|
@@ -120,8 +162,12 @@ export function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl,
|
|
|
120
162
|
durationMs: 0,
|
|
121
163
|
size: 0,
|
|
122
164
|
compression: undefined,
|
|
165
|
+
maxDurationMs: undefined,
|
|
166
|
+
maxDurationReached: false,
|
|
123
167
|
});
|
|
124
168
|
const recordingConfigRef = useRef(null);
|
|
169
|
+
const maxDurationHandledRef = useRef(false);
|
|
170
|
+
const stopFinalizationRef = useRef(null);
|
|
125
171
|
// Generate unique instance ID for debugging
|
|
126
172
|
const instanceId = useId().replace(/:/g, '').slice(0, 5);
|
|
127
173
|
const handleAudioAnalysis = useCallback(async ({ analysis, visualizationDuration, }) => {
|
|
@@ -287,10 +333,105 @@ export function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl,
|
|
|
287
333
|
logger?.error(`Error processing audio event:`, error);
|
|
288
334
|
}
|
|
289
335
|
}, []);
|
|
336
|
+
const finalizeRecordingStop = useCallback(async (reason) => {
|
|
337
|
+
if (stopFinalizationRef.current) {
|
|
338
|
+
return stopFinalizationRef.current;
|
|
339
|
+
}
|
|
340
|
+
const finalizePromise = (async () => {
|
|
341
|
+
const nativeStopResult = await audioStudio.stopRecording();
|
|
342
|
+
if (!nativeStopResult) {
|
|
343
|
+
throw new Error('Failed to stop recording');
|
|
344
|
+
}
|
|
345
|
+
const stopResult = createRecordingSnapshot(nativeStopResult);
|
|
346
|
+
if (shouldKeepFullAnalysis(recordingConfigRef.current)) {
|
|
347
|
+
stopResult.analysisData = createSerializableAnalysis(fullAnalysisRef.current);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
// `keepFullAnalysis` is a hook-level retention policy. If a platform
|
|
351
|
+
// starts returning native analysisData in the future, keep opt-out
|
|
352
|
+
// semantics explicit and avoid leaking a full history here.
|
|
353
|
+
delete stopResult.analysisData;
|
|
354
|
+
}
|
|
355
|
+
if (analysisListenerRef.current) {
|
|
356
|
+
analysisListenerRef.current.remove();
|
|
357
|
+
analysisListenerRef.current = null;
|
|
358
|
+
}
|
|
359
|
+
onAudioStreamRef.current = null;
|
|
360
|
+
stateRef.current.isRecording = false;
|
|
361
|
+
stateRef.current.isPaused = false;
|
|
362
|
+
// Note: We deliberately DON'T clear recordingConfigRef here to preserve callbacks.
|
|
363
|
+
logger?.debug(`recording stopped`, stopResult);
|
|
364
|
+
maxDurationHandledRef.current = false;
|
|
365
|
+
dispatch({
|
|
366
|
+
type: 'STOP',
|
|
367
|
+
payload: { reason },
|
|
368
|
+
});
|
|
369
|
+
const stoppedCallback = recordingConfigRef.current?.onRecordingStopped;
|
|
370
|
+
if (stoppedCallback) {
|
|
371
|
+
try {
|
|
372
|
+
void Promise.resolve(stoppedCallback(stopResult, reason)).catch((error) => {
|
|
373
|
+
logger?.error(`Error in recording stopped callback:`, error);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
logger?.error(`Error in recording stopped callback:`, error);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return stopResult;
|
|
381
|
+
})();
|
|
382
|
+
stopFinalizationRef.current = finalizePromise;
|
|
383
|
+
try {
|
|
384
|
+
return await finalizePromise;
|
|
385
|
+
}
|
|
386
|
+
finally {
|
|
387
|
+
stopFinalizationRef.current = null;
|
|
388
|
+
}
|
|
389
|
+
}, [audioStudio, dispatch, logger]);
|
|
390
|
+
const handleMaxDurationReached = useCallback(async (event) => {
|
|
391
|
+
if (maxDurationHandledRef.current) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
maxDurationHandledRef.current = true;
|
|
395
|
+
const config = recordingConfigRef.current;
|
|
396
|
+
const callbackEvent = {
|
|
397
|
+
...event,
|
|
398
|
+
autoStopped: event.autoStopped || !!config?.autoStopOnMaxDuration,
|
|
399
|
+
};
|
|
400
|
+
stateRef.current.maxDurationMs = callbackEvent.maxDurationMs;
|
|
401
|
+
stateRef.current.maxDurationReached = true;
|
|
402
|
+
dispatch({
|
|
403
|
+
type: 'MAX_DURATION_REACHED',
|
|
404
|
+
payload: callbackEvent,
|
|
405
|
+
});
|
|
406
|
+
try {
|
|
407
|
+
config?.onMaxDurationReached?.(callbackEvent);
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
logger?.error(`Error in max duration callback:`, error);
|
|
411
|
+
}
|
|
412
|
+
if (config?.autoStopOnMaxDuration && stateRef.current.isRecording) {
|
|
413
|
+
try {
|
|
414
|
+
await finalizeRecordingStop('maxDuration');
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
logger?.error(`Error auto-stopping on max duration:`, error);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}, [dispatch, finalizeRecordingStop, logger]);
|
|
290
421
|
const checkStatus = useCallback(async () => {
|
|
291
422
|
try {
|
|
292
423
|
const status = audioStudio.status();
|
|
293
424
|
logger?.debug(`Status: paused: ${status.isPaused} isRecording: ${status.isRecording} durationMs: ${status.durationMs} size: ${status.size}`, status.compression);
|
|
425
|
+
if (status.maxDurationReached === true &&
|
|
426
|
+
status.maxDurationMs != null &&
|
|
427
|
+
!stateRef.current.maxDurationReached) {
|
|
428
|
+
await handleMaxDurationReached({
|
|
429
|
+
durationMs: status.durationMs,
|
|
430
|
+
maxDurationMs: status.maxDurationMs,
|
|
431
|
+
overrunMs: Math.max(0, status.durationMs - status.maxDurationMs),
|
|
432
|
+
autoStopped: false,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
294
435
|
// Only dispatch if values actually changed
|
|
295
436
|
if (status.isRecording !== stateRef.current.isRecording ||
|
|
296
437
|
status.isPaused !== stateRef.current.isPaused) {
|
|
@@ -304,17 +445,34 @@ export function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl,
|
|
|
304
445
|
},
|
|
305
446
|
});
|
|
306
447
|
}
|
|
448
|
+
const statusMaxDurationReached = status.maxDurationReached ?? false;
|
|
449
|
+
const preserveStoppedMaxDuration = !status.isRecording &&
|
|
450
|
+
!status.isPaused &&
|
|
451
|
+
stateRef.current.maxDurationReached &&
|
|
452
|
+
!statusMaxDurationReached;
|
|
453
|
+
const nextMaxDurationMs = preserveStoppedMaxDuration
|
|
454
|
+
? stateRef.current.maxDurationMs
|
|
455
|
+
: status.maxDurationMs;
|
|
456
|
+
const nextMaxDurationReached = preserveStoppedMaxDuration
|
|
457
|
+
? true
|
|
458
|
+
: statusMaxDurationReached;
|
|
307
459
|
if (status.durationMs !== stateRef.current.durationMs ||
|
|
308
|
-
status.size !== stateRef.current.size
|
|
460
|
+
status.size !== stateRef.current.size ||
|
|
461
|
+
nextMaxDurationMs !== stateRef.current.maxDurationMs ||
|
|
462
|
+
nextMaxDurationReached !== stateRef.current.maxDurationReached) {
|
|
309
463
|
stateRef.current.durationMs = status.durationMs;
|
|
310
464
|
stateRef.current.size = status.size;
|
|
311
465
|
stateRef.current.compression = status.compression;
|
|
466
|
+
stateRef.current.maxDurationMs = nextMaxDurationMs;
|
|
467
|
+
stateRef.current.maxDurationReached = nextMaxDurationReached;
|
|
312
468
|
dispatch({
|
|
313
469
|
type: 'UPDATE_STATUS',
|
|
314
470
|
payload: {
|
|
315
471
|
durationMs: status.durationMs,
|
|
316
472
|
size: status.size,
|
|
317
473
|
compression: status.compression,
|
|
474
|
+
maxDurationMs: nextMaxDurationMs,
|
|
475
|
+
maxDurationReached: nextMaxDurationReached,
|
|
318
476
|
},
|
|
319
477
|
});
|
|
320
478
|
}
|
|
@@ -322,7 +480,7 @@ export function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl,
|
|
|
322
480
|
catch (error) {
|
|
323
481
|
logger?.error(`Error getting status:`, error);
|
|
324
482
|
}
|
|
325
|
-
}, [audioStudio, logger]);
|
|
483
|
+
}, [audioStudio, handleMaxDurationReached, logger]);
|
|
326
484
|
// Update ref when state changes
|
|
327
485
|
useEffect(() => {
|
|
328
486
|
stateRef.current = {
|
|
@@ -331,6 +489,8 @@ export function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl,
|
|
|
331
489
|
durationMs: state.durationMs,
|
|
332
490
|
size: state.size,
|
|
333
491
|
compression: state.compression,
|
|
492
|
+
maxDurationMs: state.maxDurationMs,
|
|
493
|
+
maxDurationReached: state.maxDurationReached ?? false,
|
|
334
494
|
};
|
|
335
495
|
}, [
|
|
336
496
|
state.isRecording,
|
|
@@ -338,6 +498,8 @@ export function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl,
|
|
|
338
498
|
state.durationMs,
|
|
339
499
|
state.size,
|
|
340
500
|
state.compression,
|
|
501
|
+
state.maxDurationMs,
|
|
502
|
+
state.maxDurationReached,
|
|
341
503
|
]);
|
|
342
504
|
const startRecording = useCallback(async (recordingOptions) => {
|
|
343
505
|
// Validate the encoding configuration
|
|
@@ -356,11 +518,11 @@ export function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl,
|
|
|
356
518
|
encoding: validationResult.encoding,
|
|
357
519
|
};
|
|
358
520
|
recordingConfigRef.current = validatedOptions;
|
|
521
|
+
maxDurationHandledRef.current = false;
|
|
359
522
|
logger?.debug(`start recording with validated config`, validatedOptions);
|
|
360
523
|
analysisRef.current = { ...defaultAnalysis }; // Reset analysis data
|
|
361
524
|
fullAnalysisRef.current = { ...defaultAnalysis };
|
|
362
|
-
const { onAudioStream,
|
|
363
|
-
const { enableProcessing } = options;
|
|
525
|
+
const { onAudioStream, enableProcessing } = validatedOptions;
|
|
364
526
|
const maxRecentDataDuration = 10000; // TODO compute maxRecentDataDuration based on screen dimensions
|
|
365
527
|
if (typeof onAudioStream === 'function') {
|
|
366
528
|
onAudioStreamRef.current = onAudioStream;
|
|
@@ -369,8 +531,10 @@ export function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl,
|
|
|
369
531
|
logger?.warn(`onAudioStream is not a function`, onAudioStream);
|
|
370
532
|
onAudioStreamRef.current = null;
|
|
371
533
|
}
|
|
372
|
-
// Strip
|
|
373
|
-
|
|
534
|
+
// Strip hook-only values and undefineds that can't cross the native bridge.
|
|
535
|
+
// autoStopOnMaxDuration stays hook-owned so finalization can expose
|
|
536
|
+
// the same AudioRecording result as a manual stop.
|
|
537
|
+
const cleanOptions = createNativeRecordingOptions(validatedOptions);
|
|
374
538
|
const startResult = await audioStudio.startRecording(cleanOptions);
|
|
375
539
|
dispatch({ type: 'START' });
|
|
376
540
|
startResultRef.current = startResult;
|
|
@@ -396,7 +560,7 @@ export function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl,
|
|
|
396
560
|
logger?.debug(`preparing recording`, recordingOptions);
|
|
397
561
|
analysisRef.current = { ...defaultAnalysis }; // Reset analysis data
|
|
398
562
|
fullAnalysisRef.current = { ...defaultAnalysis };
|
|
399
|
-
const { onAudioStream
|
|
563
|
+
const { onAudioStream } = recordingOptions;
|
|
400
564
|
// Store onAudioStream for later use when recording starts
|
|
401
565
|
if (typeof onAudioStream === 'function') {
|
|
402
566
|
onAudioStreamRef.current = onAudioStream;
|
|
@@ -405,34 +569,16 @@ export function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl,
|
|
|
405
569
|
logger?.warn(`onAudioStream is not a function`, onAudioStream);
|
|
406
570
|
onAudioStreamRef.current = null;
|
|
407
571
|
}
|
|
408
|
-
// Strip
|
|
409
|
-
const cleanOptions =
|
|
572
|
+
// Strip hook-only values and undefineds that can't cross the native bridge.
|
|
573
|
+
const cleanOptions = createNativeRecordingOptions(recordingOptions);
|
|
410
574
|
// Call the native prepareRecording method
|
|
411
575
|
await audioStudio.prepareRecording(cleanOptions);
|
|
412
576
|
logger?.debug(`recording prepared successfully`);
|
|
413
577
|
}, []);
|
|
414
578
|
const stopRecording = useCallback(async () => {
|
|
415
579
|
logger?.debug(`stoping recording`);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
stopResult.analysisData = fullAnalysisRef.current;
|
|
419
|
-
}
|
|
420
|
-
else {
|
|
421
|
-
// `keepFullAnalysis` is a hook-level retention policy. If a platform
|
|
422
|
-
// starts returning native analysisData in the future, keep opt-out
|
|
423
|
-
// semantics explicit and avoid leaking a full history here.
|
|
424
|
-
delete stopResult.analysisData;
|
|
425
|
-
}
|
|
426
|
-
if (analysisListenerRef.current) {
|
|
427
|
-
analysisListenerRef.current.remove();
|
|
428
|
-
analysisListenerRef.current = null;
|
|
429
|
-
}
|
|
430
|
-
onAudioStreamRef.current = null;
|
|
431
|
-
// Note: We deliberately DON'T clear recordingConfigRef here to preserve interruption callback
|
|
432
|
-
logger?.debug(`recording stopped`, stopResult);
|
|
433
|
-
dispatch({ type: 'STOP' });
|
|
434
|
-
return stopResult;
|
|
435
|
-
}, [dispatch]);
|
|
580
|
+
return finalizeRecordingStop('manual');
|
|
581
|
+
}, [finalizeRecordingStop, logger]);
|
|
436
582
|
const pauseRecording = useCallback(async () => {
|
|
437
583
|
logger?.debug(`pause recording`);
|
|
438
584
|
const pauseResult = await audioStudio.pauseRecording();
|
|
@@ -445,6 +591,14 @@ export function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl,
|
|
|
445
591
|
dispatch({ type: 'RESUME' });
|
|
446
592
|
return resumeResult;
|
|
447
593
|
}, [dispatch]);
|
|
594
|
+
useEffect(() => {
|
|
595
|
+
const subscription = addMaxDurationReachedListener(async (event) => {
|
|
596
|
+
await handleMaxDurationReached(event);
|
|
597
|
+
});
|
|
598
|
+
return () => {
|
|
599
|
+
subscription.remove();
|
|
600
|
+
};
|
|
601
|
+
}, [handleMaxDurationReached]);
|
|
448
602
|
useEffect(() => {
|
|
449
603
|
let intervalId;
|
|
450
604
|
if (state.isRecording || state.isPaused) {
|
|
@@ -542,6 +696,9 @@ export function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl,
|
|
|
542
696
|
size: state.size,
|
|
543
697
|
compression: state.compression,
|
|
544
698
|
analysisData: state.analysisData,
|
|
699
|
+
maxDurationMs: state.maxDurationMs,
|
|
700
|
+
maxDurationReached: state.maxDurationReached,
|
|
701
|
+
lastRecordingReason: state.lastRecordingReason,
|
|
545
702
|
};
|
|
546
703
|
}
|
|
547
704
|
//# sourceMappingURL=useAudioRecorder.js.map
|