@siteed/audio-studio 3.1.0 → 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 (71) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +97 -50
  3. package/android/src/androidTest/java/net/siteed/audiostudio/AudioFinalMetadataContractInstrumentedTest.kt +190 -0
  4. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +29 -83
  5. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +17 -1
  6. package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +186 -0
  7. package/android/src/main/java/net/siteed/audiostudio/AudioProcessor.kt +473 -380
  8. package/android/src/main/java/net/siteed/audiostudio/AudioStreamDecoder.kt +640 -0
  9. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +187 -13
  10. package/android/src/main/java/net/siteed/audiostudio/AudioTrimmer.kt +174 -212
  11. package/android/src/main/java/net/siteed/audiostudio/Constants.kt +4 -0
  12. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  13. package/build/cjs/AudioAnalysis/extractPreview.js +92 -15
  14. package/build/cjs/AudioAnalysis/extractPreview.js.map +1 -1
  15. package/build/cjs/AudioAnalysis/extractPreviewBars.js +134 -0
  16. package/build/cjs/AudioAnalysis/extractPreviewBars.js.map +1 -0
  17. package/build/cjs/errors/AudioExtractionError.js +127 -0
  18. package/build/cjs/errors/AudioExtractionError.js.map +1 -0
  19. package/build/cjs/errors/AudioStreamError.js +152 -0
  20. package/build/cjs/errors/AudioStreamError.js.map +1 -0
  21. package/build/cjs/errors/AudioStreamError.test.js +61 -0
  22. package/build/cjs/errors/AudioStreamError.test.js.map +1 -0
  23. package/build/cjs/index.js +12 -1
  24. package/build/cjs/index.js.map +1 -1
  25. package/build/cjs/streamAudioData.js +467 -0
  26. package/build/cjs/streamAudioData.js.map +1 -0
  27. package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  28. package/build/esm/AudioAnalysis/extractPreview.js +92 -15
  29. package/build/esm/AudioAnalysis/extractPreview.js.map +1 -1
  30. package/build/esm/AudioAnalysis/extractPreviewBars.js +128 -0
  31. package/build/esm/AudioAnalysis/extractPreviewBars.js.map +1 -0
  32. package/build/esm/errors/AudioExtractionError.js +122 -0
  33. package/build/esm/errors/AudioExtractionError.js.map +1 -0
  34. package/build/esm/errors/AudioStreamError.js +147 -0
  35. package/build/esm/errors/AudioStreamError.js.map +1 -0
  36. package/build/esm/errors/AudioStreamError.test.js +59 -0
  37. package/build/esm/errors/AudioStreamError.test.js.map +1 -0
  38. package/build/esm/index.js +5 -1
  39. package/build/esm/index.js.map +1 -1
  40. package/build/esm/streamAudioData.js +460 -0
  41. package/build/esm/streamAudioData.js.map +1 -0
  42. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +79 -0
  43. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
  44. package/build/types/AudioAnalysis/extractPreview.d.ts +2 -2
  45. package/build/types/AudioAnalysis/extractPreview.d.ts.map +1 -1
  46. package/build/types/AudioAnalysis/extractPreviewBars.d.ts +12 -0
  47. package/build/types/AudioAnalysis/extractPreviewBars.d.ts.map +1 -0
  48. package/build/types/errors/AudioExtractionError.d.ts +24 -0
  49. package/build/types/errors/AudioExtractionError.d.ts.map +1 -0
  50. package/build/types/errors/AudioStreamError.d.ts +25 -0
  51. package/build/types/errors/AudioStreamError.d.ts.map +1 -0
  52. package/build/types/errors/AudioStreamError.test.d.ts +2 -0
  53. package/build/types/errors/AudioStreamError.test.d.ts.map +1 -0
  54. package/build/types/index.d.ts +8 -1
  55. package/build/types/index.d.ts.map +1 -1
  56. package/build/types/streamAudioData.d.ts +114 -0
  57. package/build/types/streamAudioData.d.ts.map +1 -0
  58. package/ios/AudioProcessingHelpers.swift +10 -5
  59. package/ios/AudioProcessor.swift +99 -0
  60. package/ios/AudioStreamDecoder.swift +523 -0
  61. package/ios/AudioStudioModule.swift +210 -3
  62. package/ios/AudioStudioTests/AudioStreamDecoderTests.swift +128 -0
  63. package/package.json +7 -7
  64. package/src/AudioAnalysis/AudioAnalysis.types.ts +82 -0
  65. package/src/AudioAnalysis/extractPreview.ts +118 -17
  66. package/src/AudioAnalysis/extractPreviewBars.ts +193 -0
  67. package/src/errors/AudioExtractionError.ts +167 -0
  68. package/src/errors/AudioStreamError.test.ts +65 -0
  69. package/src/errors/AudioStreamError.ts +185 -0
  70. package/src/index.ts +34 -0
  71. package/src/streamAudioData.ts +654 -0
@@ -0,0 +1,193 @@
1
+ import {
2
+ AudioAnalysis,
3
+ DataPoint,
4
+ PreviewBar,
5
+ PreviewBarsOptions,
6
+ PreviewBarsResult,
7
+ } from './AudioAnalysis.types'
8
+ import { extractPreview } from './extractPreview'
9
+ import AudioStudioModule from '../AudioStudioModule'
10
+ import { mapExtractionError } from '../errors/AudioExtractionError'
11
+ import { cleanNativeOptions } from '../utils/cleanNativeOptions'
12
+
13
+ const DEFAULT_PREVIEW_BARS = 100
14
+ const DEFAULT_PREVIEW_END_TIME_MS = 30000
15
+ const DEFAULT_SILENCE_THRESHOLD = 0.01
16
+
17
+ interface NativePreviewBarsModule {
18
+ extractPreviewBars: (
19
+ options: Record<string, unknown>
20
+ ) => Promise<PreviewBarsResult>
21
+ }
22
+
23
+ function hasNativePreviewBars(
24
+ module: unknown
25
+ ): module is NativePreviewBarsModule {
26
+ return (
27
+ module !== null &&
28
+ (typeof module === 'object' || typeof module === 'function') &&
29
+ typeof (module as NativePreviewBarsModule).extractPreviewBars ===
30
+ 'function'
31
+ )
32
+ }
33
+
34
+ function clamp01(value: number): number {
35
+ if (!Number.isFinite(value)) return 0
36
+ return Math.max(0, Math.min(1, value))
37
+ }
38
+
39
+ function getPointTimeMs(value: number | undefined, fallbackMs: number): number {
40
+ return typeof value === 'number' && Number.isFinite(value)
41
+ ? Math.round(value)
42
+ : fallbackMs
43
+ }
44
+
45
+ function pointToPreviewBar(
46
+ point: DataPoint,
47
+ index: number,
48
+ fallbackBarDurationMs: number,
49
+ silenceRmsThreshold: number
50
+ ): PreviewBar {
51
+ const fallbackStartTimeMs = Math.round(index * fallbackBarDurationMs)
52
+ const fallbackEndTimeMs = Math.round((index + 1) * fallbackBarDurationMs)
53
+ const startTimeMs = getPointTimeMs(point.startTime, fallbackStartTimeMs)
54
+ const endTimeMs = getPointTimeMs(point.endTime, fallbackEndTimeMs)
55
+ const rms = clamp01(point.rms)
56
+
57
+ return {
58
+ id: point.id ?? index,
59
+ amplitude: clamp01(point.amplitude),
60
+ rms,
61
+ silent: point.silent ?? rms < silenceRmsThreshold,
62
+ startTimeMs,
63
+ endTimeMs: Math.max(startTimeMs, endTimeMs),
64
+ }
65
+ }
66
+
67
+ function calculateRange(values: number[]): { min: number; max: number } {
68
+ if (values.length === 0) {
69
+ return { min: 0, max: 0 }
70
+ }
71
+
72
+ let min = Number.POSITIVE_INFINITY
73
+ let max = Number.NEGATIVE_INFINITY
74
+
75
+ for (const value of values) {
76
+ const safeValue = clamp01(value)
77
+ min = Math.min(min, safeValue)
78
+ max = Math.max(max, safeValue)
79
+ }
80
+
81
+ return { min, max }
82
+ }
83
+
84
+ function fromAudioAnalysis(
85
+ analysis: AudioAnalysis,
86
+ requestedNumberOfBars: number,
87
+ silenceRmsThreshold: number
88
+ ): PreviewBarsResult {
89
+ const barDurationMs =
90
+ analysis.segmentDurationMs ||
91
+ Math.max(
92
+ 1,
93
+ analysis.durationMs / Math.max(1, analysis.dataPoints.length)
94
+ )
95
+ const bars = analysis.dataPoints.map((point, index) =>
96
+ pointToPreviewBar(point, index, barDurationMs, silenceRmsThreshold)
97
+ )
98
+
99
+ return {
100
+ bars,
101
+ durationMs: analysis.durationMs,
102
+ sampleRate: analysis.sampleRate,
103
+ numberOfChannels: analysis.numberOfChannels,
104
+ bitDepth: analysis.bitDepth,
105
+ samples: analysis.samples,
106
+ requestedNumberOfBars,
107
+ barDurationMs,
108
+ amplitudeRange: calculateRange(bars.map((bar) => bar.amplitude)),
109
+ rmsRange: calculateRange(bars.map((bar) => bar.rms)),
110
+ extractionTimeMs: analysis.extractionTimeMs,
111
+ }
112
+ }
113
+
114
+ function emitBarsProgressively(
115
+ bars: PreviewBar[],
116
+ onBarReady: NonNullable<PreviewBarsOptions['onBarReady']>
117
+ ): void {
118
+ const total = bars.length
119
+ for (let index = 0; index < total; index++) {
120
+ onBarReady(bars[index], index, total)
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Extracts compact waveform preview bars for UI rendering.
126
+ *
127
+ * Native platforms may provide a compact `extractPreviewBars` bridge. Until that
128
+ * bridge is available, this safely falls back to the existing `extractPreview`
129
+ * compatibility path and adapts `DataPoint` objects into compact bars.
130
+ *
131
+ * @throws {AudioExtractionError} when the underlying extraction fails.
132
+ */
133
+ export async function extractPreviewBars({
134
+ fileUri,
135
+ numberOfBars = DEFAULT_PREVIEW_BARS,
136
+ startTimeMs = 0,
137
+ endTimeMs = DEFAULT_PREVIEW_END_TIME_MS,
138
+ decodingOptions,
139
+ logger,
140
+ onBarReady,
141
+ }: PreviewBarsOptions): Promise<PreviewBarsResult> {
142
+ const requestedNumberOfBars = Math.max(1, Math.floor(numberOfBars))
143
+ const nativeOptions = {
144
+ fileUri,
145
+ numberOfBars: requestedNumberOfBars,
146
+ startTimeMs,
147
+ endTimeMs,
148
+ decodingOptions,
149
+ }
150
+
151
+ const nativeModule = AudioStudioModule as unknown
152
+ if (hasNativePreviewBars(nativeModule)) {
153
+ let result: PreviewBarsResult
154
+ try {
155
+ result = await nativeModule.extractPreviewBars(
156
+ cleanNativeOptions(nativeOptions)
157
+ )
158
+ } catch (err) {
159
+ throw mapExtractionError(err, fileUri)
160
+ }
161
+
162
+ if (onBarReady) {
163
+ emitBarsProgressively(result.bars, onBarReady)
164
+ }
165
+ return result
166
+ }
167
+
168
+ let analysis: AudioAnalysis
169
+ try {
170
+ analysis = await extractPreview({
171
+ fileUri,
172
+ numberOfPoints: requestedNumberOfBars,
173
+ startTimeMs,
174
+ endTimeMs,
175
+ decodingOptions,
176
+ logger,
177
+ })
178
+ } catch (err) {
179
+ throw mapExtractionError(err, fileUri)
180
+ }
181
+
182
+ const result = fromAudioAnalysis(
183
+ analysis,
184
+ requestedNumberOfBars,
185
+ decodingOptions?.silenceRmsThreshold ?? DEFAULT_SILENCE_THRESHOLD
186
+ )
187
+
188
+ if (onBarReady) {
189
+ emitBarsProgressively(result.bars, onBarReady)
190
+ }
191
+
192
+ return result
193
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Typed error class for audio extraction failures.
3
+ * Wraps native module errors with stable codes consumers can switch on.
4
+ */
5
+ export type AudioExtractionErrorCode =
6
+ | 'unsupported_codec'
7
+ | 'malformed_file'
8
+ | 'decode_failed'
9
+ | 'permission_denied'
10
+ | 'file_not_found'
11
+ | 'unknown'
12
+
13
+ export interface AudioExtractionErrorPayload {
14
+ code: AudioExtractionErrorCode
15
+ message: string
16
+ nativeMessage?: string
17
+ fileUri?: string
18
+ }
19
+
20
+ export class AudioExtractionError extends Error {
21
+ readonly code: AudioExtractionErrorCode
22
+ readonly nativeMessage?: string
23
+ readonly fileUri?: string
24
+
25
+ constructor(payload: AudioExtractionErrorPayload) {
26
+ super(payload.message)
27
+ this.name = 'AudioExtractionError'
28
+ this.code = payload.code
29
+ this.nativeMessage = payload.nativeMessage
30
+ this.fileUri = payload.fileUri
31
+ }
32
+
33
+ toJSON(): AudioExtractionErrorPayload {
34
+ return {
35
+ code: this.code,
36
+ message: this.message,
37
+ nativeMessage: this.nativeMessage,
38
+ fileUri: this.fileUri,
39
+ }
40
+ }
41
+ }
42
+
43
+ function getNativeMessage(err: unknown): string {
44
+ if (err instanceof Error) return err.message
45
+ if (typeof err === 'string') return err
46
+
47
+ try {
48
+ return JSON.stringify(err) ?? String(err)
49
+ } catch {
50
+ return String(err)
51
+ }
52
+ }
53
+
54
+ function getNativeCode(err: unknown): string | undefined {
55
+ if (err && typeof err === 'object' && 'code' in err) {
56
+ const code = (err as { code?: unknown }).code
57
+ if (typeof code === 'string') return code
58
+ }
59
+ return undefined
60
+ }
61
+
62
+ function mapNativeCode(
63
+ code: string | undefined
64
+ ): AudioExtractionErrorCode | null {
65
+ if (!code) return null
66
+
67
+ const normalized = code.toUpperCase()
68
+ if (
69
+ normalized.includes('FILE_NOT_FOUND') ||
70
+ normalized === 'ENOENT' ||
71
+ normalized.includes('NO_SUCH_FILE')
72
+ ) {
73
+ return 'file_not_found'
74
+ }
75
+ if (
76
+ normalized.includes('PERMISSION') ||
77
+ normalized === 'EACCES' ||
78
+ normalized.includes('NOT_AUTHORIZED')
79
+ ) {
80
+ return 'permission_denied'
81
+ }
82
+ if (
83
+ normalized.includes('UNSUPPORTED') ||
84
+ normalized.includes('NO_SUITABLE_CODEC')
85
+ ) {
86
+ return 'unsupported_codec'
87
+ }
88
+ if (
89
+ normalized.includes('INVALID_RANGE') ||
90
+ normalized.includes('INVALID_HEADER') ||
91
+ normalized.includes('MALFORMED') ||
92
+ normalized.includes('CORRUPT')
93
+ ) {
94
+ return 'malformed_file'
95
+ }
96
+ if (
97
+ normalized.includes('PROCESSING_ERROR') ||
98
+ normalized.includes('AUDIO_READ_ERROR') ||
99
+ normalized.includes('DECODE')
100
+ ) {
101
+ return 'decode_failed'
102
+ }
103
+
104
+ return null
105
+ }
106
+
107
+ /**
108
+ * Map a thrown native/JS value into an AudioExtractionError with a stable code.
109
+ * Heuristics inspect message text and known native error codes.
110
+ */
111
+ export function mapExtractionError(
112
+ err: unknown,
113
+ fileUri?: string
114
+ ): AudioExtractionError {
115
+ if (err instanceof AudioExtractionError) return err
116
+
117
+ const nativeMessage = getNativeMessage(err)
118
+ const lower = nativeMessage.toLowerCase()
119
+
120
+ let code = mapNativeCode(getNativeCode(err)) ?? 'unknown'
121
+ if (
122
+ code === 'unknown' &&
123
+ (lower.includes('unsupported') ||
124
+ lower.includes('not supported') ||
125
+ lower.includes('no suitable codec') ||
126
+ lower.includes('no track'))
127
+ ) {
128
+ code = 'unsupported_codec'
129
+ } else if (
130
+ code === 'unknown' &&
131
+ (lower.includes('not found') ||
132
+ lower.includes('no such file') ||
133
+ lower.includes('does not exist'))
134
+ ) {
135
+ code = 'file_not_found'
136
+ } else if (
137
+ code === 'unknown' &&
138
+ (lower.includes('permission') ||
139
+ lower.includes('denied') ||
140
+ lower.includes('not authorized'))
141
+ ) {
142
+ code = 'permission_denied'
143
+ } else if (
144
+ code === 'unknown' &&
145
+ (lower.includes('malformed') ||
146
+ lower.includes('corrupt') ||
147
+ lower.includes('invalid header') ||
148
+ lower.includes('invalid wav'))
149
+ ) {
150
+ code = 'malformed_file'
151
+ } else if (
152
+ code === 'unknown' &&
153
+ (lower.includes('decode') ||
154
+ lower.includes('codec') ||
155
+ lower.includes('mediaextractor') ||
156
+ lower.includes('avaudio'))
157
+ ) {
158
+ code = 'decode_failed'
159
+ }
160
+
161
+ return new AudioExtractionError({
162
+ code,
163
+ message: `Audio extraction failed (${code}): ${nativeMessage}`,
164
+ nativeMessage,
165
+ fileUri,
166
+ })
167
+ }
@@ -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
 
@@ -44,6 +48,7 @@ export { AudioDeviceManager, audioDeviceManager } from './AudioDeviceManager'
44
48
  export { useAudioDevices } from './hooks/useAudioDevices'
45
49
 
46
50
  export { setMelSpectrogramWasmUrl } from './AudioAnalysis/wasmConfig'
51
+ export { extractPreviewBars } from './AudioAnalysis/extractPreviewBars'
47
52
 
48
53
  export {
49
54
  AudioRecorderProvider,
@@ -53,6 +58,8 @@ export {
53
58
  extractPreview,
54
59
  trimAudio,
55
60
  extractAudioData,
61
+ streamAudioData,
62
+ getAudioDecodeCapabilities,
56
63
  extractMelSpectrogram,
57
64
  initMelStreamingWasm,
58
65
  computeMelFrameWasm,
@@ -61,6 +68,33 @@ export {
61
68
  useSharedAudioRecorder,
62
69
  }
63
70
 
71
+ export {
72
+ AudioExtractionError,
73
+ mapExtractionError,
74
+ } from './errors/AudioExtractionError'
75
+ export type {
76
+ AudioExtractionErrorCode,
77
+ AudioExtractionErrorPayload,
78
+ } from './errors/AudioExtractionError'
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
+
64
98
  // Export all types
65
99
  export type * from './AudioAnalysis/AudioAnalysis.types'
66
100
  export type * from './AudioStudio.types'