@siteed/audio-studio 3.1.1 → 3.2.0-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.
Files changed (37) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/android/src/main/java/net/siteed/audiostudio/AudioStreamDecoder.kt +640 -0
  3. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +134 -3
  4. package/android/src/main/java/net/siteed/audiostudio/Constants.kt +4 -0
  5. package/build/cjs/errors/AudioStreamError.js +152 -0
  6. package/build/cjs/errors/AudioStreamError.js.map +1 -0
  7. package/build/cjs/errors/AudioStreamError.test.js +61 -0
  8. package/build/cjs/errors/AudioStreamError.test.js.map +1 -0
  9. package/build/cjs/index.js +7 -1
  10. package/build/cjs/index.js.map +1 -1
  11. package/build/cjs/streamAudioData.js +467 -0
  12. package/build/cjs/streamAudioData.js.map +1 -0
  13. package/build/esm/errors/AudioStreamError.js +147 -0
  14. package/build/esm/errors/AudioStreamError.js.map +1 -0
  15. package/build/esm/errors/AudioStreamError.test.js +59 -0
  16. package/build/esm/errors/AudioStreamError.test.js.map +1 -0
  17. package/build/esm/index.js +3 -1
  18. package/build/esm/index.js.map +1 -1
  19. package/build/esm/streamAudioData.js +460 -0
  20. package/build/esm/streamAudioData.js.map +1 -0
  21. package/build/types/errors/AudioStreamError.d.ts +25 -0
  22. package/build/types/errors/AudioStreamError.d.ts.map +1 -0
  23. package/build/types/errors/AudioStreamError.test.d.ts +2 -0
  24. package/build/types/errors/AudioStreamError.test.d.ts.map +1 -0
  25. package/build/types/index.d.ts +5 -1
  26. package/build/types/index.d.ts.map +1 -1
  27. package/build/types/streamAudioData.d.ts +114 -0
  28. package/build/types/streamAudioData.d.ts.map +1 -0
  29. package/ios/AudioProcessingHelpers.swift +10 -5
  30. package/ios/AudioStreamDecoder.swift +523 -0
  31. package/ios/AudioStudioModule.swift +147 -3
  32. package/ios/AudioStudioTests/AudioStreamDecoderTests.swift +128 -0
  33. package/package.json +1 -1
  34. package/src/errors/AudioStreamError.test.ts +65 -0
  35. package/src/errors/AudioStreamError.ts +185 -0
  36. package/src/index.ts +24 -0
  37. package/src/streamAudioData.ts +654 -0
@@ -9,6 +9,10 @@ private let recordingInterruptedEvent: String = "onRecordingInterrupted"
9
9
  private let deviceChangedEvent: String = "deviceChangedEvent"
10
10
  private let trimProgressEvent: String = "TrimProgress"
11
11
  private let errorEvent: String = "error"
12
+ private let audioStreamChunkEvent: String = "AudioDataStreamChunk"
13
+ private let audioStreamProgressEvent: String = "AudioDataStreamProgress"
14
+ private let audioStreamCompleteEvent: String = "AudioDataStreamComplete"
15
+ private let audioStreamErrorEvent: String = "AudioDataStreamError"
12
16
  private let DEFAULT_SEGMENT_DURATION_MS = 100
13
17
  private let audioDeviceTypeBuiltinMic = "builtin_mic"
14
18
  private let audioDeviceTypeBluetooth = "bluetooth"
@@ -18,13 +22,16 @@ private let audioDeviceTypeWiredHeadphones = "wired_headphones"
18
22
  private let audioDeviceTypeSpeaker = "speaker"
19
23
  private let audioDeviceTypeUnknown = "unknown"
20
24
 
21
- public class AudioStudioModule: Module, AudioStreamManagerDelegate, AudioDeviceManagerDelegate {
25
+ public class AudioStudioModule: Module, AudioStreamManagerDelegate, AudioDeviceManagerDelegate, AudioStreamDecoderDelegate {
22
26
  private var streamManager = AudioStreamManager()
23
27
  private let notificationCenter = UNUserNotificationCenter.current()
24
28
  private let notificationIdentifier = "audio_recording_notification"
25
29
  private var deviceManager = AudioDeviceManager()
26
30
  private var deviceChangeObserver: Any?
27
31
 
32
+ private let streamDecodersLock = NSLock()
33
+ private var streamDecoders: [String: AudioStreamDecoder] = [:]
34
+
28
35
  // Serial queue for AVAudioEngine lifecycle ops (prepare/start/stop).
29
36
  // Prevents concurrent mutation of shared engine state and keeps callers
30
37
  // off the main thread to avoid UI freezes during heavy native init.
@@ -43,7 +50,11 @@ public class AudioStudioModule: Module, AudioStreamManagerDelegate, AudioDeviceM
43
50
  recordingInterruptedEvent,
44
51
  deviceChangedEvent,
45
52
  trimProgressEvent,
46
- errorEvent
53
+ errorEvent,
54
+ audioStreamChunkEvent,
55
+ audioStreamProgressEvent,
56
+ audioStreamCompleteEvent,
57
+ audioStreamErrorEvent
47
58
  ])
48
59
 
49
60
  OnCreate {
@@ -901,8 +912,141 @@ public class AudioStudioModule: Module, AudioStreamManagerDelegate, AudioDeviceM
901
912
  }
902
913
  }
903
914
  }
915
+
916
+ AsyncFunction("streamAudioData") { (options: [String: Any], promise: Promise) in
917
+ guard let requestId = options["requestId"] as? String,
918
+ let fileUri = options["fileUri"] as? String else {
919
+ promise.reject(
920
+ "ERR_AUDIO_STREAM_INVALID_RANGE",
921
+ "fileUri and requestId are required"
922
+ )
923
+ return
924
+ }
925
+
926
+ let streamFormat = options["streamFormat"] as? String ?? "float32"
927
+ guard streamFormat == "float32" else {
928
+ promise.reject(
929
+ "ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT",
930
+ "Only streamFormat='float32' is supported"
931
+ )
932
+ return
933
+ }
934
+
935
+ let opts = AudioStreamDecoder.Options(
936
+ requestId: requestId,
937
+ fileUri: fileUri,
938
+ startTimeMs: options["startTimeMs"] as? Double,
939
+ endTimeMs: options["endTimeMs"] as? Double,
940
+ targetSampleRate: options["targetSampleRate"] as? Double
941
+ ?? (options["sampleRate"] as? Double),
942
+ channels: options["channels"] as? Int,
943
+ normalizeAudio: options["normalizeAudio"] as? Bool ?? true,
944
+ chunkDurationMs: options["chunkDurationMs"] as? Int ?? 1000,
945
+ maxChunkBytes: options["maxChunkBytes"] as? Int,
946
+ maxBufferedChunks: options["maxBufferedChunks"] as? Int ?? 4
947
+ )
948
+
949
+ let decoder = AudioStreamDecoder(options: opts)
950
+ decoder.delegate = self
951
+ self.streamDecodersLock.lock()
952
+ if self.streamDecoders[requestId] != nil {
953
+ self.streamDecodersLock.unlock()
954
+ promise.reject(
955
+ "ERR_AUDIO_STREAM_BUSY",
956
+ "requestId already in use"
957
+ )
958
+ return
959
+ }
960
+ self.streamDecoders[requestId] = decoder
961
+ self.streamDecodersLock.unlock()
962
+ decoder.start()
963
+ promise.resolve(["requestId": requestId])
964
+ }
965
+
966
+ AsyncFunction("cancelStreamAudioData") { (requestId: String, promise: Promise) in
967
+ self.streamDecodersLock.lock()
968
+ let decoder = self.streamDecoders[requestId]
969
+ self.streamDecodersLock.unlock()
970
+ decoder?.cancel()
971
+ promise.resolve(["requestId": requestId, "cancelled": decoder != nil])
972
+ }
973
+
974
+ Function("acknowledgeStreamAudioChunk") { (requestId: String, chunkIndex: Int) in
975
+ self.streamDecodersLock.lock()
976
+ let decoder = self.streamDecoders[requestId]
977
+ self.streamDecodersLock.unlock()
978
+ decoder?.acknowledgeChunk(chunkIndex)
979
+ }
980
+
981
+ AsyncFunction("getAudioDecodeCapabilities") { (promise: Promise) in
982
+ promise.resolve([
983
+ "platform": "ios",
984
+ "supportedInputFormats": [
985
+ "audio/wav",
986
+ "audio/aac",
987
+ "audio/mp4",
988
+ "audio/mpeg",
989
+ "audio/x-m4a",
990
+ "audio/caf",
991
+ "audio/aiff",
992
+ ],
993
+ "supportedOutputFormats": ["float32"],
994
+ "supportsCancellation": true,
995
+ "supportsBackpressure": true,
996
+ "supportsTimeRange": true,
997
+ "supportsTargetSampleRate": true,
998
+ "supportsChannelMixing": true,
999
+ "knownLimitations": [
1000
+ "Opus/WebM input depends on AVFoundation codec availability for the iOS version."
1001
+ ],
1002
+ ])
1003
+ }
904
1004
  }
905
-
1005
+
1006
+ private func releaseStreamDecoder(_ requestId: String) {
1007
+ streamDecodersLock.lock()
1008
+ streamDecoders.removeValue(forKey: requestId)
1009
+ streamDecodersLock.unlock()
1010
+ }
1011
+
1012
+ // MARK: - AudioStreamDecoderDelegate
1013
+
1014
+ public func streamDecoder(
1015
+ _ decoder: AudioStreamDecoder,
1016
+ didEmitChunk payload: [String: Any]
1017
+ ) {
1018
+ sendEvent(audioStreamChunkEvent, payload)
1019
+ }
1020
+
1021
+ public func streamDecoder(
1022
+ _ decoder: AudioStreamDecoder,
1023
+ didReportProgress payload: [String: Any]
1024
+ ) {
1025
+ sendEvent(audioStreamProgressEvent, payload)
1026
+ }
1027
+
1028
+ public func streamDecoder(
1029
+ _ decoder: AudioStreamDecoder,
1030
+ didCompleteWith payload: [String: Any]
1031
+ ) {
1032
+ if let requestId = payload["requestId"] as? String {
1033
+ releaseStreamDecoder(requestId)
1034
+ }
1035
+ sendEvent(audioStreamCompleteEvent, payload)
1036
+ }
1037
+
1038
+ public func streamDecoder(
1039
+ _ decoder: AudioStreamDecoder,
1040
+ didFailWith payload: [String: Any]
1041
+ ) {
1042
+ if let requestId = payload["requestId"] as? String,
1043
+ let code = payload["code"] as? String,
1044
+ code != "ERR_AUDIO_STREAM_CANCELLED" {
1045
+ releaseStreamDecoder(requestId)
1046
+ }
1047
+ sendEvent(audioStreamErrorEvent, payload)
1048
+ }
1049
+
906
1050
  func audioStreamManager(_ manager: AudioStreamManager, didReceiveInterruption info: [String: Any]) {
907
1051
  Logger.debug("AudioStudioModule", "Delegate: didReceiveInterruption: \(info)")
908
1052
  // Convert iOS interruption events to match the TypeScript types
@@ -0,0 +1,128 @@
1
+ import XCTest
2
+ @testable import AudioStudio
3
+
4
+ final class AudioStreamDecoderTests: XCTestCase {
5
+
6
+ // MARK: - Sample sanitization
7
+
8
+ func testSafeFloatToInt16ReplacesNonFinite() {
9
+ XCTAssertEqual(safeFloatToInt16(Float.nan), 0)
10
+ XCTAssertEqual(safeFloatToInt16(Float.infinity), Int16.max)
11
+ XCTAssertEqual(safeFloatToInt16(-Float.infinity), Int16.min)
12
+ }
13
+
14
+ func testSafeFloatToInt16ClampsOutOfRange() {
15
+ XCTAssertEqual(safeFloatToInt16(2.0), Int16.max)
16
+ XCTAssertEqual(safeFloatToInt16(-2.0), Int16.min)
17
+ XCTAssertEqual(safeFloatToInt16(0.0), 0)
18
+ }
19
+
20
+ func testSafeFloatToInt16IdentityAtUnityIsBounded() {
21
+ // The previous Swift `Int16(1.0 * Float(Int16.max))` trap requires
22
+ // the result of the multiplication to fit Int16. The new helper
23
+ // must produce Int16.max for sample == 1.0 without trapping.
24
+ XCTAssertEqual(safeFloatToInt16(1.0), Int16.max)
25
+ XCTAssertEqual(safeFloatToInt16(-1.0), -Int16.max)
26
+ }
27
+
28
+ func testSafeFloatToInt32ReplacesNonFinite() {
29
+ XCTAssertEqual(safeFloatToInt32(Float.nan), 0)
30
+ XCTAssertEqual(safeFloatToInt32(Float.infinity), Int32.max)
31
+ XCTAssertEqual(safeFloatToInt32(-Float.infinity), Int32.min)
32
+ }
33
+
34
+ func testSafeFloatToInt32ClampsOutOfRange() {
35
+ XCTAssertEqual(safeFloatToInt32(5.0), Int32.max)
36
+ XCTAssertEqual(safeFloatToInt32(-5.0), Int32.min)
37
+ }
38
+
39
+ // MARK: - Decoder option bounds
40
+
41
+ func testDecoderOptionsClampsChunkDuration() {
42
+ let opts = AudioStreamDecoder.Options(
43
+ requestId: "test",
44
+ fileUri: "/dev/null",
45
+ startTimeMs: nil,
46
+ endTimeMs: nil,
47
+ targetSampleRate: nil,
48
+ channels: nil,
49
+ normalizeAudio: true,
50
+ chunkDurationMs: 5,
51
+ maxChunkBytes: nil,
52
+ maxBufferedChunks: 0
53
+ )
54
+ XCTAssertEqual(opts.chunkDurationMs, 10, "chunkDurationMs floor is 10ms")
55
+ XCTAssertEqual(opts.maxBufferedChunks, 1, "maxBufferedChunks floor is 1")
56
+
57
+ let bigOpts = AudioStreamDecoder.Options(
58
+ requestId: "big",
59
+ fileUri: "/dev/null",
60
+ startTimeMs: nil,
61
+ endTimeMs: nil,
62
+ targetSampleRate: nil,
63
+ channels: nil,
64
+ normalizeAudio: true,
65
+ chunkDurationMs: 999_999,
66
+ maxChunkBytes: nil,
67
+ maxBufferedChunks: 99
68
+ )
69
+ XCTAssertEqual(bigOpts.chunkDurationMs, 60_000, "chunkDurationMs ceiling is 60s")
70
+ }
71
+
72
+ // MARK: - Decoder event contract
73
+
74
+ final class CaptureDelegate: AudioStreamDecoderDelegate {
75
+ var chunks: [[String: Any]] = []
76
+ var progressEvents: [[String: Any]] = []
77
+ var completePayload: [String: Any]?
78
+ var errorPayload: [String: Any]?
79
+ let done = XCTestExpectation(description: "decoder terminal event")
80
+
81
+ func streamDecoder(_ decoder: AudioStreamDecoder, didEmitChunk payload: [String: Any]) {
82
+ chunks.append(payload)
83
+ if let idx = payload["chunkIndex"] as? Int {
84
+ decoder.acknowledgeChunk(idx)
85
+ }
86
+ }
87
+ func streamDecoder(_ decoder: AudioStreamDecoder, didReportProgress payload: [String: Any]) {
88
+ progressEvents.append(payload)
89
+ }
90
+ func streamDecoder(_ decoder: AudioStreamDecoder, didCompleteWith payload: [String: Any]) {
91
+ completePayload = payload
92
+ done.fulfill()
93
+ }
94
+ func streamDecoder(_ decoder: AudioStreamDecoder, didFailWith payload: [String: Any]) {
95
+ errorPayload = payload
96
+ // Some flows emit an error then a complete; let complete fulfill.
97
+ }
98
+ }
99
+
100
+ func testDecoderEmitsFileNotFoundForMissingPath() {
101
+ let delegate = CaptureDelegate()
102
+ let opts = AudioStreamDecoder.Options(
103
+ requestId: "missing",
104
+ fileUri: "/tmp/this-file-does-not-exist-\(UUID().uuidString).wav",
105
+ startTimeMs: nil,
106
+ endTimeMs: nil,
107
+ targetSampleRate: nil,
108
+ channels: nil,
109
+ normalizeAudio: true,
110
+ chunkDurationMs: 100,
111
+ maxChunkBytes: nil,
112
+ maxBufferedChunks: 2
113
+ )
114
+ let decoder = AudioStreamDecoder(options: opts)
115
+ decoder.delegate = delegate
116
+ decoder.start()
117
+ // Error path never calls complete, so wait directly on the error.
118
+ let exp = XCTestExpectation(description: "error received")
119
+ DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
120
+ if delegate.errorPayload != nil {
121
+ exp.fulfill()
122
+ }
123
+ }
124
+ wait(for: [exp], timeout: 2.0)
125
+ XCTAssertEqual(delegate.errorPayload?["code"] as? String, "ERR_AUDIO_STREAM_FILE_NOT_FOUND")
126
+ XCTAssertEqual(delegate.errorPayload?["requestId"] as? String, "missing")
127
+ }
128
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteed/audio-studio",
3
- "version": "3.1.1",
3
+ "version": "3.2.0-beta.1",
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",
@@ -0,0 +1,65 @@
1
+ import { AudioStreamError, mapStreamError } from './AudioStreamError'
2
+
3
+ describe('AudioStreamError', () => {
4
+ it('passes through an existing AudioStreamError unchanged', () => {
5
+ const original = new AudioStreamError({
6
+ code: 'ERR_AUDIO_STREAM_CANCELLED',
7
+ message: 'aborted',
8
+ recoverable: true,
9
+ })
10
+ expect(mapStreamError(original)).toBe(original)
11
+ })
12
+
13
+ it('maps native FILE_NOT_FOUND code', () => {
14
+ const mapped = mapStreamError({ code: 'FILE_NOT_FOUND', message: 'gone' })
15
+ expect(mapped.code).toBe('ERR_AUDIO_STREAM_FILE_NOT_FOUND')
16
+ expect(mapped.recoverable).toBe(false)
17
+ })
18
+
19
+ it('maps unsupported codec text', () => {
20
+ const mapped = mapStreamError(new Error('No suitable codec for audio/opus'))
21
+ expect(mapped.code).toBe('ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT')
22
+ })
23
+
24
+ it('marks cancellation as recoverable', () => {
25
+ const mapped = mapStreamError({
26
+ code: 'ERR_AUDIO_STREAM_CANCELLED',
27
+ message: 'user cancelled',
28
+ })
29
+ expect(mapped.code).toBe('ERR_AUDIO_STREAM_CANCELLED')
30
+ expect(mapped.recoverable).toBe(true)
31
+ })
32
+
33
+ it('falls back to UNKNOWN', () => {
34
+ const mapped = mapStreamError({})
35
+ expect(mapped.code).toBe('ERR_AUDIO_STREAM_UNKNOWN')
36
+ })
37
+
38
+ it('preserves nativeCode and nativeMessage', () => {
39
+ const mapped = mapStreamError({
40
+ code: 'WEIRD_NATIVE_CODE',
41
+ message: 'something went wrong on the bridge',
42
+ })
43
+ expect(mapped.nativeCode).toBe('WEIRD_NATIVE_CODE')
44
+ expect(mapped.nativeMessage).toContain('bridge')
45
+ })
46
+
47
+ it('serialises to a stable JSON payload', () => {
48
+ const err = new AudioStreamError({
49
+ code: 'ERR_AUDIO_STREAM_DECODE_FAILED',
50
+ message: 'decoder bust',
51
+ recoverable: false,
52
+ fileUri: 'file:///a.m4a',
53
+ platform: 'ios',
54
+ })
55
+ expect(err.toJSON()).toEqual({
56
+ code: 'ERR_AUDIO_STREAM_DECODE_FAILED',
57
+ message: 'decoder bust',
58
+ recoverable: false,
59
+ fileUri: 'file:///a.m4a',
60
+ platform: 'ios',
61
+ nativeCode: undefined,
62
+ nativeMessage: undefined,
63
+ })
64
+ })
65
+ })
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Stable typed errors for `streamAudioData`. Callers can switch on `code`.
3
+ */
4
+ export type AudioStreamErrorCode =
5
+ | 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT'
6
+ | 'ERR_AUDIO_STREAM_INVALID_RANGE'
7
+ | 'ERR_AUDIO_STREAM_DECODE_FAILED'
8
+ | 'ERR_AUDIO_STREAM_CANCELLED'
9
+ | 'ERR_AUDIO_STREAM_PERMISSION_DENIED'
10
+ | 'ERR_AUDIO_STREAM_FILE_NOT_FOUND'
11
+ | 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT'
12
+ | 'ERR_AUDIO_STREAM_NATIVE_UNAVAILABLE'
13
+ | 'ERR_AUDIO_STREAM_BUSY'
14
+ | 'ERR_AUDIO_STREAM_UNKNOWN'
15
+
16
+ export interface AudioStreamErrorPayload {
17
+ code: AudioStreamErrorCode
18
+ message: string
19
+ recoverable: boolean
20
+ fileUri?: string
21
+ platform?: string
22
+ nativeCode?: string
23
+ nativeMessage?: string
24
+ }
25
+
26
+ const RECOVERABLE: AudioStreamErrorCode[] = [
27
+ 'ERR_AUDIO_STREAM_CANCELLED',
28
+ 'ERR_AUDIO_STREAM_BUSY',
29
+ 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT',
30
+ 'ERR_AUDIO_STREAM_PERMISSION_DENIED',
31
+ ]
32
+
33
+ export class AudioStreamError extends Error {
34
+ readonly code: AudioStreamErrorCode
35
+ readonly recoverable: boolean
36
+ readonly fileUri?: string
37
+ readonly platform?: string
38
+ readonly nativeCode?: string
39
+ readonly nativeMessage?: string
40
+
41
+ constructor(payload: AudioStreamErrorPayload) {
42
+ super(payload.message)
43
+ this.name = 'AudioStreamError'
44
+ this.code = payload.code
45
+ this.recoverable = payload.recoverable
46
+ this.fileUri = payload.fileUri
47
+ this.platform = payload.platform
48
+ this.nativeCode = payload.nativeCode
49
+ this.nativeMessage = payload.nativeMessage
50
+ }
51
+
52
+ toJSON(): AudioStreamErrorPayload {
53
+ return {
54
+ code: this.code,
55
+ message: this.message,
56
+ recoverable: this.recoverable,
57
+ fileUri: this.fileUri,
58
+ platform: this.platform,
59
+ nativeCode: this.nativeCode,
60
+ nativeMessage: this.nativeMessage,
61
+ }
62
+ }
63
+ }
64
+
65
+ function getNativeMessage(err: unknown): string {
66
+ if (err instanceof Error) return err.message
67
+ if (typeof err === 'string') return err
68
+ try {
69
+ return JSON.stringify(err) ?? String(err)
70
+ } catch {
71
+ return String(err)
72
+ }
73
+ }
74
+
75
+ function getNativeCode(err: unknown): string | undefined {
76
+ if (err && typeof err === 'object' && 'code' in err) {
77
+ const code = (err as { code?: unknown }).code
78
+ if (typeof code === 'string') return code
79
+ }
80
+ return undefined
81
+ }
82
+
83
+ function normalizeCode(raw: string | undefined): AudioStreamErrorCode | null {
84
+ if (!raw) return null
85
+ const upper = raw.toUpperCase()
86
+ if (upper.startsWith('ERR_AUDIO_STREAM_')) {
87
+ const known: AudioStreamErrorCode[] = [
88
+ 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT',
89
+ 'ERR_AUDIO_STREAM_INVALID_RANGE',
90
+ 'ERR_AUDIO_STREAM_DECODE_FAILED',
91
+ 'ERR_AUDIO_STREAM_CANCELLED',
92
+ 'ERR_AUDIO_STREAM_PERMISSION_DENIED',
93
+ 'ERR_AUDIO_STREAM_FILE_NOT_FOUND',
94
+ 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT',
95
+ 'ERR_AUDIO_STREAM_NATIVE_UNAVAILABLE',
96
+ 'ERR_AUDIO_STREAM_BUSY',
97
+ 'ERR_AUDIO_STREAM_UNKNOWN',
98
+ ]
99
+ if ((known as string[]).includes(upper)) {
100
+ return upper as AudioStreamErrorCode
101
+ }
102
+ }
103
+ if (upper.includes('FILE_NOT_FOUND') || upper === 'ENOENT') {
104
+ return 'ERR_AUDIO_STREAM_FILE_NOT_FOUND'
105
+ }
106
+ if (upper.includes('PERMISSION') || upper === 'EACCES') {
107
+ return 'ERR_AUDIO_STREAM_PERMISSION_DENIED'
108
+ }
109
+ if (
110
+ upper.includes('UNSUPPORTED') ||
111
+ upper.includes('NO_SUITABLE_CODEC') ||
112
+ upper.includes('NO SUITABLE CODEC') ||
113
+ upper.includes('NOT SUPPORTED')
114
+ ) {
115
+ return 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT'
116
+ }
117
+ if (
118
+ upper.includes('INVALID_RANGE') ||
119
+ upper.includes('OUT_OF_RANGE') ||
120
+ upper.includes('INVALID_TIME')
121
+ ) {
122
+ return 'ERR_AUDIO_STREAM_INVALID_RANGE'
123
+ }
124
+ if (upper.includes('CANCELLED') || upper.includes('CANCELED')) {
125
+ return 'ERR_AUDIO_STREAM_CANCELLED'
126
+ }
127
+ if (upper.includes('BUSY')) {
128
+ return 'ERR_AUDIO_STREAM_BUSY'
129
+ }
130
+ if (upper.includes('BACKPRESSURE')) {
131
+ return 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT'
132
+ }
133
+ if (
134
+ upper.includes('DECODE') ||
135
+ upper.includes('CODEC') ||
136
+ upper.includes('MALFORMED')
137
+ ) {
138
+ return 'ERR_AUDIO_STREAM_DECODE_FAILED'
139
+ }
140
+ return null
141
+ }
142
+
143
+ export function mapStreamError(
144
+ err: unknown,
145
+ fileUri?: string,
146
+ platform?: string
147
+ ): AudioStreamError {
148
+ if (err instanceof AudioStreamError) return err
149
+
150
+ const nativeMessage = getNativeMessage(err)
151
+ const nativeCode = getNativeCode(err)
152
+ const lower = nativeMessage.toLowerCase()
153
+
154
+ let code =
155
+ normalizeCode(nativeCode) ??
156
+ normalizeCode(nativeMessage) ??
157
+ 'ERR_AUDIO_STREAM_UNKNOWN'
158
+
159
+ if (code === 'ERR_AUDIO_STREAM_UNKNOWN') {
160
+ if (lower.includes('not found') || lower.includes('does not exist')) {
161
+ code = 'ERR_AUDIO_STREAM_FILE_NOT_FOUND'
162
+ } else if (
163
+ lower.includes('unsupported') ||
164
+ lower.includes('no suitable codec')
165
+ ) {
166
+ code = 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT'
167
+ } else if (lower.includes('permission') || lower.includes('denied')) {
168
+ code = 'ERR_AUDIO_STREAM_PERMISSION_DENIED'
169
+ } else if (lower.includes('decode') || lower.includes('codec')) {
170
+ code = 'ERR_AUDIO_STREAM_DECODE_FAILED'
171
+ } else if (lower.includes('cancel')) {
172
+ code = 'ERR_AUDIO_STREAM_CANCELLED'
173
+ }
174
+ }
175
+
176
+ return new AudioStreamError({
177
+ code,
178
+ message: `Audio stream failed (${code}): ${nativeMessage}`,
179
+ recoverable: RECOVERABLE.includes(code),
180
+ fileUri,
181
+ platform,
182
+ nativeCode,
183
+ nativeMessage,
184
+ })
185
+ }
package/src/index.ts CHANGED
@@ -19,6 +19,10 @@ import {
19
19
  useSharedAudioRecorder,
20
20
  } from './AudioRecorder.provider'
21
21
  import AudioStudioModule from './AudioStudioModule'
22
+ import {
23
+ getAudioDecodeCapabilities,
24
+ streamAudioData,
25
+ } from './streamAudioData'
22
26
  import { trimAudio } from './trimAudio'
23
27
  import { useAudioRecorder } from './useAudioRecorder'
24
28
 
@@ -54,6 +58,8 @@ export {
54
58
  extractPreview,
55
59
  trimAudio,
56
60
  extractAudioData,
61
+ streamAudioData,
62
+ getAudioDecodeCapabilities,
57
63
  extractMelSpectrogram,
58
64
  initMelStreamingWasm,
59
65
  computeMelFrameWasm,
@@ -71,6 +77,24 @@ export type {
71
77
  AudioExtractionErrorPayload,
72
78
  } from './errors/AudioExtractionError'
73
79
 
80
+ export {
81
+ AudioStreamError,
82
+ mapStreamError,
83
+ } from './errors/AudioStreamError'
84
+ export type {
85
+ AudioStreamErrorCode,
86
+ AudioStreamErrorPayload,
87
+ } from './errors/AudioStreamError'
88
+
89
+ export type {
90
+ StreamAudioDataOptions,
91
+ StreamAudioDataChunk,
92
+ StreamAudioDataProgress,
93
+ StreamAudioDataResult,
94
+ StreamAudioDataCallbacks,
95
+ AudioDecodeCapabilities,
96
+ } from './streamAudioData'
97
+
74
98
  // Export all types
75
99
  export type * from './AudioAnalysis/AudioAnalysis.types'
76
100
  export type * from './AudioStudio.types'