@siteed/audio-studio 3.2.1-beta.0 → 3.2.1-beta.1
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/README.md +30 -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 +139 -4
- package/build/cjs/useAudioRecorder.js.map +1 -1
- 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 +140 -5
- package/build/esm/useAudioRecorder.js.map +1 -1
- package/build/types/AudioStudio.types.d.ts +44 -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 +2 -0
- package/build/types/useAudioRecorder.d.ts.map +1 -1
- 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 +48 -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 +182 -2
package/src/AudioStudioModule.ts
CHANGED
|
@@ -10,7 +10,12 @@ if (Platform.OS === 'web') {
|
|
|
10
10
|
let instance: AudioStudioWeb | null = null
|
|
11
11
|
|
|
12
12
|
AudioStudioModule = (webProps: AudioStudioWebProps) => {
|
|
13
|
-
instance ??= new AudioStudioWeb(
|
|
13
|
+
instance ??= new AudioStudioWeb({
|
|
14
|
+
...webProps,
|
|
15
|
+
emitEvent: (eventName, params) => {
|
|
16
|
+
AudioStudioModule.sendEvent(eventName, params)
|
|
17
|
+
},
|
|
18
|
+
})
|
|
14
19
|
return instance
|
|
15
20
|
}
|
|
16
21
|
AudioStudioModule.requestPermissionsAsync = async () => {
|
package/src/events.ts
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
import { LegacyEventEmitter, type EventSubscription } from 'expo-modules-core'
|
|
4
4
|
|
|
5
5
|
import { AudioAnalysis } from './AudioAnalysis/AudioAnalysis.types'
|
|
6
|
-
import {
|
|
6
|
+
import type {
|
|
7
|
+
MaxDurationReachedEvent,
|
|
8
|
+
RecordingInterruptionEvent,
|
|
9
|
+
} from './AudioStudio.types'
|
|
7
10
|
import AudioStudioModule from './AudioStudioModule'
|
|
8
11
|
|
|
9
12
|
const emitter = new LegacyEventEmitter(AudioStudioModule)
|
|
@@ -61,3 +64,12 @@ export function addRecordingInterruptionListener(
|
|
|
61
64
|
|
|
62
65
|
return subscription
|
|
63
66
|
}
|
|
67
|
+
|
|
68
|
+
export function addMaxDurationReachedListener(
|
|
69
|
+
listener: (event: MaxDurationReachedEvent) => void
|
|
70
|
+
): EventSubscription {
|
|
71
|
+
return emitter.addListener<MaxDurationReachedEvent>(
|
|
72
|
+
'MaxDurationReached',
|
|
73
|
+
listener
|
|
74
|
+
)
|
|
75
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from './streamAudioData'
|
|
26
26
|
import { trimAudio } from './trimAudio'
|
|
27
27
|
import { useAudioRecorder } from './useAudioRecorder'
|
|
28
|
+
export { addMaxDurationReachedListener } from './events'
|
|
28
29
|
|
|
29
30
|
export * from './utils/convertPCMToFloat32'
|
|
30
31
|
export * from './utils/getWavFileInfo'
|
package/src/useAudioRecorder.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
AudioStreamStatus,
|
|
11
11
|
CompressionInfo,
|
|
12
12
|
ConsoleLike,
|
|
13
|
+
MaxDurationReachedEvent,
|
|
13
14
|
RecordingConfig,
|
|
14
15
|
StartRecordingResult,
|
|
15
16
|
} from './AudioStudio.types'
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
addAudioAnalysisListener,
|
|
20
21
|
addAudioEventListener,
|
|
21
22
|
AudioEventPayload,
|
|
23
|
+
addMaxDurationReachedListener,
|
|
22
24
|
addRecordingInterruptionListener,
|
|
23
25
|
} from './events'
|
|
24
26
|
import { cleanNativeOptions } from './utils/cleanNativeOptions'
|
|
@@ -41,6 +43,8 @@ export interface UseAudioRecorderState {
|
|
|
41
43
|
size: number
|
|
42
44
|
compression?: CompressionInfo
|
|
43
45
|
analysisData?: AudioAnalysis
|
|
46
|
+
maxDurationMs?: number
|
|
47
|
+
maxDurationReached?: boolean
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
interface RecorderReducerState {
|
|
@@ -50,6 +54,8 @@ interface RecorderReducerState {
|
|
|
50
54
|
size: number
|
|
51
55
|
compression?: CompressionInfo
|
|
52
56
|
analysisData?: AudioAnalysis
|
|
57
|
+
maxDurationMs?: number
|
|
58
|
+
maxDurationReached?: boolean
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
type RecorderAction =
|
|
@@ -67,8 +73,14 @@ type RecorderAction =
|
|
|
67
73
|
durationMs: number
|
|
68
74
|
size: number
|
|
69
75
|
compression?: CompressionInfo
|
|
76
|
+
maxDurationMs?: number
|
|
77
|
+
maxDurationReached?: boolean
|
|
70
78
|
}
|
|
71
79
|
}
|
|
80
|
+
| {
|
|
81
|
+
type: 'MAX_DURATION_REACHED'
|
|
82
|
+
payload: MaxDurationReachedEvent
|
|
83
|
+
}
|
|
72
84
|
| { type: 'UPDATE_ANALYSIS'; payload: AudioAnalysis }
|
|
73
85
|
|
|
74
86
|
const defaultAnalysis: AudioAnalysis = {
|
|
@@ -104,6 +116,8 @@ function audioRecorderReducer(
|
|
|
104
116
|
size: 0,
|
|
105
117
|
compression: undefined,
|
|
106
118
|
analysisData: defaultAnalysis,
|
|
119
|
+
maxDurationMs: undefined,
|
|
120
|
+
maxDurationReached: false,
|
|
107
121
|
}
|
|
108
122
|
case 'STOP':
|
|
109
123
|
return {
|
|
@@ -114,6 +128,8 @@ function audioRecorderReducer(
|
|
|
114
128
|
size: 0,
|
|
115
129
|
compression: undefined,
|
|
116
130
|
analysisData: undefined,
|
|
131
|
+
// Preserve max-duration state after stop so UI and agentic
|
|
132
|
+
// validation can explain why recording ended. START resets it.
|
|
117
133
|
}
|
|
118
134
|
case 'PAUSE':
|
|
119
135
|
return { ...state, isPaused: true, isRecording: false }
|
|
@@ -138,9 +154,17 @@ function audioRecorderReducer(
|
|
|
138
154
|
format: action.payload.compression.format,
|
|
139
155
|
}
|
|
140
156
|
: undefined,
|
|
157
|
+
maxDurationMs: action.payload.maxDurationMs,
|
|
158
|
+
maxDurationReached: action.payload.maxDurationReached,
|
|
141
159
|
}
|
|
142
160
|
return newState
|
|
143
161
|
}
|
|
162
|
+
case 'MAX_DURATION_REACHED':
|
|
163
|
+
return {
|
|
164
|
+
...state,
|
|
165
|
+
maxDurationMs: action.payload.maxDurationMs,
|
|
166
|
+
maxDurationReached: true,
|
|
167
|
+
}
|
|
144
168
|
case 'UPDATE_ANALYSIS':
|
|
145
169
|
return {
|
|
146
170
|
...state,
|
|
@@ -176,6 +200,8 @@ export function useAudioRecorder({
|
|
|
176
200
|
size: 0,
|
|
177
201
|
compression: undefined,
|
|
178
202
|
analysisData: undefined,
|
|
203
|
+
maxDurationMs: undefined,
|
|
204
|
+
maxDurationReached: false,
|
|
179
205
|
})
|
|
180
206
|
|
|
181
207
|
const startResultRef = useRef<StartRecordingResult | null>(null)
|
|
@@ -208,9 +234,12 @@ export function useAudioRecorder({
|
|
|
208
234
|
durationMs: 0,
|
|
209
235
|
size: 0,
|
|
210
236
|
compression: undefined as CompressionInfo | undefined,
|
|
237
|
+
maxDurationMs: undefined as number | undefined,
|
|
238
|
+
maxDurationReached: false,
|
|
211
239
|
})
|
|
212
240
|
|
|
213
241
|
const recordingConfigRef = useRef<RecordingConfig | null>(null)
|
|
242
|
+
const maxDurationHandledRef = useRef(false)
|
|
214
243
|
|
|
215
244
|
// Generate unique instance ID for debugging
|
|
216
245
|
const instanceId = useId().replace(/:/g, '').slice(0, 5)
|
|
@@ -442,6 +471,113 @@ export function useAudioRecorder({
|
|
|
442
471
|
[]
|
|
443
472
|
)
|
|
444
473
|
|
|
474
|
+
const handleMaxDurationReached = useCallback(
|
|
475
|
+
async (event: MaxDurationReachedEvent) => {
|
|
476
|
+
if (maxDurationHandledRef.current) {
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
maxDurationHandledRef.current = true
|
|
481
|
+
const config = recordingConfigRef.current
|
|
482
|
+
const callbackEvent: MaxDurationReachedEvent = {
|
|
483
|
+
...event,
|
|
484
|
+
autoStopped:
|
|
485
|
+
event.autoStopped || !!config?.autoStopOnMaxDuration,
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
stateRef.current.maxDurationMs = callbackEvent.maxDurationMs
|
|
489
|
+
stateRef.current.maxDurationReached = true
|
|
490
|
+
dispatch({
|
|
491
|
+
type: 'MAX_DURATION_REACHED',
|
|
492
|
+
payload: callbackEvent,
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
config?.onMaxDurationReached?.(callbackEvent)
|
|
497
|
+
} catch (error) {
|
|
498
|
+
logger?.error(`Error in max duration callback:`, error)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const finishStoppedState = () => {
|
|
502
|
+
if (analysisListenerRef.current) {
|
|
503
|
+
analysisListenerRef.current.remove()
|
|
504
|
+
analysisListenerRef.current = null
|
|
505
|
+
}
|
|
506
|
+
onAudioStreamRef.current = null
|
|
507
|
+
stateRef.current.isRecording = false
|
|
508
|
+
stateRef.current.isPaused = false
|
|
509
|
+
dispatch({ type: 'STOP' })
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const waitForPlatformAutoStop = async () => {
|
|
513
|
+
const timeoutMs = 3000
|
|
514
|
+
const startedAt = Date.now()
|
|
515
|
+
let lastStatus: AudioStreamStatus | undefined
|
|
516
|
+
|
|
517
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
518
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
519
|
+
try {
|
|
520
|
+
const currentStatus: AudioStreamStatus =
|
|
521
|
+
audioStudio.status()
|
|
522
|
+
lastStatus = currentStatus
|
|
523
|
+
if (
|
|
524
|
+
!currentStatus.isRecording &&
|
|
525
|
+
!currentStatus.isPaused
|
|
526
|
+
) {
|
|
527
|
+
finishStoppedState()
|
|
528
|
+
return
|
|
529
|
+
}
|
|
530
|
+
} catch (error) {
|
|
531
|
+
logger?.warn(
|
|
532
|
+
`Error checking status after max duration auto-stop:`,
|
|
533
|
+
error
|
|
534
|
+
)
|
|
535
|
+
break
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (
|
|
540
|
+
lastStatus &&
|
|
541
|
+
(lastStatus.isRecording || lastStatus.isPaused)
|
|
542
|
+
) {
|
|
543
|
+
try {
|
|
544
|
+
await audioStudio.stopRecording()
|
|
545
|
+
} catch (error) {
|
|
546
|
+
logger?.warn(
|
|
547
|
+
`Error completing max duration auto-stop fallback:`,
|
|
548
|
+
error
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
// At this point platform-owned auto-stop did not settle cleanly.
|
|
553
|
+
// Clear hook state so the UI does not stay stuck as recording.
|
|
554
|
+
finishStoppedState()
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Only the original event tells us whether the platform already
|
|
558
|
+
// owns auto-stop. Keep stream callbacks alive until status confirms
|
|
559
|
+
// stop completion so native final audio flushes can still reach JS.
|
|
560
|
+
if (event.autoStopped && stateRef.current.isRecording) {
|
|
561
|
+
await waitForPlatformAutoStop()
|
|
562
|
+
return
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (
|
|
566
|
+
config?.autoStopOnMaxDuration &&
|
|
567
|
+
!event.autoStopped &&
|
|
568
|
+
stateRef.current.isRecording
|
|
569
|
+
) {
|
|
570
|
+
try {
|
|
571
|
+
await audioStudio.stopRecording()
|
|
572
|
+
finishStoppedState()
|
|
573
|
+
} catch (error) {
|
|
574
|
+
logger?.error(`Error auto-stopping on max duration:`, error)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
[audioStudio, dispatch, logger]
|
|
579
|
+
)
|
|
580
|
+
|
|
445
581
|
const checkStatus = useCallback(async () => {
|
|
446
582
|
try {
|
|
447
583
|
const status: AudioStreamStatus = audioStudio.status()
|
|
@@ -450,6 +586,22 @@ export function useAudioRecorder({
|
|
|
450
586
|
status.compression
|
|
451
587
|
)
|
|
452
588
|
|
|
589
|
+
if (
|
|
590
|
+
status.maxDurationReached === true &&
|
|
591
|
+
status.maxDurationMs != null &&
|
|
592
|
+
!stateRef.current.maxDurationReached
|
|
593
|
+
) {
|
|
594
|
+
await handleMaxDurationReached({
|
|
595
|
+
durationMs: status.durationMs,
|
|
596
|
+
maxDurationMs: status.maxDurationMs,
|
|
597
|
+
overrunMs: Math.max(
|
|
598
|
+
0,
|
|
599
|
+
status.durationMs - status.maxDurationMs
|
|
600
|
+
),
|
|
601
|
+
autoStopped: false,
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
|
|
453
605
|
// Only dispatch if values actually changed
|
|
454
606
|
if (
|
|
455
607
|
status.isRecording !== stateRef.current.isRecording ||
|
|
@@ -468,24 +620,32 @@ export function useAudioRecorder({
|
|
|
468
620
|
|
|
469
621
|
if (
|
|
470
622
|
status.durationMs !== stateRef.current.durationMs ||
|
|
471
|
-
status.size !== stateRef.current.size
|
|
623
|
+
status.size !== stateRef.current.size ||
|
|
624
|
+
status.maxDurationMs !== stateRef.current.maxDurationMs ||
|
|
625
|
+
status.maxDurationReached !==
|
|
626
|
+
stateRef.current.maxDurationReached
|
|
472
627
|
) {
|
|
473
628
|
stateRef.current.durationMs = status.durationMs
|
|
474
629
|
stateRef.current.size = status.size
|
|
475
630
|
stateRef.current.compression = status.compression
|
|
631
|
+
stateRef.current.maxDurationMs = status.maxDurationMs
|
|
632
|
+
stateRef.current.maxDurationReached =
|
|
633
|
+
status.maxDurationReached ?? false
|
|
476
634
|
dispatch({
|
|
477
635
|
type: 'UPDATE_STATUS',
|
|
478
636
|
payload: {
|
|
479
637
|
durationMs: status.durationMs,
|
|
480
638
|
size: status.size,
|
|
481
639
|
compression: status.compression,
|
|
640
|
+
maxDurationMs: status.maxDurationMs,
|
|
641
|
+
maxDurationReached: status.maxDurationReached,
|
|
482
642
|
},
|
|
483
643
|
})
|
|
484
644
|
}
|
|
485
645
|
} catch (error) {
|
|
486
646
|
logger?.error(`Error getting status:`, error)
|
|
487
647
|
}
|
|
488
|
-
}, [audioStudio, logger])
|
|
648
|
+
}, [audioStudio, handleMaxDurationReached, logger])
|
|
489
649
|
|
|
490
650
|
// Update ref when state changes
|
|
491
651
|
useEffect(() => {
|
|
@@ -495,6 +655,8 @@ export function useAudioRecorder({
|
|
|
495
655
|
durationMs: state.durationMs,
|
|
496
656
|
size: state.size,
|
|
497
657
|
compression: state.compression,
|
|
658
|
+
maxDurationMs: state.maxDurationMs,
|
|
659
|
+
maxDurationReached: state.maxDurationReached ?? false,
|
|
498
660
|
}
|
|
499
661
|
}, [
|
|
500
662
|
state.isRecording,
|
|
@@ -502,6 +664,8 @@ export function useAudioRecorder({
|
|
|
502
664
|
state.durationMs,
|
|
503
665
|
state.size,
|
|
504
666
|
state.compression,
|
|
667
|
+
state.maxDurationMs,
|
|
668
|
+
state.maxDurationReached,
|
|
505
669
|
])
|
|
506
670
|
|
|
507
671
|
const startRecording = useCallback(
|
|
@@ -525,6 +689,7 @@ export function useAudioRecorder({
|
|
|
525
689
|
}
|
|
526
690
|
|
|
527
691
|
recordingConfigRef.current = validatedOptions
|
|
692
|
+
maxDurationHandledRef.current = false
|
|
528
693
|
logger?.debug(
|
|
529
694
|
`start recording with validated config`,
|
|
530
695
|
validatedOptions
|
|
@@ -535,6 +700,7 @@ export function useAudioRecorder({
|
|
|
535
700
|
const {
|
|
536
701
|
onAudioStream,
|
|
537
702
|
onRecordingInterrupted,
|
|
703
|
+
onMaxDurationReached,
|
|
538
704
|
onAudioAnalysis,
|
|
539
705
|
keepFullAnalysis: _keepFullAnalysis,
|
|
540
706
|
...options
|
|
@@ -592,6 +758,7 @@ export function useAudioRecorder({
|
|
|
592
758
|
const {
|
|
593
759
|
onAudioStream,
|
|
594
760
|
onRecordingInterrupted,
|
|
761
|
+
onMaxDurationReached,
|
|
595
762
|
onAudioAnalysis,
|
|
596
763
|
keepFullAnalysis: _keepFullAnalysis,
|
|
597
764
|
...options
|
|
@@ -635,6 +802,7 @@ export function useAudioRecorder({
|
|
|
635
802
|
|
|
636
803
|
// Note: We deliberately DON'T clear recordingConfigRef here to preserve interruption callback
|
|
637
804
|
logger?.debug(`recording stopped`, stopResult)
|
|
805
|
+
maxDurationHandledRef.current = false
|
|
638
806
|
dispatch({ type: 'STOP' })
|
|
639
807
|
return stopResult
|
|
640
808
|
}, [dispatch])
|
|
@@ -653,6 +821,16 @@ export function useAudioRecorder({
|
|
|
653
821
|
return resumeResult
|
|
654
822
|
}, [dispatch])
|
|
655
823
|
|
|
824
|
+
useEffect(() => {
|
|
825
|
+
const subscription = addMaxDurationReachedListener(async (event) => {
|
|
826
|
+
await handleMaxDurationReached(event)
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
return () => {
|
|
830
|
+
subscription.remove()
|
|
831
|
+
}
|
|
832
|
+
}, [handleMaxDurationReached])
|
|
833
|
+
|
|
656
834
|
useEffect(() => {
|
|
657
835
|
let intervalId: ReturnType<typeof setInterval> | undefined
|
|
658
836
|
|
|
@@ -802,5 +980,7 @@ export function useAudioRecorder({
|
|
|
802
980
|
size: state.size,
|
|
803
981
|
compression: state.compression,
|
|
804
982
|
analysisData: state.analysisData,
|
|
983
|
+
maxDurationMs: state.maxDurationMs,
|
|
984
|
+
maxDurationReached: state.maxDurationReached,
|
|
805
985
|
}
|
|
806
986
|
}
|