@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/cjs/events.js
CHANGED
|
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.addAudioEventListener = addAudioEventListener;
|
|
8
8
|
exports.addAudioAnalysisListener = addAudioAnalysisListener;
|
|
9
9
|
exports.addRecordingInterruptionListener = addRecordingInterruptionListener;
|
|
10
|
+
exports.addMaxDurationReachedListener = addMaxDurationReachedListener;
|
|
10
11
|
const expo_modules_core_1 = require("expo-modules-core");
|
|
11
12
|
const AudioStudioModule_1 = __importDefault(require("./AudioStudioModule"));
|
|
12
13
|
const emitter = new expo_modules_core_1.LegacyEventEmitter(AudioStudioModule_1.default);
|
|
@@ -26,4 +27,7 @@ function addRecordingInterruptionListener(listener) {
|
|
|
26
27
|
});
|
|
27
28
|
return subscription;
|
|
28
29
|
}
|
|
30
|
+
function addMaxDurationReachedListener(listener) {
|
|
31
|
+
return emitter.addListener('MaxDurationReached', listener);
|
|
32
|
+
}
|
|
29
33
|
//# sourceMappingURL=events.js.map
|
package/build/cjs/events.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/events.ts"],"names":[],"mappings":";AAAA,sCAAsC;;;;;
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/events.ts"],"names":[],"mappings":";AAAA,sCAAsC;;;;;AAmCtC,sDAIC;AAKD,4DAIC;AAED,4EAeC;AAED,sEAOC;AAxED,yDAA8E;AAO9E,4EAAmD;AAEnD,MAAM,OAAO,GAAG,IAAI,sCAAkB,CAAC,2BAAiB,CAAC,CAAA;AAwBzD,SAAgB,qBAAqB,CACjC,QAAqD;IAErD,OAAO,OAAO,CAAC,WAAW,CAAoB,WAAW,EAAE,QAAQ,CAAC,CAAA;AACxE,CAAC;AAKD,SAAgB,wBAAwB,CACpC,QAAsD;IAEtD,OAAO,OAAO,CAAC,WAAW,CAAqB,eAAe,EAAE,QAAQ,CAAC,CAAA;AAC7E,CAAC;AAED,SAAgB,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,SAAgB,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/cjs/index.js
CHANGED
|
@@ -18,7 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
18
18
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
19
|
};
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.ExpoAudioStreamModule = exports.mapStreamError = exports.AudioStreamError = exports.mapExtractionError = exports.AudioExtractionError = exports.useSharedAudioRecorder = exports.useAudioRecorder = exports.MAX_DURATION_MS = exports.computeMelFrameWasm = exports.initMelStreamingWasm = exports.extractMelSpectrogram = exports.getAudioDecodeCapabilities = exports.streamAudioData = exports.extractAudioData = exports.trimAudio = exports.extractPreview = exports.extractAudioAnalysis = exports.extractRawWavAnalysis = exports.AudioStudioModule = exports.AudioRecorderProvider = exports.extractPreviewBars = exports.setMelSpectrogramWasmUrl = exports.useAudioDevices = exports.audioDeviceManager = exports.AudioDeviceManager = exports.validateRecordingConfig = exports.getFallbackBitDepth = exports.getFallbackEncoding = exports.isBitDepthSupported = exports.isEncodingSupported = exports.getPlatformCapabilities = void 0;
|
|
21
|
+
exports.ExpoAudioStreamModule = exports.mapStreamError = exports.AudioStreamError = exports.mapExtractionError = exports.AudioExtractionError = exports.useSharedAudioRecorder = exports.useAudioRecorder = exports.MAX_DURATION_MS = exports.computeMelFrameWasm = exports.initMelStreamingWasm = exports.extractMelSpectrogram = exports.getAudioDecodeCapabilities = exports.streamAudioData = exports.extractAudioData = exports.trimAudio = exports.extractPreview = exports.extractAudioAnalysis = exports.extractRawWavAnalysis = exports.AudioStudioModule = exports.AudioRecorderProvider = exports.extractPreviewBars = exports.setMelSpectrogramWasmUrl = exports.useAudioDevices = exports.audioDeviceManager = exports.AudioDeviceManager = exports.validateRecordingConfig = exports.getFallbackBitDepth = exports.getFallbackEncoding = exports.isBitDepthSupported = exports.isEncodingSupported = exports.getPlatformCapabilities = exports.addMaxDurationReachedListener = void 0;
|
|
22
22
|
const extractAudioAnalysis_1 = require("./AudioAnalysis/extractAudioAnalysis");
|
|
23
23
|
Object.defineProperty(exports, "extractRawWavAnalysis", { enumerable: true, get: function () { return extractAudioAnalysis_1.extractRawWavAnalysis; } });
|
|
24
24
|
Object.defineProperty(exports, "extractAudioAnalysis", { enumerable: true, get: function () { return extractAudioAnalysis_1.extractAudioAnalysis; } });
|
|
@@ -44,6 +44,8 @@ const trimAudio_1 = require("./trimAudio");
|
|
|
44
44
|
Object.defineProperty(exports, "trimAudio", { enumerable: true, get: function () { return trimAudio_1.trimAudio; } });
|
|
45
45
|
const useAudioRecorder_1 = require("./useAudioRecorder");
|
|
46
46
|
Object.defineProperty(exports, "useAudioRecorder", { enumerable: true, get: function () { return useAudioRecorder_1.useAudioRecorder; } });
|
|
47
|
+
var events_1 = require("./events");
|
|
48
|
+
Object.defineProperty(exports, "addMaxDurationReachedListener", { enumerable: true, get: function () { return events_1.addMaxDurationReachedListener; } });
|
|
47
49
|
__exportStar(require("./utils/convertPCMToFloat32"), exports);
|
|
48
50
|
__exportStar(require("./utils/getWavFileInfo"), exports);
|
|
49
51
|
__exportStar(require("./utils/writeWavHeader"), exports);
|
package/build/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA,eAAe;;;;;;;;;;;;;;;;;;;;AAEf,+EAG6C;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA,eAAe;;;;;;;;;;;;;;;;;;;;AAEf,+EAG6C;AAmDzC,sGArDA,4CAAqB,OAqDA;AACrB,qGArDA,2CAAoB,OAqDA;AAnDxB,uEAAmE;AAsD/D,iGAtDK,mCAAgB,OAsDL;AArDpB,iFAG8C;AAqD1C,sGAvDA,6CAAqB,OAuDA;AAGrB,gGAzDA,uCAAe,OAyDA;AAvDnB,mEAA+D;AA+C3D,+FA/CK,+BAAc,OA+CL;AA9ClB,2EAG2C;AAiDvC,qGAnDA,yCAAoB,OAmDA;AACpB,oGAnDA,wCAAmB,OAmDA;AAjDvB,qEAGiC;AAmC7B,sGArCA,8CAAqB,OAqCA;AAcrB,uGAlDA,+CAAsB,OAkDA;AAhD1B,4EAAmD;AAmC/C,4BAnCG,2BAAiB,CAmCH;AAlCrB,uDAG0B;AAsCtB,2GAxCA,4CAA0B,OAwCA;AAD1B,gGAtCA,iCAAe,OAsCA;AApCnB,2CAAuC;AAkCnC,0FAlCK,qBAAS,OAkCL;AAjCb,yDAAqD;AAyCjD,iGAzCK,mCAAgB,OAyCL;AAxCpB,mCAAwD;AAA/C,uHAAA,6BAA6B,OAAA;AAEtC,8DAA2C;AAC3C,yDAAsC;AACtC,yDAAsC;AAEtC,+BAA+B;AAC/B,uEAQwC;AAPpC,8HAAA,uBAAuB,OAAA;AACvB,0HAAA,mBAAmB,OAAA;AACnB,0HAAA,mBAAmB,OAAA;AACnB,0HAAA,mBAAmB,OAAA;AACnB,0HAAA,mBAAmB,OAAA;AACnB,8HAAA,uBAAuB,OAAA;AAI3B,4BAA4B;AAC5B,2DAA6E;AAApE,wHAAA,kBAAkB,OAAA;AAAE,wHAAA,kBAAkB,OAAA;AAE/C,8BAA8B;AAC9B,2DAAyD;AAAhD,kHAAA,eAAe,OAAA;AAExB,yDAAqE;AAA5D,sHAAA,wBAAwB,OAAA;AACjC,yEAAuE;AAA9D,wHAAA,kBAAkB,OAAA;AAoB3B,sEAGsC;AAFlC,4HAAA,oBAAoB,OAAA;AACpB,0HAAA,kBAAkB,OAAA;AAOtB,8DAGkC;AAF9B,oHAAA,gBAAgB,OAAA;AAChB,kHAAA,cAAc,OAAA;AAoBlB,gDAAgD;AACnC,QAAA,qBAAqB,GAAG,2BAAiB,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"]}
|
|
@@ -11,7 +11,7 @@ const AudioDeviceManager_1 = require("./AudioDeviceManager");
|
|
|
11
11
|
const AudioStudioModule_1 = __importDefault(require("./AudioStudioModule"));
|
|
12
12
|
const platformLimitations_1 = require("./constants/platformLimitations");
|
|
13
13
|
const events_1 = require("./events");
|
|
14
|
-
const
|
|
14
|
+
const nativeRecordingOptions_1 = require("./utils/nativeRecordingOptions");
|
|
15
15
|
const defaultAnalysis = {
|
|
16
16
|
segmentDurationMs: 100,
|
|
17
17
|
bitDepth: 32,
|
|
@@ -30,6 +30,31 @@ const defaultAnalysis = {
|
|
|
30
30
|
},
|
|
31
31
|
extractionTimeMs: 0,
|
|
32
32
|
};
|
|
33
|
+
function finiteOrZero(value) {
|
|
34
|
+
return Number.isFinite(value) ? value : 0;
|
|
35
|
+
}
|
|
36
|
+
function sanitizeSerializableValue(value) {
|
|
37
|
+
if (typeof value === 'number') {
|
|
38
|
+
return finiteOrZero(value);
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return value.map((item) => sanitizeSerializableValue(item));
|
|
42
|
+
}
|
|
43
|
+
if (value && typeof value === 'object') {
|
|
44
|
+
const sanitized = {};
|
|
45
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
46
|
+
sanitized[key] = sanitizeSerializableValue(nestedValue);
|
|
47
|
+
}
|
|
48
|
+
return sanitized;
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function createSerializableAnalysis(analysis) {
|
|
53
|
+
return sanitizeSerializableValue(analysis);
|
|
54
|
+
}
|
|
55
|
+
function createRecordingSnapshot(recording) {
|
|
56
|
+
return sanitizeSerializableValue(recording);
|
|
57
|
+
}
|
|
33
58
|
function audioRecorderReducer(state, action) {
|
|
34
59
|
switch (action.type) {
|
|
35
60
|
case 'START':
|
|
@@ -41,6 +66,9 @@ function audioRecorderReducer(state, action) {
|
|
|
41
66
|
size: 0,
|
|
42
67
|
compression: undefined,
|
|
43
68
|
analysisData: defaultAnalysis,
|
|
69
|
+
maxDurationMs: undefined,
|
|
70
|
+
maxDurationReached: false,
|
|
71
|
+
lastRecordingReason: undefined,
|
|
44
72
|
};
|
|
45
73
|
case 'STOP':
|
|
46
74
|
return {
|
|
@@ -51,6 +79,9 @@ function audioRecorderReducer(state, action) {
|
|
|
51
79
|
size: 0,
|
|
52
80
|
compression: undefined,
|
|
53
81
|
analysisData: undefined,
|
|
82
|
+
lastRecordingReason: action.payload.reason,
|
|
83
|
+
// Preserve max-duration state after stop so UI and agentic
|
|
84
|
+
// validation can explain why recording ended. START resets it.
|
|
54
85
|
};
|
|
55
86
|
case 'PAUSE':
|
|
56
87
|
return { ...state, isPaused: true, isRecording: false };
|
|
@@ -75,9 +106,17 @@ function audioRecorderReducer(state, action) {
|
|
|
75
106
|
format: action.payload.compression.format,
|
|
76
107
|
}
|
|
77
108
|
: undefined,
|
|
109
|
+
maxDurationMs: action.payload.maxDurationMs,
|
|
110
|
+
maxDurationReached: action.payload.maxDurationReached,
|
|
78
111
|
};
|
|
79
112
|
return newState;
|
|
80
113
|
}
|
|
114
|
+
case 'MAX_DURATION_REACHED':
|
|
115
|
+
return {
|
|
116
|
+
...state,
|
|
117
|
+
maxDurationMs: action.payload.maxDurationMs,
|
|
118
|
+
maxDurationReached: true,
|
|
119
|
+
};
|
|
81
120
|
case 'UPDATE_ANALYSIS':
|
|
82
121
|
return {
|
|
83
122
|
...state,
|
|
@@ -102,6 +141,9 @@ function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl, } = {}
|
|
|
102
141
|
size: 0,
|
|
103
142
|
compression: undefined,
|
|
104
143
|
analysisData: undefined,
|
|
144
|
+
maxDurationMs: undefined,
|
|
145
|
+
maxDurationReached: false,
|
|
146
|
+
lastRecordingReason: undefined,
|
|
105
147
|
});
|
|
106
148
|
const startResultRef = (0, react_1.useRef)(null);
|
|
107
149
|
const analysisListenerRef = (0, react_1.useRef)(null);
|
|
@@ -126,8 +168,12 @@ function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl, } = {}
|
|
|
126
168
|
durationMs: 0,
|
|
127
169
|
size: 0,
|
|
128
170
|
compression: undefined,
|
|
171
|
+
maxDurationMs: undefined,
|
|
172
|
+
maxDurationReached: false,
|
|
129
173
|
});
|
|
130
174
|
const recordingConfigRef = (0, react_1.useRef)(null);
|
|
175
|
+
const maxDurationHandledRef = (0, react_1.useRef)(false);
|
|
176
|
+
const stopFinalizationRef = (0, react_1.useRef)(null);
|
|
131
177
|
// Generate unique instance ID for debugging
|
|
132
178
|
const instanceId = (0, react_1.useId)().replace(/:/g, '').slice(0, 5);
|
|
133
179
|
const handleAudioAnalysis = (0, react_1.useCallback)(async ({ analysis, visualizationDuration, }) => {
|
|
@@ -293,10 +339,105 @@ function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl, } = {}
|
|
|
293
339
|
logger?.error(`Error processing audio event:`, error);
|
|
294
340
|
}
|
|
295
341
|
}, []);
|
|
342
|
+
const finalizeRecordingStop = (0, react_1.useCallback)(async (reason) => {
|
|
343
|
+
if (stopFinalizationRef.current) {
|
|
344
|
+
return stopFinalizationRef.current;
|
|
345
|
+
}
|
|
346
|
+
const finalizePromise = (async () => {
|
|
347
|
+
const nativeStopResult = await audioStudio.stopRecording();
|
|
348
|
+
if (!nativeStopResult) {
|
|
349
|
+
throw new Error('Failed to stop recording');
|
|
350
|
+
}
|
|
351
|
+
const stopResult = createRecordingSnapshot(nativeStopResult);
|
|
352
|
+
if (shouldKeepFullAnalysis(recordingConfigRef.current)) {
|
|
353
|
+
stopResult.analysisData = createSerializableAnalysis(fullAnalysisRef.current);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
// `keepFullAnalysis` is a hook-level retention policy. If a platform
|
|
357
|
+
// starts returning native analysisData in the future, keep opt-out
|
|
358
|
+
// semantics explicit and avoid leaking a full history here.
|
|
359
|
+
delete stopResult.analysisData;
|
|
360
|
+
}
|
|
361
|
+
if (analysisListenerRef.current) {
|
|
362
|
+
analysisListenerRef.current.remove();
|
|
363
|
+
analysisListenerRef.current = null;
|
|
364
|
+
}
|
|
365
|
+
onAudioStreamRef.current = null;
|
|
366
|
+
stateRef.current.isRecording = false;
|
|
367
|
+
stateRef.current.isPaused = false;
|
|
368
|
+
// Note: We deliberately DON'T clear recordingConfigRef here to preserve callbacks.
|
|
369
|
+
logger?.debug(`recording stopped`, stopResult);
|
|
370
|
+
maxDurationHandledRef.current = false;
|
|
371
|
+
dispatch({
|
|
372
|
+
type: 'STOP',
|
|
373
|
+
payload: { reason },
|
|
374
|
+
});
|
|
375
|
+
const stoppedCallback = recordingConfigRef.current?.onRecordingStopped;
|
|
376
|
+
if (stoppedCallback) {
|
|
377
|
+
try {
|
|
378
|
+
void Promise.resolve(stoppedCallback(stopResult, reason)).catch((error) => {
|
|
379
|
+
logger?.error(`Error in recording stopped callback:`, error);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
logger?.error(`Error in recording stopped callback:`, error);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return stopResult;
|
|
387
|
+
})();
|
|
388
|
+
stopFinalizationRef.current = finalizePromise;
|
|
389
|
+
try {
|
|
390
|
+
return await finalizePromise;
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
stopFinalizationRef.current = null;
|
|
394
|
+
}
|
|
395
|
+
}, [audioStudio, dispatch, logger]);
|
|
396
|
+
const handleMaxDurationReached = (0, react_1.useCallback)(async (event) => {
|
|
397
|
+
if (maxDurationHandledRef.current) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
maxDurationHandledRef.current = true;
|
|
401
|
+
const config = recordingConfigRef.current;
|
|
402
|
+
const callbackEvent = {
|
|
403
|
+
...event,
|
|
404
|
+
autoStopped: event.autoStopped || !!config?.autoStopOnMaxDuration,
|
|
405
|
+
};
|
|
406
|
+
stateRef.current.maxDurationMs = callbackEvent.maxDurationMs;
|
|
407
|
+
stateRef.current.maxDurationReached = true;
|
|
408
|
+
dispatch({
|
|
409
|
+
type: 'MAX_DURATION_REACHED',
|
|
410
|
+
payload: callbackEvent,
|
|
411
|
+
});
|
|
412
|
+
try {
|
|
413
|
+
config?.onMaxDurationReached?.(callbackEvent);
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
logger?.error(`Error in max duration callback:`, error);
|
|
417
|
+
}
|
|
418
|
+
if (config?.autoStopOnMaxDuration && stateRef.current.isRecording) {
|
|
419
|
+
try {
|
|
420
|
+
await finalizeRecordingStop('maxDuration');
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
logger?.error(`Error auto-stopping on max duration:`, error);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}, [dispatch, finalizeRecordingStop, logger]);
|
|
296
427
|
const checkStatus = (0, react_1.useCallback)(async () => {
|
|
297
428
|
try {
|
|
298
429
|
const status = audioStudio.status();
|
|
299
430
|
logger?.debug(`Status: paused: ${status.isPaused} isRecording: ${status.isRecording} durationMs: ${status.durationMs} size: ${status.size}`, status.compression);
|
|
431
|
+
if (status.maxDurationReached === true &&
|
|
432
|
+
status.maxDurationMs != null &&
|
|
433
|
+
!stateRef.current.maxDurationReached) {
|
|
434
|
+
await handleMaxDurationReached({
|
|
435
|
+
durationMs: status.durationMs,
|
|
436
|
+
maxDurationMs: status.maxDurationMs,
|
|
437
|
+
overrunMs: Math.max(0, status.durationMs - status.maxDurationMs),
|
|
438
|
+
autoStopped: false,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
300
441
|
// Only dispatch if values actually changed
|
|
301
442
|
if (status.isRecording !== stateRef.current.isRecording ||
|
|
302
443
|
status.isPaused !== stateRef.current.isPaused) {
|
|
@@ -310,17 +451,34 @@ function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl, } = {}
|
|
|
310
451
|
},
|
|
311
452
|
});
|
|
312
453
|
}
|
|
454
|
+
const statusMaxDurationReached = status.maxDurationReached ?? false;
|
|
455
|
+
const preserveStoppedMaxDuration = !status.isRecording &&
|
|
456
|
+
!status.isPaused &&
|
|
457
|
+
stateRef.current.maxDurationReached &&
|
|
458
|
+
!statusMaxDurationReached;
|
|
459
|
+
const nextMaxDurationMs = preserveStoppedMaxDuration
|
|
460
|
+
? stateRef.current.maxDurationMs
|
|
461
|
+
: status.maxDurationMs;
|
|
462
|
+
const nextMaxDurationReached = preserveStoppedMaxDuration
|
|
463
|
+
? true
|
|
464
|
+
: statusMaxDurationReached;
|
|
313
465
|
if (status.durationMs !== stateRef.current.durationMs ||
|
|
314
|
-
status.size !== stateRef.current.size
|
|
466
|
+
status.size !== stateRef.current.size ||
|
|
467
|
+
nextMaxDurationMs !== stateRef.current.maxDurationMs ||
|
|
468
|
+
nextMaxDurationReached !== stateRef.current.maxDurationReached) {
|
|
315
469
|
stateRef.current.durationMs = status.durationMs;
|
|
316
470
|
stateRef.current.size = status.size;
|
|
317
471
|
stateRef.current.compression = status.compression;
|
|
472
|
+
stateRef.current.maxDurationMs = nextMaxDurationMs;
|
|
473
|
+
stateRef.current.maxDurationReached = nextMaxDurationReached;
|
|
318
474
|
dispatch({
|
|
319
475
|
type: 'UPDATE_STATUS',
|
|
320
476
|
payload: {
|
|
321
477
|
durationMs: status.durationMs,
|
|
322
478
|
size: status.size,
|
|
323
479
|
compression: status.compression,
|
|
480
|
+
maxDurationMs: nextMaxDurationMs,
|
|
481
|
+
maxDurationReached: nextMaxDurationReached,
|
|
324
482
|
},
|
|
325
483
|
});
|
|
326
484
|
}
|
|
@@ -328,7 +486,7 @@ function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl, } = {}
|
|
|
328
486
|
catch (error) {
|
|
329
487
|
logger?.error(`Error getting status:`, error);
|
|
330
488
|
}
|
|
331
|
-
}, [audioStudio, logger]);
|
|
489
|
+
}, [audioStudio, handleMaxDurationReached, logger]);
|
|
332
490
|
// Update ref when state changes
|
|
333
491
|
(0, react_1.useEffect)(() => {
|
|
334
492
|
stateRef.current = {
|
|
@@ -337,6 +495,8 @@ function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl, } = {}
|
|
|
337
495
|
durationMs: state.durationMs,
|
|
338
496
|
size: state.size,
|
|
339
497
|
compression: state.compression,
|
|
498
|
+
maxDurationMs: state.maxDurationMs,
|
|
499
|
+
maxDurationReached: state.maxDurationReached ?? false,
|
|
340
500
|
};
|
|
341
501
|
}, [
|
|
342
502
|
state.isRecording,
|
|
@@ -344,6 +504,8 @@ function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl, } = {}
|
|
|
344
504
|
state.durationMs,
|
|
345
505
|
state.size,
|
|
346
506
|
state.compression,
|
|
507
|
+
state.maxDurationMs,
|
|
508
|
+
state.maxDurationReached,
|
|
347
509
|
]);
|
|
348
510
|
const startRecording = (0, react_1.useCallback)(async (recordingOptions) => {
|
|
349
511
|
// Validate the encoding configuration
|
|
@@ -362,11 +524,11 @@ function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl, } = {}
|
|
|
362
524
|
encoding: validationResult.encoding,
|
|
363
525
|
};
|
|
364
526
|
recordingConfigRef.current = validatedOptions;
|
|
527
|
+
maxDurationHandledRef.current = false;
|
|
365
528
|
logger?.debug(`start recording with validated config`, validatedOptions);
|
|
366
529
|
analysisRef.current = { ...defaultAnalysis }; // Reset analysis data
|
|
367
530
|
fullAnalysisRef.current = { ...defaultAnalysis };
|
|
368
|
-
const { onAudioStream,
|
|
369
|
-
const { enableProcessing } = options;
|
|
531
|
+
const { onAudioStream, enableProcessing } = validatedOptions;
|
|
370
532
|
const maxRecentDataDuration = 10000; // TODO compute maxRecentDataDuration based on screen dimensions
|
|
371
533
|
if (typeof onAudioStream === 'function') {
|
|
372
534
|
onAudioStreamRef.current = onAudioStream;
|
|
@@ -375,8 +537,10 @@ function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl, } = {}
|
|
|
375
537
|
logger?.warn(`onAudioStream is not a function`, onAudioStream);
|
|
376
538
|
onAudioStreamRef.current = null;
|
|
377
539
|
}
|
|
378
|
-
// Strip
|
|
379
|
-
|
|
540
|
+
// Strip hook-only values and undefineds that can't cross the native bridge.
|
|
541
|
+
// autoStopOnMaxDuration stays hook-owned so finalization can expose
|
|
542
|
+
// the same AudioRecording result as a manual stop.
|
|
543
|
+
const cleanOptions = (0, nativeRecordingOptions_1.createNativeRecordingOptions)(validatedOptions);
|
|
380
544
|
const startResult = await audioStudio.startRecording(cleanOptions);
|
|
381
545
|
dispatch({ type: 'START' });
|
|
382
546
|
startResultRef.current = startResult;
|
|
@@ -402,7 +566,7 @@ function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl, } = {}
|
|
|
402
566
|
logger?.debug(`preparing recording`, recordingOptions);
|
|
403
567
|
analysisRef.current = { ...defaultAnalysis }; // Reset analysis data
|
|
404
568
|
fullAnalysisRef.current = { ...defaultAnalysis };
|
|
405
|
-
const { onAudioStream
|
|
569
|
+
const { onAudioStream } = recordingOptions;
|
|
406
570
|
// Store onAudioStream for later use when recording starts
|
|
407
571
|
if (typeof onAudioStream === 'function') {
|
|
408
572
|
onAudioStreamRef.current = onAudioStream;
|
|
@@ -411,34 +575,16 @@ function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl, } = {}
|
|
|
411
575
|
logger?.warn(`onAudioStream is not a function`, onAudioStream);
|
|
412
576
|
onAudioStreamRef.current = null;
|
|
413
577
|
}
|
|
414
|
-
// Strip
|
|
415
|
-
const cleanOptions = (0,
|
|
578
|
+
// Strip hook-only values and undefineds that can't cross the native bridge.
|
|
579
|
+
const cleanOptions = (0, nativeRecordingOptions_1.createNativeRecordingOptions)(recordingOptions);
|
|
416
580
|
// Call the native prepareRecording method
|
|
417
581
|
await audioStudio.prepareRecording(cleanOptions);
|
|
418
582
|
logger?.debug(`recording prepared successfully`);
|
|
419
583
|
}, []);
|
|
420
584
|
const stopRecording = (0, react_1.useCallback)(async () => {
|
|
421
585
|
logger?.debug(`stoping recording`);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
stopResult.analysisData = fullAnalysisRef.current;
|
|
425
|
-
}
|
|
426
|
-
else {
|
|
427
|
-
// `keepFullAnalysis` is a hook-level retention policy. If a platform
|
|
428
|
-
// starts returning native analysisData in the future, keep opt-out
|
|
429
|
-
// semantics explicit and avoid leaking a full history here.
|
|
430
|
-
delete stopResult.analysisData;
|
|
431
|
-
}
|
|
432
|
-
if (analysisListenerRef.current) {
|
|
433
|
-
analysisListenerRef.current.remove();
|
|
434
|
-
analysisListenerRef.current = null;
|
|
435
|
-
}
|
|
436
|
-
onAudioStreamRef.current = null;
|
|
437
|
-
// Note: We deliberately DON'T clear recordingConfigRef here to preserve interruption callback
|
|
438
|
-
logger?.debug(`recording stopped`, stopResult);
|
|
439
|
-
dispatch({ type: 'STOP' });
|
|
440
|
-
return stopResult;
|
|
441
|
-
}, [dispatch]);
|
|
586
|
+
return finalizeRecordingStop('manual');
|
|
587
|
+
}, [finalizeRecordingStop, logger]);
|
|
442
588
|
const pauseRecording = (0, react_1.useCallback)(async () => {
|
|
443
589
|
logger?.debug(`pause recording`);
|
|
444
590
|
const pauseResult = await audioStudio.pauseRecording();
|
|
@@ -451,6 +597,14 @@ function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl, } = {}
|
|
|
451
597
|
dispatch({ type: 'RESUME' });
|
|
452
598
|
return resumeResult;
|
|
453
599
|
}, [dispatch]);
|
|
600
|
+
(0, react_1.useEffect)(() => {
|
|
601
|
+
const subscription = (0, events_1.addMaxDurationReachedListener)(async (event) => {
|
|
602
|
+
await handleMaxDurationReached(event);
|
|
603
|
+
});
|
|
604
|
+
return () => {
|
|
605
|
+
subscription.remove();
|
|
606
|
+
};
|
|
607
|
+
}, [handleMaxDurationReached]);
|
|
454
608
|
(0, react_1.useEffect)(() => {
|
|
455
609
|
let intervalId;
|
|
456
610
|
if (state.isRecording || state.isPaused) {
|
|
@@ -548,6 +702,9 @@ function useAudioRecorder({ logger, audioWorkletUrl, featuresExtratorUrl, } = {}
|
|
|
548
702
|
size: state.size,
|
|
549
703
|
compression: state.compression,
|
|
550
704
|
analysisData: state.analysisData,
|
|
705
|
+
maxDurationMs: state.maxDurationMs,
|
|
706
|
+
maxDurationReached: state.maxDurationReached,
|
|
707
|
+
lastRecordingReason: state.lastRecordingReason,
|
|
551
708
|
};
|
|
552
709
|
}
|
|
553
710
|
//# sourceMappingURL=useAudioRecorder.js.map
|