@siteed/audio-studio 3.1.1 → 3.2.0

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 (96) hide show
  1. package/CHANGELOG.md +375 -4
  2. package/android/src/main/java/net/siteed/audiostudio/AudioStreamDecoder.kt +852 -0
  3. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +167 -3
  4. package/android/src/main/java/net/siteed/audiostudio/Constants.kt +4 -0
  5. package/build/cjs/errors/AudioStreamError.js +161 -0
  6. package/build/cjs/errors/AudioStreamError.js.map +1 -0
  7. package/build/cjs/errors/AudioStreamError.test.js +82 -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 +534 -0
  12. package/build/cjs/streamAudioData.js.map +1 -0
  13. package/build/cjs/utils/audioProcessing.js +14 -10
  14. package/build/cjs/utils/audioProcessing.js.map +1 -1
  15. package/build/esm/errors/AudioStreamError.js +156 -0
  16. package/build/esm/errors/AudioStreamError.js.map +1 -0
  17. package/build/esm/errors/AudioStreamError.test.js +80 -0
  18. package/build/esm/errors/AudioStreamError.test.js.map +1 -0
  19. package/build/esm/index.js +3 -1
  20. package/build/esm/index.js.map +1 -1
  21. package/build/esm/streamAudioData.js +527 -0
  22. package/build/esm/streamAudioData.js.map +1 -0
  23. package/build/esm/utils/audioProcessing.js +14 -10
  24. package/build/esm/utils/audioProcessing.js.map +1 -1
  25. package/build/types/errors/AudioStreamError.d.ts +25 -0
  26. package/build/types/errors/AudioStreamError.d.ts.map +1 -0
  27. package/build/types/errors/AudioStreamError.test.d.ts +2 -0
  28. package/build/types/errors/AudioStreamError.test.d.ts.map +1 -0
  29. package/build/types/index.d.ts +5 -1
  30. package/build/types/index.d.ts.map +1 -1
  31. package/build/types/streamAudioData.d.ts +119 -0
  32. package/build/types/streamAudioData.d.ts.map +1 -0
  33. package/build/types/utils/audioProcessing.d.ts +2 -2
  34. package/build/types/utils/audioProcessing.d.ts.map +1 -1
  35. package/ios/AudioProcessingHelpers.swift +10 -5
  36. package/ios/AudioStreamDecoder.swift +614 -0
  37. package/ios/AudioStudioModule.swift +186 -3
  38. package/package.json +163 -146
  39. package/scripts/README.md +58 -0
  40. package/src/errors/AudioStreamError.test.ts +92 -0
  41. package/src/errors/AudioStreamError.ts +199 -0
  42. package/src/index.ts +24 -0
  43. package/src/streamAudioData.ts +758 -0
  44. package/src/utils/audioProcessing.ts +25 -14
  45. package/android/src/androidTest/assets/chorus.wav +0 -0
  46. package/android/src/androidTest/assets/jfk.wav +0 -0
  47. package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
  48. package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
  49. package/android/src/androidTest/java/net/siteed/audiostudio/AudioFinalMetadataContractInstrumentedTest.kt +0 -190
  50. package/android/src/androidTest/java/net/siteed/audiostudio/AudioProcessorInstrumentedTest.kt +0 -197
  51. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +0 -487
  52. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +0 -250
  53. package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +0 -186
  54. package/android/src/androidTest/java/net/siteed/audiostudio/integration/AudioFocusStrategyIntegrationTest.kt +0 -332
  55. package/android/src/androidTest/java/net/siteed/audiostudio/integration/BufferDurationIntegrationTest.kt +0 -324
  56. package/android/src/androidTest/java/net/siteed/audiostudio/integration/CompressedOnlyOutputTest.kt +0 -253
  57. package/android/src/androidTest/java/net/siteed/audiostudio/integration/DeviceDisconnectionFallbackTest.kt +0 -218
  58. package/android/src/androidTest/java/net/siteed/audiostudio/integration/EventEmissionIntervalTest.kt +0 -120
  59. package/android/src/androidTest/java/net/siteed/audiostudio/integration/M4aFormatTest.kt +0 -345
  60. package/android/src/androidTest/java/net/siteed/audiostudio/integration/OutputControlIntegrationTest.kt +0 -340
  61. package/android/src/androidTest/java/net/siteed/audiostudio/integration/PcmStreamingDurationTest.kt +0 -252
  62. package/android/src/androidTest/java/net/siteed/audiostudio/integration/README.md +0 -95
  63. package/android/src/androidTest/java/net/siteed/audiostudio/integration/run_integration_tests.sh +0 -43
  64. package/android/src/test/java/net/siteed/audiostudio/AndroidCallStateTest.kt +0 -37
  65. package/android/src/test/java/net/siteed/audiostudio/AndroidEventEmitterTest.kt +0 -28
  66. package/android/src/test/java/net/siteed/audiostudio/AudioFileHandlerTest.kt +0 -279
  67. package/android/src/test/java/net/siteed/audiostudio/AudioFocusStrategyTest.kt +0 -249
  68. package/android/src/test/java/net/siteed/audiostudio/AudioFormatTest.kt +0 -151
  69. package/android/src/test/java/net/siteed/audiostudio/AudioFormatUtilsTest.kt +0 -273
  70. package/android/src/test/java/net/siteed/audiostudio/DeviceDisconnectionFallbackUnitTest.kt +0 -140
  71. package/android/src/test/java/net/siteed/audiostudio/InterruptionAutoResumePolicyTest.kt +0 -49
  72. package/android/src/test/resources/chorus.wav +0 -0
  73. package/android/src/test/resources/generate_test_audio.py +0 -94
  74. package/android/src/test/resources/jfk.wav +0 -0
  75. package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
  76. package/android/src/test/resources/recorder_hello_world.wav +0 -0
  77. package/ios/AudioStudioTests/AudioFileHandlerTests.swift +0 -338
  78. package/ios/AudioStudioTests/AudioFormatUtilsTests.swift +0 -331
  79. package/ios/AudioStudioTests/AudioTestHelpers.swift +0 -130
  80. package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +0 -334
  81. package/ios/AudioStudioTests/EventEmissionIntervalTests.swift +0 -105
  82. package/ios/AudioStudioTests/Info.plist +0 -22
  83. package/ios/AudioStudioTests/README.md +0 -39
  84. package/ios/AudioStudioTests/SimpleAudioTest.swift +0 -98
  85. package/ios/AudioStudioTests/TestAudioGenerator.swift +0 -75
  86. package/ios/tests/README.md +0 -41
  87. package/ios/tests/integration/buffer_and_fallback_test.swift +0 -178
  88. package/ios/tests/integration/buffer_duration_test.swift +0 -185
  89. package/ios/tests/integration/compressed_only_output_test.swift +0 -271
  90. package/ios/tests/integration/output_control_test.swift +0 -322
  91. package/ios/tests/integration/run_integration_tests.sh +0 -37
  92. package/ios/tests/opus_support_test_macos.swift +0 -154
  93. package/ios/tests/standalone/audio_processing_test.swift +0 -144
  94. package/ios/tests/standalone/audio_recording_test.swift +0 -277
  95. package/ios/tests/standalone/audio_streaming_test.swift +0 -249
  96. package/ios/tests/standalone/standalone_test.swift +0 -144
@@ -0,0 +1,92 @@
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({
15
+ code: 'FILE_NOT_FOUND',
16
+ message: 'gone',
17
+ })
18
+ expect(mapped.code).toBe('ERR_AUDIO_STREAM_FILE_NOT_FOUND')
19
+ expect(mapped.recoverable).toBe(false)
20
+ })
21
+
22
+ it('maps unsupported codec text', () => {
23
+ const mapped = mapStreamError(
24
+ new Error('No suitable codec for audio/opus')
25
+ )
26
+ expect(mapped.code).toBe('ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT')
27
+ })
28
+
29
+ it('marks cancellation as recoverable', () => {
30
+ const mapped = mapStreamError({
31
+ code: 'ERR_AUDIO_STREAM_CANCELLED',
32
+ message: 'user cancelled',
33
+ })
34
+ expect(mapped.code).toBe('ERR_AUDIO_STREAM_CANCELLED')
35
+ expect(mapped.recoverable).toBe(true)
36
+ })
37
+
38
+ it('maps backpressure timeout as recoverable', () => {
39
+ const mapped = mapStreamError({
40
+ code: 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT',
41
+ message: 'ack timed out',
42
+ })
43
+ expect(mapped.code).toBe('ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT')
44
+ expect(mapped.recoverable).toBe(true)
45
+ })
46
+
47
+ it('falls back to UNKNOWN', () => {
48
+ const mapped = mapStreamError({})
49
+ expect(mapped.code).toBe('ERR_AUDIO_STREAM_UNKNOWN')
50
+ })
51
+
52
+ it('warns when native returns an unknown audio stream code', () => {
53
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
54
+ const mapped = mapStreamError({
55
+ code: 'ERR_AUDIO_STREAM_FOOBAR',
56
+ message: 'new native code',
57
+ })
58
+ expect(mapped.code).toBe('ERR_AUDIO_STREAM_UNKNOWN')
59
+ expect(warn).toHaveBeenCalledWith(
60
+ '[AudioStreamError] Unknown native audio stream error code: ERR_AUDIO_STREAM_FOOBAR'
61
+ )
62
+ warn.mockRestore()
63
+ })
64
+
65
+ it('preserves nativeCode and nativeMessage', () => {
66
+ const mapped = mapStreamError({
67
+ code: 'WEIRD_NATIVE_CODE',
68
+ message: 'something went wrong on the bridge',
69
+ })
70
+ expect(mapped.nativeCode).toBe('WEIRD_NATIVE_CODE')
71
+ expect(mapped.nativeMessage).toContain('bridge')
72
+ })
73
+
74
+ it('serialises to a stable JSON payload', () => {
75
+ const err = new AudioStreamError({
76
+ code: 'ERR_AUDIO_STREAM_DECODE_FAILED',
77
+ message: 'decoder bust',
78
+ recoverable: false,
79
+ fileUri: 'file:///a.m4a',
80
+ platform: 'ios',
81
+ })
82
+ expect(err.toJSON()).toEqual({
83
+ code: 'ERR_AUDIO_STREAM_DECODE_FAILED',
84
+ message: 'decoder bust',
85
+ recoverable: false,
86
+ fileUri: 'file:///a.m4a',
87
+ platform: 'ios',
88
+ nativeCode: undefined,
89
+ nativeMessage: undefined,
90
+ })
91
+ })
92
+ })
@@ -0,0 +1,199 @@
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 isUnknownAudioStreamCode(raw: string | undefined): boolean {
84
+ if (!raw) return false
85
+ return (
86
+ raw.toUpperCase().startsWith('ERR_AUDIO_STREAM_') &&
87
+ normalizeCode(raw) === null
88
+ )
89
+ }
90
+
91
+ function normalizeCode(raw: string | undefined): AudioStreamErrorCode | null {
92
+ if (!raw) return null
93
+ const upper = raw.toUpperCase()
94
+ if (upper.startsWith('ERR_AUDIO_STREAM_')) {
95
+ const known: AudioStreamErrorCode[] = [
96
+ 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT',
97
+ 'ERR_AUDIO_STREAM_INVALID_RANGE',
98
+ 'ERR_AUDIO_STREAM_DECODE_FAILED',
99
+ 'ERR_AUDIO_STREAM_CANCELLED',
100
+ 'ERR_AUDIO_STREAM_PERMISSION_DENIED',
101
+ 'ERR_AUDIO_STREAM_FILE_NOT_FOUND',
102
+ 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT',
103
+ 'ERR_AUDIO_STREAM_NATIVE_UNAVAILABLE',
104
+ 'ERR_AUDIO_STREAM_BUSY',
105
+ 'ERR_AUDIO_STREAM_UNKNOWN',
106
+ ]
107
+ if ((known as string[]).includes(upper)) {
108
+ return upper as AudioStreamErrorCode
109
+ }
110
+ }
111
+ if (upper.includes('FILE_NOT_FOUND') || upper === 'ENOENT') {
112
+ return 'ERR_AUDIO_STREAM_FILE_NOT_FOUND'
113
+ }
114
+ if (upper.includes('PERMISSION') || upper === 'EACCES') {
115
+ return 'ERR_AUDIO_STREAM_PERMISSION_DENIED'
116
+ }
117
+ if (
118
+ upper.includes('UNSUPPORTED') ||
119
+ upper.includes('NO_SUITABLE_CODEC') ||
120
+ upper.includes('NO SUITABLE CODEC') ||
121
+ upper.includes('NOT SUPPORTED')
122
+ ) {
123
+ return 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT'
124
+ }
125
+ if (
126
+ upper.includes('INVALID_RANGE') ||
127
+ upper.includes('OUT_OF_RANGE') ||
128
+ upper.includes('INVALID_TIME')
129
+ ) {
130
+ return 'ERR_AUDIO_STREAM_INVALID_RANGE'
131
+ }
132
+ if (upper.includes('CANCELLED') || upper.includes('CANCELED')) {
133
+ return 'ERR_AUDIO_STREAM_CANCELLED'
134
+ }
135
+ if (upper.includes('BUSY')) {
136
+ return 'ERR_AUDIO_STREAM_BUSY'
137
+ }
138
+ if (upper.includes('BACKPRESSURE')) {
139
+ return 'ERR_AUDIO_STREAM_BACKPRESSURE_TIMEOUT'
140
+ }
141
+ if (
142
+ upper.includes('DECODE') ||
143
+ upper.includes('CODEC') ||
144
+ upper.includes('MALFORMED')
145
+ ) {
146
+ return 'ERR_AUDIO_STREAM_DECODE_FAILED'
147
+ }
148
+ return null
149
+ }
150
+
151
+ export function mapStreamError(
152
+ err: unknown,
153
+ fileUri?: string,
154
+ platform?: string
155
+ ): AudioStreamError {
156
+ if (err instanceof AudioStreamError) return err
157
+
158
+ const nativeMessage = getNativeMessage(err)
159
+ const nativeCode = getNativeCode(err)
160
+ const lower = nativeMessage.toLowerCase()
161
+
162
+ if (isUnknownAudioStreamCode(nativeCode)) {
163
+ console.warn(
164
+ `[AudioStreamError] Unknown native audio stream error code: ${nativeCode}`
165
+ )
166
+ }
167
+
168
+ let code =
169
+ normalizeCode(nativeCode) ??
170
+ normalizeCode(nativeMessage) ??
171
+ 'ERR_AUDIO_STREAM_UNKNOWN'
172
+
173
+ if (code === 'ERR_AUDIO_STREAM_UNKNOWN') {
174
+ if (lower.includes('not found') || lower.includes('does not exist')) {
175
+ code = 'ERR_AUDIO_STREAM_FILE_NOT_FOUND'
176
+ } else if (
177
+ lower.includes('unsupported') ||
178
+ lower.includes('no suitable codec')
179
+ ) {
180
+ code = 'ERR_AUDIO_STREAM_UNSUPPORTED_FORMAT'
181
+ } else if (lower.includes('permission') || lower.includes('denied')) {
182
+ code = 'ERR_AUDIO_STREAM_PERMISSION_DENIED'
183
+ } else if (lower.includes('decode') || lower.includes('codec')) {
184
+ code = 'ERR_AUDIO_STREAM_DECODE_FAILED'
185
+ } else if (lower.includes('cancel')) {
186
+ code = 'ERR_AUDIO_STREAM_CANCELLED'
187
+ }
188
+ }
189
+
190
+ return new AudioStreamError({
191
+ code,
192
+ message: `Audio stream failed (${code}): ${nativeMessage}`,
193
+ recoverable: RECOVERABLE.includes(code),
194
+ fileUri,
195
+ platform,
196
+ nativeCode,
197
+ nativeMessage,
198
+ })
199
+ }
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'