@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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { RecordingConfig } from '../AudioStudio.types';
|
|
2
|
+
export declare function createNativeRecordingOptions(recordingOptions: RecordingConfig): {
|
|
3
|
+
sampleRate?: import("..").SampleRate;
|
|
4
|
+
channels?: 1 | 2;
|
|
5
|
+
encoding?: import("..").EncodingType;
|
|
6
|
+
interval?: number;
|
|
7
|
+
intervalAnalysis?: number;
|
|
8
|
+
keepAwake?: boolean;
|
|
9
|
+
showNotification?: boolean;
|
|
10
|
+
showWaveformInNotification?: boolean;
|
|
11
|
+
notification?: import("..").NotificationConfig;
|
|
12
|
+
enableProcessing?: boolean;
|
|
13
|
+
ios?: import("..").IOSConfig;
|
|
14
|
+
android?: import("..").AndroidConfig;
|
|
15
|
+
web?: import("..").WebConfig;
|
|
16
|
+
segmentDurationMs?: number;
|
|
17
|
+
features?: import("..").AudioFeaturesOptions;
|
|
18
|
+
output?: import("..").OutputConfig;
|
|
19
|
+
autoResumeAfterInterruption?: boolean;
|
|
20
|
+
maxDurationMs?: number;
|
|
21
|
+
outputDirectory?: string;
|
|
22
|
+
filename?: string;
|
|
23
|
+
deviceId?: string;
|
|
24
|
+
deviceDisconnectionBehavior?: import("..").DeviceDisconnectionBehaviorType;
|
|
25
|
+
bufferDurationSeconds?: number;
|
|
26
|
+
streamFormat?: "float32" | "raw";
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=nativeRecordingOptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nativeRecordingOptions.d.ts","sourceRoot":"","sources":["../../../src/utils/nativeRecordingOptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAGtD,wBAAgB,4BAA4B,CAAC,gBAAgB,EAAE,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;EAgB7E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nativeRecordingOptions.test.d.ts","sourceRoot":"","sources":["../../../src/utils/nativeRecordingOptions.test.ts"],"names":[],"mappings":""}
|
|
@@ -11,6 +11,7 @@ import Accelerate
|
|
|
11
11
|
import UIKit
|
|
12
12
|
import MediaPlayer
|
|
13
13
|
import UserNotifications
|
|
14
|
+
import QuartzCore
|
|
14
15
|
|
|
15
16
|
// Constants
|
|
16
17
|
internal let WAV_HEADER_SIZE: Int64 = 44 // Standard WAV header is 44 bytes
|
|
@@ -48,6 +49,11 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
48
49
|
private var startTime: Date?
|
|
49
50
|
private var totalPausedDuration: TimeInterval = 0 // Track total paused time
|
|
50
51
|
private var currentPauseStart: Date? // Track current pause start
|
|
52
|
+
private var maxDurationTimer: DispatchSourceTimer?
|
|
53
|
+
private var maxDurationTargetMs: Int64 = 0
|
|
54
|
+
private var maxDurationAccumulatedActiveMs: Double = 0
|
|
55
|
+
private var maxDurationSegmentStart: CFTimeInterval?
|
|
56
|
+
private var maxDurationReached: Bool = false
|
|
51
57
|
var isRecording = false
|
|
52
58
|
var isPaused = false
|
|
53
59
|
var isPrepared = false // Add this new state flag
|
|
@@ -690,37 +696,39 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
690
696
|
/// Gets the current status of the recording.
|
|
691
697
|
/// - Returns: A dictionary containing the recording status information.
|
|
692
698
|
func getStatus() -> [String: Any] {
|
|
693
|
-
guard let settings = recordingSettings else {
|
|
694
|
-
print("Recording settings are not available.")
|
|
695
|
-
return [:]
|
|
696
|
-
}
|
|
697
|
-
|
|
698
699
|
let durationInSeconds = currentRecordingDuration()
|
|
699
700
|
let durationInMilliseconds = Int(durationInSeconds * 1000)
|
|
701
|
+
let settings = recordingSettings
|
|
700
702
|
|
|
701
703
|
var status: [String: Any] = [
|
|
702
704
|
"durationMs": durationInMilliseconds,
|
|
703
705
|
"isRecording": isRecording,
|
|
704
706
|
"isPaused": isPaused,
|
|
705
707
|
"mimeType": mimeType,
|
|
706
|
-
"size": totalDataSize
|
|
708
|
+
"size": totalDataSize,
|
|
709
|
+
"maxDurationReached": maxDurationReached
|
|
707
710
|
]
|
|
711
|
+
|
|
712
|
+
let statusMaxDurationMs = settings?.maxDurationMs ?? maxDurationTargetMs
|
|
713
|
+
if statusMaxDurationMs > 0 {
|
|
714
|
+
status["maxDurationMs"] = statusMaxDurationMs
|
|
715
|
+
}
|
|
708
716
|
|
|
709
717
|
// Safely handle optional interval values
|
|
710
|
-
if let interval = settings
|
|
718
|
+
if let interval = settings?.interval {
|
|
711
719
|
status["interval"] = interval
|
|
712
720
|
} else {
|
|
713
721
|
status["interval"] = 1000 // Default value
|
|
714
722
|
}
|
|
715
723
|
|
|
716
|
-
if let intervalAnalysis = settings
|
|
724
|
+
if let intervalAnalysis = settings?.intervalAnalysis {
|
|
717
725
|
status["intervalAnalysis"] = intervalAnalysis
|
|
718
726
|
} else {
|
|
719
727
|
status["intervalAnalysis"] = 500 // Default value
|
|
720
728
|
}
|
|
721
729
|
|
|
722
730
|
// Add compression info if enabled
|
|
723
|
-
if settings
|
|
731
|
+
if settings?.output.compressed.enabled == true,
|
|
724
732
|
let compressedURL = compressedFileURL,
|
|
725
733
|
FileManager.default.fileExists(atPath: compressedURL.path) {
|
|
726
734
|
do {
|
|
@@ -744,6 +752,86 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
744
752
|
return status
|
|
745
753
|
}
|
|
746
754
|
|
|
755
|
+
private func maxDurationActiveMs(now: CFTimeInterval = CACurrentMediaTime()) -> Double {
|
|
756
|
+
if let segmentStart = maxDurationSegmentStart {
|
|
757
|
+
return maxDurationAccumulatedActiveMs + ((now - segmentStart) * 1000)
|
|
758
|
+
}
|
|
759
|
+
return maxDurationAccumulatedActiveMs
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
private func startMaxDurationTimer(settings: RecordingSettings) {
|
|
763
|
+
cancelMaxDurationTimer()
|
|
764
|
+
maxDurationReached = false
|
|
765
|
+
maxDurationTargetMs = settings.maxDurationMs
|
|
766
|
+
maxDurationAccumulatedActiveMs = 0
|
|
767
|
+
maxDurationSegmentStart = nil
|
|
768
|
+
guard settings.maxDurationMs > 0 else { return }
|
|
769
|
+
|
|
770
|
+
maxDurationSegmentStart = CACurrentMediaTime()
|
|
771
|
+
scheduleMaxDurationTimer()
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
private func scheduleMaxDurationTimer() {
|
|
775
|
+
guard maxDurationTargetMs > 0, !maxDurationReached, isRecording, !isPaused else {
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
maxDurationTimer?.cancel()
|
|
780
|
+
let remainingMs = max(0, Double(maxDurationTargetMs) - maxDurationActiveMs())
|
|
781
|
+
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
|
|
782
|
+
timer.schedule(deadline: .now() + .milliseconds(Int(remainingMs.rounded(.up))))
|
|
783
|
+
timer.setEventHandler { [weak self] in
|
|
784
|
+
self?.maxDurationTimer = nil
|
|
785
|
+
self?.emitMaxDurationReached()
|
|
786
|
+
}
|
|
787
|
+
maxDurationTimer = timer
|
|
788
|
+
timer.resume()
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
private func pauseMaxDurationTimer() {
|
|
792
|
+
maxDurationTimer?.cancel()
|
|
793
|
+
maxDurationTimer = nil
|
|
794
|
+
if maxDurationSegmentStart != nil {
|
|
795
|
+
maxDurationAccumulatedActiveMs = maxDurationActiveMs()
|
|
796
|
+
maxDurationSegmentStart = nil
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
private func resumeMaxDurationTimer() {
|
|
801
|
+
guard maxDurationTargetMs > 0, !maxDurationReached else { return }
|
|
802
|
+
maxDurationSegmentStart = CACurrentMediaTime()
|
|
803
|
+
scheduleMaxDurationTimer()
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
private func cancelMaxDurationTimer() {
|
|
807
|
+
maxDurationTimer?.cancel()
|
|
808
|
+
maxDurationTimer = nil
|
|
809
|
+
maxDurationSegmentStart = nil
|
|
810
|
+
if !maxDurationReached {
|
|
811
|
+
maxDurationTargetMs = 0
|
|
812
|
+
maxDurationAccumulatedActiveMs = 0
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
private func emitMaxDurationReached() {
|
|
817
|
+
guard maxDurationTargetMs > 0, !maxDurationReached else { return }
|
|
818
|
+
guard isRecording, !isPaused else { return }
|
|
819
|
+
|
|
820
|
+
let durationMs = Int64(maxDurationActiveMs().rounded())
|
|
821
|
+
maxDurationReached = true
|
|
822
|
+
let shouldAutoStop = recordingSettings?.autoStopOnMaxDuration == true
|
|
823
|
+
delegate?.audioStreamManager(self, didReachMaxDuration: [
|
|
824
|
+
"durationMs": durationMs,
|
|
825
|
+
"maxDurationMs": maxDurationTargetMs,
|
|
826
|
+
"overrunMs": max(Int64(0), durationMs - maxDurationTargetMs),
|
|
827
|
+
"streamUuid": recordingUUID ?? "",
|
|
828
|
+
"autoStopped": shouldAutoStop
|
|
829
|
+
])
|
|
830
|
+
if shouldAutoStop {
|
|
831
|
+
_ = stopRecording()
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
747
835
|
/// Builds compression info dictionary with incremental compressed data for streaming events.
|
|
748
836
|
/// Mirrors the Android pattern (AudioRecorderManager.kt) for consistent cross-platform behavior.
|
|
749
837
|
/// - Returns: A dictionary containing compression metadata and base64-encoded data chunk, or nil if compression is disabled.
|
|
@@ -1179,6 +1267,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1179
1267
|
isRecording = true
|
|
1180
1268
|
isPaused = false
|
|
1181
1269
|
pausedBySystemInterruption = false
|
|
1270
|
+
startMaxDurationTimer(settings: settings)
|
|
1182
1271
|
|
|
1183
1272
|
// Start the audio engine
|
|
1184
1273
|
try audioEngine.start()
|
|
@@ -1220,6 +1309,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1220
1309
|
} catch {
|
|
1221
1310
|
Logger.debug("Error starting audio engine: \(error.localizedDescription)")
|
|
1222
1311
|
isRecording = false
|
|
1312
|
+
cancelMaxDurationTimer()
|
|
1223
1313
|
cleanupPreparation()
|
|
1224
1314
|
return nil
|
|
1225
1315
|
}
|
|
@@ -1231,6 +1321,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1231
1321
|
guard isPrepared && !isRecording else { return }
|
|
1232
1322
|
|
|
1233
1323
|
Logger.debug("Cleaning up prepared resources that weren't used")
|
|
1324
|
+
cancelMaxDurationTimer()
|
|
1234
1325
|
|
|
1235
1326
|
// Remove input tap
|
|
1236
1327
|
audioEngine.inputNode.removeTap(onBus: 0)
|
|
@@ -1296,6 +1387,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1296
1387
|
|
|
1297
1388
|
// Store when we paused
|
|
1298
1389
|
currentPauseStart = Date()
|
|
1390
|
+
pauseMaxDurationTimer()
|
|
1299
1391
|
|
|
1300
1392
|
// Update state
|
|
1301
1393
|
isPaused = true
|
|
@@ -1357,6 +1449,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1357
1449
|
|
|
1358
1450
|
// Clear the stored valid duration
|
|
1359
1451
|
lastValidDuration = nil
|
|
1452
|
+
resumeMaxDurationTimer()
|
|
1360
1453
|
|
|
1361
1454
|
// Reset emission timers to ensure emission starts immediately after resume
|
|
1362
1455
|
lastEmissionTime = Date()
|
|
@@ -1843,6 +1936,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1843
1936
|
|
|
1844
1937
|
// Set stopping flag to prevent race conditions with background/foreground transitions
|
|
1845
1938
|
stopping = true
|
|
1939
|
+
cancelMaxDurationTimer()
|
|
1846
1940
|
|
|
1847
1941
|
Logger.debug("Stopping recording...")
|
|
1848
1942
|
|
|
@@ -12,5 +12,6 @@ protocol AudioStreamManagerDelegate: AnyObject {
|
|
|
12
12
|
func audioStreamManager(_ manager: AudioStreamManager, didResumeRecording resumeTime: Date)
|
|
13
13
|
func audioStreamManager(_ manager: AudioStreamManager, didUpdateNotificationState isPaused: Bool)
|
|
14
14
|
func audioStreamManager(_ manager: AudioStreamManager, didReceiveInterruption info: [String: Any])
|
|
15
|
+
func audioStreamManager(_ manager: AudioStreamManager, didReachMaxDuration info: [String: Any])
|
|
15
16
|
func audioStreamManager(_ manager: AudioStreamManager, didFailWithError error: String)
|
|
16
17
|
}
|
|
@@ -6,6 +6,7 @@ import AVFoundation
|
|
|
6
6
|
private let audioDataEvent: String = "AudioData"
|
|
7
7
|
private let audioAnalysisEvent: String = "AudioAnalysis"
|
|
8
8
|
private let recordingInterruptedEvent: String = "onRecordingInterrupted"
|
|
9
|
+
private let maxDurationReachedEvent: String = "MaxDurationReached"
|
|
9
10
|
private let deviceChangedEvent: String = "deviceChangedEvent"
|
|
10
11
|
private let trimProgressEvent: String = "TrimProgress"
|
|
11
12
|
private let errorEvent: String = "error"
|
|
@@ -48,6 +49,7 @@ public class AudioStudioModule: Module, AudioStreamManagerDelegate, AudioDeviceM
|
|
|
48
49
|
audioDataEvent,
|
|
49
50
|
audioAnalysisEvent,
|
|
50
51
|
recordingInterruptedEvent,
|
|
52
|
+
maxDurationReachedEvent,
|
|
51
53
|
deviceChangedEvent,
|
|
52
54
|
trimProgressEvent,
|
|
53
55
|
errorEvent,
|
|
@@ -1122,6 +1124,10 @@ public class AudioStudioModule: Module, AudioStreamManagerDelegate, AudioDeviceM
|
|
|
1122
1124
|
"timestamp": Date().timeIntervalSince1970 * 1000
|
|
1123
1125
|
])
|
|
1124
1126
|
}
|
|
1127
|
+
|
|
1128
|
+
func audioStreamManager(_ manager: AudioStreamManager, didReachMaxDuration info: [String: Any]) {
|
|
1129
|
+
sendEvent(maxDurationReachedEvent, info)
|
|
1130
|
+
}
|
|
1125
1131
|
|
|
1126
1132
|
func audioStreamManager(_ manager: AudioStreamManager, didPauseRecording pauseTime: Date) {
|
|
1127
1133
|
Logger.debug("AudioStudioModule", "Delegate: didPauseRecording")
|
|
@@ -22,13 +22,13 @@ struct OutputSettings {
|
|
|
22
22
|
var enabled: Bool = true
|
|
23
23
|
var format: String = "wav" // Currently only "wav" is supported
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
struct CompressedOutput {
|
|
27
27
|
var enabled: Bool = false
|
|
28
28
|
var format: String = "aac" // "aac" or "opus" (opus falls back to aac on iOS)
|
|
29
29
|
var bitrate: Int = 128000
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
var primary: PrimaryOutput = PrimaryOutput()
|
|
33
33
|
var compressed: CompressedOutput = CompressedOutput()
|
|
34
34
|
}
|
|
@@ -39,13 +39,13 @@ struct CompressedRecordingInfo {
|
|
|
39
39
|
var bitrate: Int
|
|
40
40
|
var format: String
|
|
41
41
|
var size: Int64 = 0 // Add size with default value
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
static func validate(format: String, bitrate: Int) -> Result<(String, Int), Error> {
|
|
44
44
|
// Validate format
|
|
45
45
|
guard ["aac", "opus"].contains(format.lowercased()) else {
|
|
46
46
|
return .failure(RecordingError.unsupportedFormat(format))
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
// Adjust bitrate based on format
|
|
50
50
|
let adjustedBitrate: Int
|
|
51
51
|
if format.lowercased() == "aac" {
|
|
@@ -57,7 +57,7 @@ struct CompressedRecordingInfo {
|
|
|
57
57
|
// Typical Opus voice bitrates: 8-24 kbps, music: 32-128 kbps
|
|
58
58
|
adjustedBitrate = min(max(bitrate, 8000), 320000)
|
|
59
59
|
}
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
return .success((format, adjustedBitrate))
|
|
62
62
|
}
|
|
63
63
|
}
|
|
@@ -77,7 +77,7 @@ enum RecordingError: Error {
|
|
|
77
77
|
case unsupportedFormat(String)
|
|
78
78
|
case invalidBitrate(Int)
|
|
79
79
|
case invalidOutputDirectory(String)
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
var localizedDescription: String {
|
|
82
82
|
switch self {
|
|
83
83
|
case .unsupportedFormat(let format):
|
|
@@ -98,56 +98,58 @@ struct RecordingSettings {
|
|
|
98
98
|
var bitDepth: Int = 16
|
|
99
99
|
var interval: Int?
|
|
100
100
|
var intervalAnalysis: Int?
|
|
101
|
-
|
|
101
|
+
|
|
102
102
|
// Feature flags
|
|
103
103
|
var keepAwake: Bool = true
|
|
104
104
|
var showNotification: Bool = false
|
|
105
105
|
var enableProcessing: Bool = false
|
|
106
|
-
|
|
106
|
+
|
|
107
107
|
// Remove pointsPerSecond and algorithm
|
|
108
108
|
var featureOptions: [String: Bool]? = ["rms": true, "zcr": true]
|
|
109
|
-
|
|
109
|
+
|
|
110
110
|
// iOS-specific configuration
|
|
111
111
|
var ios: IOSConfig?
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
// Notification configuration
|
|
114
114
|
var notification: NotificationConfig?
|
|
115
|
-
|
|
115
|
+
|
|
116
116
|
// Output configuration
|
|
117
117
|
var output: OutputSettings = OutputSettings()
|
|
118
|
-
|
|
118
|
+
|
|
119
119
|
let autoResumeAfterInterruption: Bool
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
var outputDirectory: String? = nil
|
|
122
122
|
var filename: String? = nil
|
|
123
|
-
|
|
123
|
+
|
|
124
124
|
// Update default to 100ms
|
|
125
125
|
var segmentDurationMs: Int = 100 // Default 100ms segments
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
// Add these new properties
|
|
128
128
|
var deviceId: String?
|
|
129
129
|
var deviceDisconnectionBehavior: DeviceDisconnectionBehavior = .FALLBACK
|
|
130
130
|
var bufferDurationSeconds: Double?
|
|
131
131
|
var streamFormat: String = "raw"
|
|
132
|
-
|
|
132
|
+
var maxDurationMs: Int64 = 0
|
|
133
|
+
var autoStopOnMaxDuration: Bool = false
|
|
134
|
+
|
|
133
135
|
static func fromDictionary(_ dict: [String: Any]) -> Result<RecordingSettings, Error> {
|
|
134
136
|
// Parse output configuration
|
|
135
137
|
var outputSettings = OutputSettings()
|
|
136
|
-
|
|
138
|
+
|
|
137
139
|
if let outputDict = dict["output"] as? [String: Any] {
|
|
138
140
|
// Parse primary output settings
|
|
139
141
|
if let primaryDict = outputDict["primary"] as? [String: Any] {
|
|
140
142
|
outputSettings.primary.enabled = primaryDict["enabled"] as? Bool ?? true
|
|
141
143
|
outputSettings.primary.format = primaryDict["format"] as? String ?? "wav"
|
|
142
144
|
}
|
|
143
|
-
|
|
145
|
+
|
|
144
146
|
// Parse compressed output settings
|
|
145
147
|
if let compressedDict = outputDict["compressed"] as? [String: Any] {
|
|
146
148
|
outputSettings.compressed.enabled = compressedDict["enabled"] as? Bool ?? false
|
|
147
149
|
let format = (compressedDict["format"] as? String)?.lowercased() ?? "aac"
|
|
148
150
|
outputSettings.compressed.format = format
|
|
149
151
|
outputSettings.compressed.bitrate = compressedDict["bitrate"] as? Int ?? 128000
|
|
150
|
-
|
|
152
|
+
|
|
151
153
|
// Validate compression settings if enabled
|
|
152
154
|
if outputSettings.compressed.enabled {
|
|
153
155
|
if case .failure(let error) = CompressedRecordingInfo.validate(
|
|
@@ -159,40 +161,43 @@ struct RecordingSettings {
|
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
|
-
|
|
164
|
+
|
|
163
165
|
// Add extraction of new properties
|
|
164
166
|
let deviceId = dict["deviceId"] as? String
|
|
165
167
|
let deviceDisconnectionBehaviorStr = dict["deviceDisconnectionBehavior"] as? String
|
|
166
|
-
|
|
168
|
+
|
|
167
169
|
// Create settings
|
|
168
170
|
var settings = RecordingSettings(
|
|
169
171
|
sampleRate: dict["sampleRate"] as? Double ?? 44100.0,
|
|
170
172
|
desiredSampleRate: dict["desiredSampleRate"] as? Double ?? 44100.0,
|
|
171
173
|
autoResumeAfterInterruption: dict["autoResumeAfterInterruption"] as? Bool ?? false
|
|
172
174
|
)
|
|
173
|
-
|
|
175
|
+
|
|
174
176
|
settings.output = outputSettings
|
|
175
|
-
|
|
177
|
+
|
|
176
178
|
// Parse core settings
|
|
177
179
|
settings.numberOfChannels = dict["channels"] as? Int ?? 1
|
|
178
180
|
settings.bitDepth = dict["bitDepth"] as? Int ?? 16
|
|
179
181
|
settings.interval = dict["interval"] as? Int
|
|
180
182
|
settings.intervalAnalysis = dict["intervalAnalysis"] as? Int
|
|
181
|
-
|
|
183
|
+
if let maxDurationNumber = dict["maxDurationMs"] as? NSNumber {
|
|
184
|
+
settings.maxDurationMs = maxDurationNumber.int64Value
|
|
185
|
+
}
|
|
186
|
+
settings.autoStopOnMaxDuration = dict["autoStopOnMaxDuration"] as? Bool ?? false
|
|
182
187
|
// Parse feature flags
|
|
183
188
|
settings.keepAwake = dict["keepAwake"] as? Bool ?? true
|
|
184
189
|
settings.showNotification = dict["showNotification"] as? Bool ?? false
|
|
185
190
|
settings.enableProcessing = dict["enableProcessing"] as? Bool ?? false
|
|
186
|
-
|
|
191
|
+
|
|
187
192
|
settings.featureOptions = dict["features"] as? [String: Bool]
|
|
188
|
-
|
|
193
|
+
|
|
189
194
|
// Update segmentDurationMs parsing
|
|
190
195
|
settings.segmentDurationMs = dict["segmentDurationMs"] as? Int ?? 100
|
|
191
|
-
|
|
196
|
+
|
|
192
197
|
// Parse iOS-specific config
|
|
193
198
|
if let iosDict = dict["ios"] as? [String: Any],
|
|
194
199
|
let audioSessionDict = iosDict["audioSession"] as? [String: Any] {
|
|
195
|
-
|
|
200
|
+
|
|
196
201
|
// Map category
|
|
197
202
|
let category: AVAudioSession.Category
|
|
198
203
|
if let categoryStr = audioSessionDict["category"] as? String {
|
|
@@ -208,7 +213,7 @@ struct RecordingSettings {
|
|
|
208
213
|
} else {
|
|
209
214
|
category = .record
|
|
210
215
|
}
|
|
211
|
-
|
|
216
|
+
|
|
212
217
|
// Map mode
|
|
213
218
|
let mode: AVAudioSession.Mode
|
|
214
219
|
if let modeStr = audioSessionDict["mode"] as? String {
|
|
@@ -226,7 +231,7 @@ struct RecordingSettings {
|
|
|
226
231
|
} else {
|
|
227
232
|
mode = .default
|
|
228
233
|
}
|
|
229
|
-
|
|
234
|
+
|
|
230
235
|
// Map category options
|
|
231
236
|
var categoryOptions: AVAudioSession.CategoryOptions = []
|
|
232
237
|
if let optionsArray = audioSessionDict["categoryOptions"] as? [String] {
|
|
@@ -243,63 +248,63 @@ struct RecordingSettings {
|
|
|
243
248
|
}
|
|
244
249
|
}
|
|
245
250
|
}
|
|
246
|
-
|
|
251
|
+
|
|
247
252
|
settings.ios = IOSConfig(audioSession: IOSAudioSessionConfig(
|
|
248
253
|
category: category,
|
|
249
254
|
mode: mode,
|
|
250
255
|
categoryOptions: categoryOptions
|
|
251
256
|
))
|
|
252
257
|
}
|
|
253
|
-
|
|
258
|
+
|
|
254
259
|
// Parse notification config
|
|
255
260
|
if let notificationDict = dict["notification"] as? [String: Any] {
|
|
256
261
|
var notificationConfig = NotificationConfig()
|
|
257
262
|
notificationConfig.title = notificationDict["title"] as? String
|
|
258
263
|
notificationConfig.text = notificationDict["text"] as? String
|
|
259
264
|
notificationConfig.icon = notificationDict["icon"] as? String
|
|
260
|
-
|
|
265
|
+
|
|
261
266
|
// Parse iOS-specific notification config
|
|
262
267
|
if let iosNotificationDict = notificationDict["ios"] as? [String: Any] {
|
|
263
268
|
notificationConfig.ios = IOSNotificationConfig(
|
|
264
269
|
categoryIdentifier: iosNotificationDict["categoryIdentifier"] as? String
|
|
265
270
|
)
|
|
266
271
|
}
|
|
267
|
-
|
|
272
|
+
|
|
268
273
|
settings.notification = notificationConfig
|
|
269
274
|
}
|
|
270
|
-
|
|
275
|
+
|
|
271
276
|
// Parse output settings (they remain nil if not provided)
|
|
272
277
|
if let directory = dict["outputDirectory"] as? String {
|
|
273
278
|
// Only validate if a custom directory is provided
|
|
274
279
|
let fileManager = FileManager.default
|
|
275
280
|
var isDirectory: ObjCBool = false
|
|
276
|
-
|
|
281
|
+
|
|
277
282
|
// Clean up the directory path by removing file:// protocol if present
|
|
278
283
|
let cleanDirectory = directory.replacingOccurrences(of: "file://", with: "")
|
|
279
284
|
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
|
280
285
|
.replacingOccurrences(of: "//", with: "/")
|
|
281
|
-
|
|
286
|
+
|
|
282
287
|
if !fileManager.fileExists(atPath: cleanDirectory, isDirectory: &isDirectory) {
|
|
283
288
|
return .failure(RecordingError.invalidOutputDirectory("Directory does not exist: \(cleanDirectory)"))
|
|
284
289
|
}
|
|
285
|
-
|
|
290
|
+
|
|
286
291
|
if !isDirectory.boolValue {
|
|
287
292
|
return .failure(RecordingError.invalidOutputDirectory("Path is not a directory: \(cleanDirectory)"))
|
|
288
293
|
}
|
|
289
|
-
|
|
294
|
+
|
|
290
295
|
if !fileManager.isWritableFile(atPath: cleanDirectory) {
|
|
291
296
|
return .failure(RecordingError.invalidOutputDirectory("Directory is not writable: \(cleanDirectory)"))
|
|
292
297
|
}
|
|
293
|
-
|
|
298
|
+
|
|
294
299
|
settings.outputDirectory = cleanDirectory
|
|
295
300
|
}
|
|
296
|
-
|
|
301
|
+
|
|
297
302
|
settings.filename = dict["filename"] as? String
|
|
298
|
-
|
|
303
|
+
|
|
299
304
|
// Set new properties
|
|
300
305
|
settings.deviceId = deviceId
|
|
301
306
|
settings.deviceDisconnectionBehavior = DeviceDisconnectionBehavior(rawValue: deviceDisconnectionBehaviorStr ?? "fallback") ?? .FALLBACK
|
|
302
|
-
|
|
307
|
+
|
|
303
308
|
if let bufferDuration = dict["bufferDurationSeconds"] as? Double {
|
|
304
309
|
settings.bufferDurationSeconds = bufferDuration
|
|
305
310
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siteed/audio-studio",
|
|
3
|
-
"version": "3.2.1-beta.
|
|
3
|
+
"version": "3.2.1-beta.2",
|
|
4
4
|
"description": "Comprehensive audio processing library for React Native and Expo with recording, analysis, visualization, and streaming capabilities across iOS, Android, and web",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
package/src/AudioStudio.types.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
AudioFeaturesOptions,
|
|
5
5
|
DecodingConfig,
|
|
6
6
|
} from './AudioAnalysis/AudioAnalysis.types'
|
|
7
|
-
import { AudioAnalysisEvent } from './events'
|
|
7
|
+
import type { AudioAnalysisEvent } from './events'
|
|
8
8
|
|
|
9
9
|
export interface CompressionInfo {
|
|
10
10
|
/** Size of the compressed audio data in bytes */
|
|
@@ -36,6 +36,10 @@ export interface AudioStreamStatus {
|
|
|
36
36
|
mimeType: string
|
|
37
37
|
/** Information about audio compression if enabled */
|
|
38
38
|
compression?: CompressionInfo
|
|
39
|
+
/** Configured maximum active recording duration in milliseconds, if enabled */
|
|
40
|
+
maxDurationMs?: number
|
|
41
|
+
/** Whether the current recording session has reached the configured maximum duration */
|
|
42
|
+
maxDurationReached?: boolean
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
interface AudioDataEventBase {
|
|
@@ -190,6 +194,21 @@ export interface StartRecordingResult {
|
|
|
190
194
|
}
|
|
191
195
|
}
|
|
192
196
|
|
|
197
|
+
export interface MaxDurationReachedEvent {
|
|
198
|
+
/** Active recording duration that triggered the event, in milliseconds */
|
|
199
|
+
durationMs: number
|
|
200
|
+
/** Configured active recording duration limit, in milliseconds */
|
|
201
|
+
maxDurationMs: number
|
|
202
|
+
/** Amount by which timer delivery exceeded the limit, in milliseconds */
|
|
203
|
+
overrunMs: number
|
|
204
|
+
/** Active stream identifier when available */
|
|
205
|
+
streamUuid?: string
|
|
206
|
+
/** Whether the recorder was configured to stop automatically after this event */
|
|
207
|
+
autoStopped: boolean
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export type RecordingStopReason = 'manual' | 'maxDuration'
|
|
211
|
+
|
|
193
212
|
export interface AudioSessionConfig {
|
|
194
213
|
/**
|
|
195
214
|
* Audio session category that defines the audio behavior
|
|
@@ -484,6 +503,43 @@ export interface RecordingConfig {
|
|
|
484
503
|
/** Optional callback to handle recording interruptions */
|
|
485
504
|
onRecordingInterrupted?: (_: RecordingInterruptionEvent) => void
|
|
486
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Maximum cumulative active recording duration, in milliseconds.
|
|
508
|
+
*
|
|
509
|
+
* Paused time does not count. Set to undefined, 0, or a negative value to disable.
|
|
510
|
+
*/
|
|
511
|
+
maxDurationMs?: number
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Stop recording automatically when maxDurationMs is reached.
|
|
515
|
+
*
|
|
516
|
+
* Defaults to false. When used with `useAudioRecorder`, the
|
|
517
|
+
* MaxDurationReached event is emitted immediately, then the hook stops the
|
|
518
|
+
* recorder and exposes the final result through `onRecordingStopped`.
|
|
519
|
+
*/
|
|
520
|
+
autoStopOnMaxDuration?: boolean
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Optional callback invoked when maxDurationMs is reached.
|
|
524
|
+
*
|
|
525
|
+
* This remains an immediate threshold callback. If
|
|
526
|
+
* autoStopOnMaxDuration is true, use `onRecordingStopped` for the full
|
|
527
|
+
* recording result after stop completes.
|
|
528
|
+
*/
|
|
529
|
+
onMaxDurationReached?: (_: MaxDurationReachedEvent) => void
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Optional callback invoked after a recording has fully stopped and the
|
|
533
|
+
* final `AudioRecording` result is available.
|
|
534
|
+
*
|
|
535
|
+
* The reason is `manual` when stopped through `stopRecording()` and
|
|
536
|
+
* `maxDuration` when stopped by `autoStopOnMaxDuration`.
|
|
537
|
+
*/
|
|
538
|
+
onRecordingStopped?: (
|
|
539
|
+
recording: AudioRecording,
|
|
540
|
+
reason: RecordingStopReason
|
|
541
|
+
) => void | Promise<void>
|
|
542
|
+
|
|
487
543
|
/** Optional directory path where output files will be saved */
|
|
488
544
|
outputDirectory?: string // If not provided, uses default app directory
|
|
489
545
|
/** Optional filename for the recording (uses UUID if not provided) */
|
|
@@ -710,10 +766,23 @@ export interface UseAudioRecorderState {
|
|
|
710
766
|
size: number // Size in bytes of the recorded audio
|
|
711
767
|
/** Information about compression if enabled */
|
|
712
768
|
compression?: CompressionInfo
|
|
769
|
+
/** Configured maximum active recording duration in milliseconds, if enabled */
|
|
770
|
+
maxDurationMs?: number
|
|
771
|
+
/** Whether the current recording session has reached the configured maximum duration */
|
|
772
|
+
maxDurationReached?: boolean
|
|
713
773
|
/** Analysis data for the recording if processing was enabled */
|
|
714
774
|
analysisData?: AudioAnalysis // Analysis data for the recording depending on enableProcessing flag
|
|
775
|
+
/** Reason associated with the last completed recording */
|
|
776
|
+
lastRecordingReason?: RecordingStopReason
|
|
715
777
|
/** Optional callback to handle recording interruptions */
|
|
716
778
|
onRecordingInterrupted?: (_: RecordingInterruptionEvent) => void
|
|
779
|
+
/** Optional callback invoked when maxDurationMs is reached */
|
|
780
|
+
onMaxDurationReached?: (_: MaxDurationReachedEvent) => void
|
|
781
|
+
/** Optional callback invoked when a recording fully stops */
|
|
782
|
+
onRecordingStopped?: (
|
|
783
|
+
recording: AudioRecording,
|
|
784
|
+
reason: RecordingStopReason
|
|
785
|
+
) => void | Promise<void>
|
|
717
786
|
}
|
|
718
787
|
|
|
719
788
|
/**
|