@siteed/audio-studio 3.0.5 → 3.1.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 (63) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +108 -41
  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/AudioRecorderManager.kt +74 -22
  9. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +86 -19
  10. package/android/src/main/java/net/siteed/audiostudio/AudioTrimmer.kt +174 -212
  11. package/android/src/main/java/net/siteed/audiostudio/EventSender.kt +6 -0
  12. package/android/src/test/java/net/siteed/audiostudio/AndroidCallStateTest.kt +37 -0
  13. package/android/src/test/java/net/siteed/audiostudio/AndroidEventEmitterTest.kt +28 -0
  14. package/android/src/test/java/net/siteed/audiostudio/InterruptionAutoResumePolicyTest.kt +49 -0
  15. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  16. package/build/cjs/AudioAnalysis/extractPreview.js +92 -15
  17. package/build/cjs/AudioAnalysis/extractPreview.js.map +1 -1
  18. package/build/cjs/AudioAnalysis/extractPreviewBars.js +134 -0
  19. package/build/cjs/AudioAnalysis/extractPreviewBars.js.map +1 -0
  20. package/build/cjs/AudioStudio.types.js.map +1 -1
  21. package/build/cjs/errors/AudioExtractionError.js +127 -0
  22. package/build/cjs/errors/AudioExtractionError.js.map +1 -0
  23. package/build/cjs/index.js +6 -1
  24. package/build/cjs/index.js.map +1 -1
  25. package/build/cjs/useAudioRecorder.js +36 -18
  26. package/build/cjs/useAudioRecorder.js.map +1 -1
  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/AudioStudio.types.js.map +1 -1
  33. package/build/esm/errors/AudioExtractionError.js +122 -0
  34. package/build/esm/errors/AudioExtractionError.js.map +1 -0
  35. package/build/esm/index.js +2 -0
  36. package/build/esm/index.js.map +1 -1
  37. package/build/esm/useAudioRecorder.js +36 -18
  38. package/build/esm/useAudioRecorder.js.map +1 -1
  39. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +79 -0
  40. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
  41. package/build/types/AudioAnalysis/extractPreview.d.ts +2 -2
  42. package/build/types/AudioAnalysis/extractPreview.d.ts.map +1 -1
  43. package/build/types/AudioAnalysis/extractPreviewBars.d.ts +12 -0
  44. package/build/types/AudioAnalysis/extractPreviewBars.d.ts.map +1 -0
  45. package/build/types/AudioStudio.types.d.ts +14 -1
  46. package/build/types/AudioStudio.types.d.ts.map +1 -1
  47. package/build/types/errors/AudioExtractionError.d.ts +24 -0
  48. package/build/types/errors/AudioExtractionError.d.ts.map +1 -0
  49. package/build/types/index.d.ts +3 -0
  50. package/build/types/index.d.ts.map +1 -1
  51. package/build/types/useAudioRecorder.d.ts.map +1 -1
  52. package/ios/AudioProcessor.swift +99 -0
  53. package/ios/AudioStreamManager.swift +79 -15
  54. package/ios/AudioStudioModule.swift +63 -0
  55. package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +41 -1
  56. package/package.json +7 -7
  57. package/src/AudioAnalysis/AudioAnalysis.types.ts +82 -0
  58. package/src/AudioAnalysis/extractPreview.ts +118 -17
  59. package/src/AudioAnalysis/extractPreviewBars.ts +193 -0
  60. package/src/AudioStudio.types.ts +15 -1
  61. package/src/errors/AudioExtractionError.ts +167 -0
  62. package/src/index.ts +10 -0
  63. package/src/useAudioRecorder.tsx +36 -14
@@ -3,5 +3,11 @@ package net.siteed.audiostudio
3
3
  import android.os.Bundle
4
4
 
5
5
  interface EventSender {
6
+ /**
7
+ * Best-effort event delivery to JavaScript.
8
+ *
9
+ * Native recording/device callbacks must not crash if Expo rejects an event
10
+ * while the module is not ready to emit.
11
+ */
6
12
  fun sendExpoEvent(eventName: String, params: Bundle)
7
13
  }
@@ -0,0 +1,37 @@
1
+ package net.siteed.audiostudio
2
+
3
+ import android.media.AudioManager
4
+ import android.telephony.TelephonyManager
5
+ import org.junit.Assert.assertFalse
6
+ import org.junit.Assert.assertTrue
7
+ import org.junit.Test
8
+
9
+ class AndroidCallStateTest {
10
+ @Test
11
+ fun idleCallStateWinsOverStaleInCallAudioMode() {
12
+ val result = AndroidCallState.isOngoingCall(
13
+ TelephonyManager.CALL_STATE_IDLE,
14
+ AudioManager.MODE_IN_CALL
15
+ )
16
+
17
+ assertFalse(result)
18
+ }
19
+
20
+ @Test
21
+ fun ringingAndOffhookAreOngoingCalls() {
22
+ assertTrue(AndroidCallState.isOngoingCall(
23
+ TelephonyManager.CALL_STATE_RINGING,
24
+ AudioManager.MODE_NORMAL
25
+ ))
26
+ assertTrue(AndroidCallState.isOngoingCall(
27
+ TelephonyManager.CALL_STATE_OFFHOOK,
28
+ AudioManager.MODE_NORMAL
29
+ ))
30
+ }
31
+
32
+ @Test
33
+ fun unknownCallStateFallsBackToAudioMode() {
34
+ assertTrue(AndroidCallState.isOngoingCall(null, AudioManager.MODE_IN_COMMUNICATION))
35
+ assertFalse(AndroidCallState.isOngoingCall(null, AudioManager.MODE_NORMAL))
36
+ }
37
+ }
@@ -0,0 +1,28 @@
1
+ package net.siteed.audiostudio
2
+
3
+ import org.junit.Assert.assertFalse
4
+ import org.junit.Assert.assertTrue
5
+ import org.junit.Test
6
+
7
+ class AndroidEventEmitterTest {
8
+ @Test
9
+ fun safeSendReturnsTrueWhenEventIsSent() {
10
+ var sent = false
11
+
12
+ val result = AndroidEventEmitter.safeSend("Test", "event") {
13
+ sent = true
14
+ }
15
+
16
+ assertTrue(result)
17
+ assertTrue(sent)
18
+ }
19
+
20
+ @Test
21
+ fun safeSendCatchesEmitterExceptions() {
22
+ val result = AndroidEventEmitter.safeSend("Test", "event") {
23
+ throw IllegalArgumentException("module not ready")
24
+ }
25
+
26
+ assertFalse(result)
27
+ }
28
+ }
@@ -0,0 +1,49 @@
1
+ package net.siteed.audiostudio
2
+
3
+ import org.junit.Assert.assertFalse
4
+ import org.junit.Assert.assertTrue
5
+ import org.junit.Test
6
+
7
+ class InterruptionAutoResumePolicyTest {
8
+ @Test
9
+ fun autoResumesOnlyWhenSystemInterruptionPausedRecording() {
10
+ assertTrue(InterruptionAutoResumePolicy.shouldAutoResume(
11
+ autoResumeAfterInterruption = true,
12
+ isRecording = true,
13
+ isPaused = true,
14
+ pausedBySystemInterruption = true
15
+ ))
16
+ }
17
+
18
+ @Test
19
+ fun doesNotAutoResumeUserPausedRecording() {
20
+ assertFalse(InterruptionAutoResumePolicy.shouldAutoResume(
21
+ autoResumeAfterInterruption = true,
22
+ isRecording = true,
23
+ isPaused = true,
24
+ pausedBySystemInterruption = false
25
+ ))
26
+ }
27
+
28
+ @Test
29
+ fun requiresAutoResumeAndActivePausedRecording() {
30
+ assertFalse(InterruptionAutoResumePolicy.shouldAutoResume(
31
+ autoResumeAfterInterruption = false,
32
+ isRecording = true,
33
+ isPaused = true,
34
+ pausedBySystemInterruption = true
35
+ ))
36
+ assertFalse(InterruptionAutoResumePolicy.shouldAutoResume(
37
+ autoResumeAfterInterruption = true,
38
+ isRecording = false,
39
+ isPaused = true,
40
+ pausedBySystemInterruption = true
41
+ ))
42
+ assertFalse(InterruptionAutoResumePolicy.shouldAutoResume(
43
+ autoResumeAfterInterruption = true,
44
+ isRecording = true,
45
+ isPaused = false,
46
+ pausedBySystemInterruption = true
47
+ ))
48
+ }
49
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"AudioAnalysis.types.js","sourceRoot":"","sources":["../../../src/AudioAnalysis/AudioAnalysis.types.ts"],"names":[],"mappings":";AAAA,iEAAiE","sourcesContent":["// packages/audio-studio/src/AudioAnalysis/AudioAnalysis.types.ts\n\nimport { BitDepth, ConsoleLike } from '../AudioStudio.types'\n\n/**\n * Represents the configuration for decoding audio data.\n */\nexport interface DecodingConfig {\n /** Target sample rate for decoded audio (Android and Web) */\n targetSampleRate?: number\n /** Target number of channels (Android and Web) */\n targetChannels?: number\n /** Target bit depth (Android and Web) */\n targetBitDepth?: BitDepth\n /** Whether to normalize audio levels (Android and Web) */\n normalizeAudio?: boolean\n}\n\n/**\n * Represents speech-related features extracted from audio.\n */\nexport interface SpeechFeatures {\n isActive: boolean // Whether speech is detected in this segment\n speakerId?: number // Optional speaker identification\n // Could add more speech-related features here like:\n // confidence: number\n // language?: string\n // sentiment?: number\n // etc.\n}\n\n/**\n * Represents various audio features extracted from an audio signal.\n */\nexport interface AudioFeatures {\n energy?: number // The infinite integral of the squared signal, representing the overall energy of the audio.\n mfcc?: number[] // Mel-frequency cepstral coefficients, describing the short-term power spectrum of a sound.\n rms?: number // Root mean square value, indicating the amplitude of the audio signal.\n minAmplitude?: number // Minimum amplitude value in the audio signal.\n maxAmplitude?: number // Maximum amplitude value in the audio signal.\n zcr?: number // Zero-crossing rate, indicating the rate at which the signal changes sign.\n spectralCentroid?: number // The center of mass of the spectrum, indicating the brightness of the sound.\n spectralFlatness?: number // Measure of the flatness of the spectrum, indicating how noise-like the signal is.\n spectralRolloff?: number // The frequency below which a specified percentage (usually 85%) of the total spectral energy lies.\n spectralBandwidth?: number // The width of the spectrum, indicating the range of frequencies present.\n chromagram?: number[] // Chromagram, representing the 12 different pitch classes of the audio.\n tempo?: number // Estimated tempo of the audio signal, measured in beats per minute (BPM).\n hnr?: number // Harmonics-to-noise ratio, indicating the proportion of harmonics to noise in the audio signal.\n melSpectrogram?: number[] // Mel-scaled spectrogram representation of the audio.\n spectralContrast?: number[] // Spectral contrast features representing the difference between peaks and valleys.\n tonnetz?: number[] // Tonal network features representing harmonic relationships.\n pitch?: number // Pitch of the audio signal, measured in Hertz (Hz).\n crc32?: number // crc32 checksum of the audio signal, used to verify the integrity of the audio.\n}\n\n/**\n * Options to specify which audio features to extract.\n * Note: Advanced features (spectral features, chromagram, pitch, etc.) are experimental,\n * especially during live recording, due to high processing requirements.\n */\nexport interface AudioFeaturesOptions {\n // Basic features - well optimized\n energy?: boolean\n rms?: boolean\n zcr?: boolean\n\n // Advanced features - experimental, may impact performance in live recording\n mfcc?: boolean\n spectralCentroid?: boolean\n spectralFlatness?: boolean\n spectralRolloff?: boolean\n spectralBandwidth?: boolean\n chromagram?: boolean\n tempo?: boolean\n hnr?: boolean\n melSpectrogram?: boolean\n spectralContrast?: boolean\n tonnetz?: boolean\n pitch?: boolean\n\n // Utility\n crc32?: boolean\n}\n\n/**\n * Represents a single data point in the audio analysis.\n */\nexport interface DataPoint {\n id: number\n amplitude: number // Peak amplitude for the segment\n rms: number // Root mean square value\n dB: number // dBFS (decibels relative to full scale) computed from RMS value\n silent: boolean // Always computed\n features?: AudioFeatures\n speech?: SpeechFeatures\n startTime?: number\n endTime?: number\n // start / end position in bytes\n startPosition?: number\n endPosition?: number\n // number of audio samples for this point (samples size depends on bit depth)\n samples?: number\n}\n\n/**\n * Represents the complete data from the audio analysis.\n */\nexport interface AudioAnalysis {\n segmentDurationMs: number // Duration of each segment in milliseconds\n durationMs: number // Duration of the audio in milliseconds\n /**\n * Bit depth used for audio analysis processing.\n *\n * **Important**: This represents the internal processing bit depth, which may differ\n * from the recording bit depth. Audio is typically converted to 32-bit float for\n * analysis to ensure precision in calculations, regardless of the original recording format.\n *\n * Platform behavior:\n * - iOS: Always 32 (float processing)\n * - Android: Always 32 (float processing)\n * - Web: Always 32 (Web Audio API standard)\n *\n * The actual recorded file will maintain the requested bit depth (8, 16, or 32).\n */\n bitDepth: number\n samples: number // Size of the audio in bytes\n numberOfChannels: number // Number of audio channels\n sampleRate: number // Sample rate of the audio\n dataPoints: DataPoint[] // Array of data points from the analysis.\n amplitudeRange: {\n min: number\n max: number\n }\n rmsRange: {\n min: number\n max: number\n }\n extractionTimeMs: number // Time taken to extract/process the analysis in milliseconds\n // TODO: speaker changes into a broader speech analysis section\n speechAnalysis?: {\n speakerChanges: {\n timestamp: number\n speakerId: number\n }[]\n // Could add more speech analysis data here like:\n // dominantSpeaker?: number\n // totalSpeechDuration?: number\n // speakerStats?: { [speakerId: number]: { duration: number, segments: number } }\n }\n}\n\n/**\n * Options for specifying a time range within an audio file.\n */\nexport interface AudioRangeOptions {\n /** Start time in milliseconds */\n startTimeMs?: number\n /** End time in milliseconds */\n endTimeMs?: number\n}\n\n/**\n * Options for generating a quick preview of audio waveform.\n * This is optimized for UI rendering with a specified number of points.\n */\nexport interface PreviewOptions extends AudioRangeOptions {\n /** URI of the audio file to analyze */\n fileUri: string\n /**\n * Total number of points to generate for the preview.\n * @default 100\n */\n numberOfPoints?: number\n /**\n * Optional logger for debugging.\n */\n logger?: ConsoleLike\n /**\n * Optional configuration for decoding the audio file.\n * Defaults to:\n * - targetSampleRate: undefined (keep original)\n * - targetChannels: undefined (keep original)\n * - targetBitDepth: 16\n * - normalizeAudio: false\n */\n decodingOptions?: DecodingConfig\n}\n\n/**\n * Options for mel-spectrogram extraction\n *\n * @experimental This feature is experimental and currently only available on Android.\n * The API may change in future versions.\n */\nexport interface ExtractMelSpectrogramOptions {\n fileUri?: string // Path to audio file\n arrayBuffer?: ArrayBuffer // Raw audio buffer\n windowSizeMs: number // Window size in ms (e.g., 25)\n hopLengthMs: number // Hop length in ms (e.g., 10)\n nMels: number // Number of mel filters (e.g., 60)\n fMin?: number // Min frequency (default: 0)\n fMax?: number // Max frequency (default: sampleRate / 2)\n windowType?: 'hann' | 'hamming' // Window function (default: 'hann')\n normalize?: boolean // Mean normalization (default: false)\n logScale?: boolean // Log scaling of mel energies (default: true)\n decodingOptions?: DecodingConfig // Audio decoding settings\n /** Optional start time in ms. If neither startTimeMs nor endTimeMs is set, defaults to 0. */\n startTimeMs?: number\n /** Optional end time in ms. Clamped so that the range does not exceed MAX_DURATION_MS (30 s). */\n endTimeMs?: number\n logger?: ConsoleLike\n}\n\n/**\n * Result type for WASM-based audio feature extraction.\n */\nexport interface AudioFeaturesWasmResult {\n spectralCentroid: number\n spectralFlatness: number\n spectralRolloff: number\n spectralBandwidth: number\n mfcc: number[]\n chromagram: number[]\n}\n\n/**\n * Return type for mel spectrogram extraction\n *\n * @experimental This feature is experimental and currently only available on Android.\n * The API may change in future versions.\n */\nexport interface MelSpectrogram {\n spectrogram: number[][] // 2D array [time][mel]\n sampleRate: number // Audio sample rate\n nMels: number // Number of mel filters\n timeSteps: number // Number of time frames\n durationMs: number // Audio duration in ms\n}\n"]}
1
+ {"version":3,"file":"AudioAnalysis.types.js","sourceRoot":"","sources":["../../../src/AudioAnalysis/AudioAnalysis.types.ts"],"names":[],"mappings":";AAAA,iEAAiE","sourcesContent":["// packages/audio-studio/src/AudioAnalysis/AudioAnalysis.types.ts\n\nimport { BitDepth, ConsoleLike } from '../AudioStudio.types'\n\n/**\n * Represents the configuration for decoding audio data.\n */\nexport interface DecodingConfig {\n /** Target sample rate for decoded audio (Android and Web) */\n targetSampleRate?: number\n /** Target number of channels (Android and Web) */\n targetChannels?: number\n /** Target bit depth (Android and Web) */\n targetBitDepth?: BitDepth\n /** Whether to normalize audio levels (Android and Web) */\n normalizeAudio?: boolean\n /**\n * RMS threshold below which a segment is flagged silent.\n * Range 0..1. Default 0.01.\n * Currently applied as a JS post-process so the same behavior holds across iOS/Android/Web.\n */\n silenceRmsThreshold?: number\n}\n\n/**\n * Represents speech-related features extracted from audio.\n */\nexport interface SpeechFeatures {\n isActive: boolean // Whether speech is detected in this segment\n speakerId?: number // Optional speaker identification\n // Could add more speech-related features here like:\n // confidence: number\n // language?: string\n // sentiment?: number\n // etc.\n}\n\n/**\n * Represents various audio features extracted from an audio signal.\n */\nexport interface AudioFeatures {\n energy?: number // The infinite integral of the squared signal, representing the overall energy of the audio.\n mfcc?: number[] // Mel-frequency cepstral coefficients, describing the short-term power spectrum of a sound.\n rms?: number // Root mean square value, indicating the amplitude of the audio signal.\n minAmplitude?: number // Minimum amplitude value in the audio signal.\n maxAmplitude?: number // Maximum amplitude value in the audio signal.\n zcr?: number // Zero-crossing rate, indicating the rate at which the signal changes sign.\n spectralCentroid?: number // The center of mass of the spectrum, indicating the brightness of the sound.\n spectralFlatness?: number // Measure of the flatness of the spectrum, indicating how noise-like the signal is.\n spectralRolloff?: number // The frequency below which a specified percentage (usually 85%) of the total spectral energy lies.\n spectralBandwidth?: number // The width of the spectrum, indicating the range of frequencies present.\n chromagram?: number[] // Chromagram, representing the 12 different pitch classes of the audio.\n tempo?: number // Estimated tempo of the audio signal, measured in beats per minute (BPM).\n hnr?: number // Harmonics-to-noise ratio, indicating the proportion of harmonics to noise in the audio signal.\n melSpectrogram?: number[] // Mel-scaled spectrogram representation of the audio.\n spectralContrast?: number[] // Spectral contrast features representing the difference between peaks and valleys.\n tonnetz?: number[] // Tonal network features representing harmonic relationships.\n pitch?: number // Pitch of the audio signal, measured in Hertz (Hz).\n crc32?: number // crc32 checksum of the audio signal, used to verify the integrity of the audio.\n}\n\n/**\n * Options to specify which audio features to extract.\n * Note: Advanced features (spectral features, chromagram, pitch, etc.) are experimental,\n * especially during live recording, due to high processing requirements.\n */\nexport interface AudioFeaturesOptions {\n // Basic features - well optimized\n energy?: boolean\n rms?: boolean\n zcr?: boolean\n\n // Advanced features - experimental, may impact performance in live recording\n mfcc?: boolean\n spectralCentroid?: boolean\n spectralFlatness?: boolean\n spectralRolloff?: boolean\n spectralBandwidth?: boolean\n chromagram?: boolean\n tempo?: boolean\n hnr?: boolean\n melSpectrogram?: boolean\n spectralContrast?: boolean\n tonnetz?: boolean\n pitch?: boolean\n\n // Utility\n crc32?: boolean\n}\n\n/**\n * Represents a single data point in the audio analysis.\n */\nexport interface DataPoint {\n id: number\n amplitude: number // Peak amplitude for the segment\n rms: number // Root mean square value\n dB: number // dBFS (decibels relative to full scale) computed from RMS value\n silent: boolean // Always computed\n features?: AudioFeatures\n speech?: SpeechFeatures\n startTime?: number\n endTime?: number\n // start / end position in bytes\n startPosition?: number\n endPosition?: number\n // number of audio samples for this point (samples size depends on bit depth)\n samples?: number\n}\n\n/**\n * Represents the complete data from the audio analysis.\n */\nexport interface AudioAnalysis {\n segmentDurationMs: number // Duration of each segment in milliseconds\n durationMs: number // Duration of the audio in milliseconds\n /**\n * Bit depth used for audio analysis processing.\n *\n * **Important**: This represents the internal processing bit depth, which may differ\n * from the recording bit depth. Audio is typically converted to 32-bit float for\n * analysis to ensure precision in calculations, regardless of the original recording format.\n *\n * Platform behavior:\n * - iOS: Always 32 (float processing)\n * - Android: Always 32 (float processing)\n * - Web: Always 32 (Web Audio API standard)\n *\n * The actual recorded file will maintain the requested bit depth (8, 16, or 32).\n */\n bitDepth: number\n samples: number // Size of the audio in bytes\n numberOfChannels: number // Number of audio channels\n sampleRate: number // Sample rate of the audio\n dataPoints: DataPoint[] // Array of data points from the analysis.\n amplitudeRange: {\n min: number\n max: number\n }\n rmsRange: {\n min: number\n max: number\n }\n extractionTimeMs: number // Time taken to extract/process the analysis in milliseconds\n // TODO: speaker changes into a broader speech analysis section\n speechAnalysis?: {\n speakerChanges: {\n timestamp: number\n speakerId: number\n }[]\n // Could add more speech analysis data here like:\n // dominantSpeaker?: number\n // totalSpeechDuration?: number\n // speakerStats?: { [speakerId: number]: { duration: number, segments: number } }\n }\n}\n\n/**\n * Options for specifying a time range within an audio file.\n */\nexport interface AudioRangeOptions {\n /** Start time in milliseconds */\n startTimeMs?: number\n /** End time in milliseconds */\n endTimeMs?: number\n}\n\n/**\n * Options for generating a quick preview of audio waveform.\n * This is optimized for UI rendering with a specified number of points.\n */\nexport interface PreviewBar {\n /** Stable zero-based bar identifier. */\n id: number\n /** Peak amplitude for this bar, normalized to 0..1. */\n amplitude: number\n /** Root mean square amplitude for this bar, normalized to 0..1. */\n rms: number\n /** Whether this bar is below the configured silence RMS threshold. */\n silent: boolean\n /** Bar start time in milliseconds from the extracted range start. */\n startTimeMs: number\n /** Bar end time in milliseconds from the extracted range start. */\n endTimeMs: number\n}\n\n/**\n * Compact preview-bars result for UI waveform rendering.\n * Unlike `AudioAnalysis`, this intentionally omits full `DataPoint` feature data.\n */\nexport interface PreviewBarsResult {\n bars: PreviewBar[]\n durationMs: number\n sampleRate: number\n numberOfChannels: number\n bitDepth: number\n samples: number\n /** Requested bar count before native/platform clamping. */\n requestedNumberOfBars: number\n /** Approximate duration represented by each bar. */\n barDurationMs: number\n amplitudeRange: {\n min: number\n max: number\n }\n rmsRange: {\n min: number\n max: number\n }\n extractionTimeMs: number\n}\n\n/**\n * Options for extracting compact waveform preview bars for UI rendering.\n */\nexport interface PreviewBarsOptions extends AudioRangeOptions {\n /** URI of the audio file to analyze */\n fileUri: string\n /**\n * Total number of bars to generate for the preview.\n * @default 100\n */\n numberOfBars?: number\n /** Optional logger for debugging. */\n logger?: ConsoleLike\n /** Optional configuration for decoding the audio file. */\n decodingOptions?: DecodingConfig\n /**\n * Optional callback fired once per compact bar after extraction resolves.\n * Native progressive streaming is not implied by this callback.\n */\n onBarReady?: (bar: PreviewBar, index: number, total: number) => void\n}\n\nexport interface PreviewOptions extends AudioRangeOptions {\n /** URI of the audio file to analyze */\n fileUri: string\n /**\n * Total number of points to generate for the preview.\n * @default 100\n */\n numberOfPoints?: number\n /**\n * Optional logger for debugging.\n */\n logger?: ConsoleLike\n /**\n * Optional configuration for decoding the audio file.\n * Defaults to:\n * - targetSampleRate: undefined (keep original)\n * - targetChannels: undefined (keep original)\n * - targetBitDepth: 16\n * - normalizeAudio: false\n */\n decodingOptions?: DecodingConfig\n /**\n * Optional callback fired once per data point as the preview becomes available.\n * Today the native module returns the full analysis in one shot; the points are then\n * micro-batched on the JS side so consumers can render bars incrementally.\n * Native progressive streaming is a future enhancement.\n */\n onPointReady?: (point: DataPoint, index: number, total: number) => void\n /**\n * Optional cancellation signal for JS-side progressive point emission.\n * Aborting does not cancel native extraction after it has started, but it\n * stops any queued `onPointReady` callbacks from an older request.\n */\n signal?: AbortSignal\n}\n\n/**\n * Options for mel-spectrogram extraction\n *\n * @experimental This feature is experimental and currently only available on Android.\n * The API may change in future versions.\n */\nexport interface ExtractMelSpectrogramOptions {\n fileUri?: string // Path to audio file\n arrayBuffer?: ArrayBuffer // Raw audio buffer\n windowSizeMs: number // Window size in ms (e.g., 25)\n hopLengthMs: number // Hop length in ms (e.g., 10)\n nMels: number // Number of mel filters (e.g., 60)\n fMin?: number // Min frequency (default: 0)\n fMax?: number // Max frequency (default: sampleRate / 2)\n windowType?: 'hann' | 'hamming' // Window function (default: 'hann')\n normalize?: boolean // Mean normalization (default: false)\n logScale?: boolean // Log scaling of mel energies (default: true)\n decodingOptions?: DecodingConfig // Audio decoding settings\n /** Optional start time in ms. If neither startTimeMs nor endTimeMs is set, defaults to 0. */\n startTimeMs?: number\n /** Optional end time in ms. Clamped so that the range does not exceed MAX_DURATION_MS (30 s). */\n endTimeMs?: number\n logger?: ConsoleLike\n}\n\n/**\n * Result type for WASM-based audio feature extraction.\n */\nexport interface AudioFeaturesWasmResult {\n spectralCentroid: number\n spectralFlatness: number\n spectralRolloff: number\n spectralBandwidth: number\n mfcc: number[]\n chromagram: number[]\n}\n\n/**\n * Return type for mel spectrogram extraction\n *\n * @experimental This feature is experimental and currently only available on Android.\n * The API may change in future versions.\n */\nexport interface MelSpectrogram {\n spectrogram: number[][] // 2D array [time][mel]\n sampleRate: number // Audio sample rate\n nMels: number // Number of mel filters\n timeSteps: number // Number of time frames\n durationMs: number // Audio duration in ms\n}\n"]}
@@ -1,28 +1,105 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extractPreview = extractPreview;
4
+ const AudioExtractionError_1 = require("../errors/AudioExtractionError");
4
5
  const extractAudioAnalysis_1 = require("./extractAudioAnalysis");
6
+ const DEFAULT_SILENCE_THRESHOLD = 0.01;
7
+ /**
8
+ * Apply a silence threshold to the data points by recomputing the `silent` flag from rms.
9
+ * Returns a new array (does not mutate the source).
10
+ */
11
+ function applySilenceThreshold(dataPoints, threshold) {
12
+ return dataPoints.map((p) => ({
13
+ ...p,
14
+ silent: p.rms < threshold,
15
+ }));
16
+ }
17
+ const SMALL_TOTAL_INSTANT_THRESHOLD = 50;
18
+ const PROGRESSIVE_BATCH_DELAY_MS = 30;
19
+ const PROGRESSIVE_BATCH_COUNT = 8;
20
+ /**
21
+ * Schedule progressive emission of points after the native one-shot resolve.
22
+ * Native progressive streaming is a future enhancement; today the points are
23
+ * micro-batched on the JS side so consumers (and the agentic recipe runner)
24
+ * can observe an in-flight `pointsReceived < totalPoints` window.
25
+ */
26
+ function emitPointsProgressively(dataPoints, onPointReady, signal, logger) {
27
+ const total = dataPoints.length;
28
+ if (total === 0)
29
+ return;
30
+ const safeEmit = (point, index) => {
31
+ if (signal?.aborted)
32
+ return;
33
+ try {
34
+ onPointReady(point, index, total);
35
+ }
36
+ catch (err) {
37
+ // Swallow callback errors so a buggy consumer cannot break extraction.
38
+ logger?.warn?.('extractPreview onPointReady callback failed', err);
39
+ }
40
+ };
41
+ if (signal?.aborted)
42
+ return;
43
+ if (total <= SMALL_TOTAL_INSTANT_THRESHOLD) {
44
+ for (let i = 0; i < total; i++)
45
+ safeEmit(dataPoints[i], i);
46
+ return;
47
+ }
48
+ // First quarter flushes immediately so the UI shows something within a frame.
49
+ const firstFlushCount = Math.max(1, Math.floor(total / 4));
50
+ for (let i = 0; i < firstFlushCount; i++)
51
+ safeEmit(dataPoints[i], i);
52
+ if (firstFlushCount >= total)
53
+ return;
54
+ const remaining = total - firstFlushCount;
55
+ const batchSize = Math.max(1, Math.ceil(remaining / PROGRESSIVE_BATCH_COUNT));
56
+ let cursor = firstFlushCount;
57
+ const pump = () => {
58
+ if (signal?.aborted)
59
+ return;
60
+ const end = Math.min(total, cursor + batchSize);
61
+ for (let i = cursor; i < end; i++)
62
+ safeEmit(dataPoints[i], i);
63
+ cursor = end;
64
+ if (cursor < total) {
65
+ setTimeout(pump, PROGRESSIVE_BATCH_DELAY_MS);
66
+ }
67
+ };
68
+ setTimeout(pump, PROGRESSIVE_BATCH_DELAY_MS);
69
+ }
5
70
  /**
6
71
  * Generates a simplified preview of the audio waveform for quick visualization.
7
72
  * Ideal for UI rendering with a specified number of points.
8
73
  *
9
74
  * @param options - The options for the preview, including file URI and time range.
10
75
  * @returns A promise that resolves to the audio preview data.
76
+ * @throws {AudioExtractionError} when the underlying extraction fails.
11
77
  */
12
- async function extractPreview({ fileUri, numberOfPoints = 100, startTimeMs = 0, endTimeMs = 30000, // First 30 seconds
13
- decodingOptions, logger, }) {
14
- const durationMs = endTimeMs - startTimeMs;
15
- const segmentDurationMs = Math.floor(durationMs / numberOfPoints);
16
- // Call extractAudioAnalysis with calculated parameters
17
- const analysis = await (0, extractAudioAnalysis_1.extractAudioAnalysis)({
18
- fileUri,
19
- startTimeMs,
20
- endTimeMs,
21
- logger,
22
- segmentDurationMs,
23
- decodingOptions,
24
- });
25
- // Transform the result into AudioPreview format
26
- return analysis;
78
+ async function extractPreview({ fileUri, numberOfPoints = 100, startTimeMs = 0, endTimeMs = 30000, decodingOptions, logger, onPointReady, signal, }) {
79
+ const durationMs = Math.max(1, endTimeMs - startTimeMs);
80
+ const segmentDurationMs = Math.max(1, Math.floor(durationMs / numberOfPoints));
81
+ let analysis;
82
+ try {
83
+ analysis = await (0, extractAudioAnalysis_1.extractAudioAnalysis)({
84
+ fileUri,
85
+ startTimeMs,
86
+ endTimeMs,
87
+ logger,
88
+ segmentDurationMs,
89
+ decodingOptions,
90
+ });
91
+ }
92
+ catch (err) {
93
+ throw (0, AudioExtractionError_1.mapExtractionError)(err, fileUri);
94
+ }
95
+ const threshold = decodingOptions?.silenceRmsThreshold ?? DEFAULT_SILENCE_THRESHOLD;
96
+ const adjusted = {
97
+ ...analysis,
98
+ dataPoints: applySilenceThreshold(analysis.dataPoints, threshold),
99
+ };
100
+ if (onPointReady) {
101
+ emitPointsProgressively(adjusted.dataPoints, onPointReady, signal, logger);
102
+ }
103
+ return adjusted;
27
104
  }
28
105
  //# sourceMappingURL=extractPreview.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"extractPreview.js","sourceRoot":"","sources":["../../../src/AudioAnalysis/extractPreview.ts"],"names":[],"mappings":";;AAUA,wCAuBC;AAhCD,iEAA6D;AAE7D;;;;;;GAMG;AACI,KAAK,UAAU,cAAc,CAAC,EACjC,OAAO,EACP,cAAc,GAAG,GAAG,EACpB,WAAW,GAAG,CAAC,EACf,SAAS,GAAG,KAAK,EAAE,mBAAmB;AACtC,eAAe,EACf,MAAM,GACO;IACb,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,CAAA;IAC1C,MAAM,iBAAiB,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,cAAc,CAAC,CAAA;IAEjE,uDAAuD;IACvD,MAAM,QAAQ,GAAG,MAAM,IAAA,2CAAoB,EAAC;QACxC,OAAO;QACP,WAAW;QACX,SAAS;QACT,MAAM;QACN,iBAAiB;QACjB,eAAe;KAClB,CAAC,CAAA;IAEF,gDAAgD;IAChD,OAAO,QAAQ,CAAA;AACnB,CAAC","sourcesContent":["import { PreviewOptions, AudioAnalysis } from './AudioAnalysis.types'\nimport { extractAudioAnalysis } from './extractAudioAnalysis'\n\n/**\n * Generates a simplified preview of the audio waveform for quick visualization.\n * Ideal for UI rendering with a specified number of points.\n *\n * @param options - The options for the preview, including file URI and time range.\n * @returns A promise that resolves to the audio preview data.\n */\nexport async function extractPreview({\n fileUri,\n numberOfPoints = 100,\n startTimeMs = 0,\n endTimeMs = 30000, // First 30 seconds\n decodingOptions,\n logger,\n}: PreviewOptions): Promise<AudioAnalysis> {\n const durationMs = endTimeMs - startTimeMs\n const segmentDurationMs = Math.floor(durationMs / numberOfPoints)\n\n // Call extractAudioAnalysis with calculated parameters\n const analysis = await extractAudioAnalysis({\n fileUri,\n startTimeMs,\n endTimeMs,\n logger,\n segmentDurationMs,\n decodingOptions,\n })\n\n // Transform the result into AudioPreview format\n return analysis\n}\n"]}
1
+ {"version":3,"file":"extractPreview.js","sourceRoot":"","sources":["../../../src/AudioAnalysis/extractPreview.ts"],"names":[],"mappings":"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","sourcesContent":["import { mapExtractionError } from '../errors/AudioExtractionError'\nimport { PreviewOptions, AudioAnalysis, DataPoint } from './AudioAnalysis.types'\nimport { extractAudioAnalysis } from './extractAudioAnalysis'\n\nconst DEFAULT_SILENCE_THRESHOLD = 0.01\n\n/**\n * Apply a silence threshold to the data points by recomputing the `silent` flag from rms.\n * Returns a new array (does not mutate the source).\n */\nfunction applySilenceThreshold(\n dataPoints: DataPoint[],\n threshold: number\n): DataPoint[] {\n return dataPoints.map((p) => ({\n ...p,\n silent: p.rms < threshold,\n }))\n}\n\nconst SMALL_TOTAL_INSTANT_THRESHOLD = 50\nconst PROGRESSIVE_BATCH_DELAY_MS = 30\nconst PROGRESSIVE_BATCH_COUNT = 8\n\n/**\n * Schedule progressive emission of points after the native one-shot resolve.\n * Native progressive streaming is a future enhancement; today the points are\n * micro-batched on the JS side so consumers (and the agentic recipe runner)\n * can observe an in-flight `pointsReceived < totalPoints` window.\n */\nfunction emitPointsProgressively(\n dataPoints: DataPoint[],\n onPointReady: NonNullable<PreviewOptions['onPointReady']>,\n signal?: PreviewOptions['signal'],\n logger?: PreviewOptions['logger']\n): void {\n const total = dataPoints.length\n if (total === 0) return\n\n const safeEmit = (point: DataPoint, index: number) => {\n if (signal?.aborted) return\n try {\n onPointReady(point, index, total)\n } catch (err) {\n // Swallow callback errors so a buggy consumer cannot break extraction.\n logger?.warn?.('extractPreview onPointReady callback failed', err)\n }\n }\n\n if (signal?.aborted) return\n if (total <= SMALL_TOTAL_INSTANT_THRESHOLD) {\n for (let i = 0; i < total; i++) safeEmit(dataPoints[i], i)\n return\n }\n\n // First quarter flushes immediately so the UI shows something within a frame.\n const firstFlushCount = Math.max(1, Math.floor(total / 4))\n for (let i = 0; i < firstFlushCount; i++) safeEmit(dataPoints[i], i)\n\n if (firstFlushCount >= total) return\n\n const remaining = total - firstFlushCount\n const batchSize = Math.max(\n 1,\n Math.ceil(remaining / PROGRESSIVE_BATCH_COUNT)\n )\n let cursor = firstFlushCount\n const pump = () => {\n if (signal?.aborted) return\n const end = Math.min(total, cursor + batchSize)\n for (let i = cursor; i < end; i++) safeEmit(dataPoints[i], i)\n cursor = end\n if (cursor < total) {\n setTimeout(pump, PROGRESSIVE_BATCH_DELAY_MS)\n }\n }\n setTimeout(pump, PROGRESSIVE_BATCH_DELAY_MS)\n}\n\n/**\n * Generates a simplified preview of the audio waveform for quick visualization.\n * Ideal for UI rendering with a specified number of points.\n *\n * @param options - The options for the preview, including file URI and time range.\n * @returns A promise that resolves to the audio preview data.\n * @throws {AudioExtractionError} when the underlying extraction fails.\n */\nexport async function extractPreview({\n fileUri,\n numberOfPoints = 100,\n startTimeMs = 0,\n endTimeMs = 30000,\n decodingOptions,\n logger,\n onPointReady,\n signal,\n}: PreviewOptions): Promise<AudioAnalysis> {\n const durationMs = Math.max(1, endTimeMs - startTimeMs)\n const segmentDurationMs = Math.max(\n 1,\n Math.floor(durationMs / numberOfPoints)\n )\n\n let analysis: AudioAnalysis\n try {\n analysis = await extractAudioAnalysis({\n fileUri,\n startTimeMs,\n endTimeMs,\n logger,\n segmentDurationMs,\n decodingOptions,\n })\n } catch (err) {\n throw mapExtractionError(err, fileUri)\n }\n\n const threshold =\n decodingOptions?.silenceRmsThreshold ?? DEFAULT_SILENCE_THRESHOLD\n const adjusted: AudioAnalysis = {\n ...analysis,\n dataPoints: applySilenceThreshold(analysis.dataPoints, threshold),\n }\n\n if (onPointReady) {\n emitPointsProgressively(\n adjusted.dataPoints,\n onPointReady,\n signal,\n logger\n )\n }\n\n return adjusted\n}\n"]}
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.extractPreviewBars = extractPreviewBars;
7
+ const extractPreview_1 = require("./extractPreview");
8
+ const AudioStudioModule_1 = __importDefault(require("../AudioStudioModule"));
9
+ const AudioExtractionError_1 = require("../errors/AudioExtractionError");
10
+ const cleanNativeOptions_1 = require("../utils/cleanNativeOptions");
11
+ const DEFAULT_PREVIEW_BARS = 100;
12
+ const DEFAULT_PREVIEW_END_TIME_MS = 30000;
13
+ const DEFAULT_SILENCE_THRESHOLD = 0.01;
14
+ function hasNativePreviewBars(module) {
15
+ return (module !== null &&
16
+ (typeof module === 'object' || typeof module === 'function') &&
17
+ typeof module.extractPreviewBars ===
18
+ 'function');
19
+ }
20
+ function clamp01(value) {
21
+ if (!Number.isFinite(value))
22
+ return 0;
23
+ return Math.max(0, Math.min(1, value));
24
+ }
25
+ function getPointTimeMs(value, fallbackMs) {
26
+ return typeof value === 'number' && Number.isFinite(value)
27
+ ? Math.round(value)
28
+ : fallbackMs;
29
+ }
30
+ function pointToPreviewBar(point, index, fallbackBarDurationMs, silenceRmsThreshold) {
31
+ const fallbackStartTimeMs = Math.round(index * fallbackBarDurationMs);
32
+ const fallbackEndTimeMs = Math.round((index + 1) * fallbackBarDurationMs);
33
+ const startTimeMs = getPointTimeMs(point.startTime, fallbackStartTimeMs);
34
+ const endTimeMs = getPointTimeMs(point.endTime, fallbackEndTimeMs);
35
+ const rms = clamp01(point.rms);
36
+ return {
37
+ id: point.id ?? index,
38
+ amplitude: clamp01(point.amplitude),
39
+ rms,
40
+ silent: point.silent ?? rms < silenceRmsThreshold,
41
+ startTimeMs,
42
+ endTimeMs: Math.max(startTimeMs, endTimeMs),
43
+ };
44
+ }
45
+ function calculateRange(values) {
46
+ if (values.length === 0) {
47
+ return { min: 0, max: 0 };
48
+ }
49
+ let min = Number.POSITIVE_INFINITY;
50
+ let max = Number.NEGATIVE_INFINITY;
51
+ for (const value of values) {
52
+ const safeValue = clamp01(value);
53
+ min = Math.min(min, safeValue);
54
+ max = Math.max(max, safeValue);
55
+ }
56
+ return { min, max };
57
+ }
58
+ function fromAudioAnalysis(analysis, requestedNumberOfBars, silenceRmsThreshold) {
59
+ const barDurationMs = analysis.segmentDurationMs ||
60
+ Math.max(1, analysis.durationMs / Math.max(1, analysis.dataPoints.length));
61
+ const bars = analysis.dataPoints.map((point, index) => pointToPreviewBar(point, index, barDurationMs, silenceRmsThreshold));
62
+ return {
63
+ bars,
64
+ durationMs: analysis.durationMs,
65
+ sampleRate: analysis.sampleRate,
66
+ numberOfChannels: analysis.numberOfChannels,
67
+ bitDepth: analysis.bitDepth,
68
+ samples: analysis.samples,
69
+ requestedNumberOfBars,
70
+ barDurationMs,
71
+ amplitudeRange: calculateRange(bars.map((bar) => bar.amplitude)),
72
+ rmsRange: calculateRange(bars.map((bar) => bar.rms)),
73
+ extractionTimeMs: analysis.extractionTimeMs,
74
+ };
75
+ }
76
+ function emitBarsProgressively(bars, onBarReady) {
77
+ const total = bars.length;
78
+ for (let index = 0; index < total; index++) {
79
+ onBarReady(bars[index], index, total);
80
+ }
81
+ }
82
+ /**
83
+ * Extracts compact waveform preview bars for UI rendering.
84
+ *
85
+ * Native platforms may provide a compact `extractPreviewBars` bridge. Until that
86
+ * bridge is available, this safely falls back to the existing `extractPreview`
87
+ * compatibility path and adapts `DataPoint` objects into compact bars.
88
+ *
89
+ * @throws {AudioExtractionError} when the underlying extraction fails.
90
+ */
91
+ async function extractPreviewBars({ fileUri, numberOfBars = DEFAULT_PREVIEW_BARS, startTimeMs = 0, endTimeMs = DEFAULT_PREVIEW_END_TIME_MS, decodingOptions, logger, onBarReady, }) {
92
+ const requestedNumberOfBars = Math.max(1, Math.floor(numberOfBars));
93
+ const nativeOptions = {
94
+ fileUri,
95
+ numberOfBars: requestedNumberOfBars,
96
+ startTimeMs,
97
+ endTimeMs,
98
+ decodingOptions,
99
+ };
100
+ const nativeModule = AudioStudioModule_1.default;
101
+ if (hasNativePreviewBars(nativeModule)) {
102
+ let result;
103
+ try {
104
+ result = await nativeModule.extractPreviewBars((0, cleanNativeOptions_1.cleanNativeOptions)(nativeOptions));
105
+ }
106
+ catch (err) {
107
+ throw (0, AudioExtractionError_1.mapExtractionError)(err, fileUri);
108
+ }
109
+ if (onBarReady) {
110
+ emitBarsProgressively(result.bars, onBarReady);
111
+ }
112
+ return result;
113
+ }
114
+ let analysis;
115
+ try {
116
+ analysis = await (0, extractPreview_1.extractPreview)({
117
+ fileUri,
118
+ numberOfPoints: requestedNumberOfBars,
119
+ startTimeMs,
120
+ endTimeMs,
121
+ decodingOptions,
122
+ logger,
123
+ });
124
+ }
125
+ catch (err) {
126
+ throw (0, AudioExtractionError_1.mapExtractionError)(err, fileUri);
127
+ }
128
+ const result = fromAudioAnalysis(analysis, requestedNumberOfBars, decodingOptions?.silenceRmsThreshold ?? DEFAULT_SILENCE_THRESHOLD);
129
+ if (onBarReady) {
130
+ emitBarsProgressively(result.bars, onBarReady);
131
+ }
132
+ return result;
133
+ }
134
+ //# sourceMappingURL=extractPreviewBars.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractPreviewBars.js","sourceRoot":"","sources":["../../../src/AudioAnalysis/extractPreviewBars.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AudioAnalysis,\n DataPoint,\n PreviewBar,\n PreviewBarsOptions,\n PreviewBarsResult,\n} from './AudioAnalysis.types'\nimport { extractPreview } from './extractPreview'\nimport AudioStudioModule from '../AudioStudioModule'\nimport { mapExtractionError } from '../errors/AudioExtractionError'\nimport { cleanNativeOptions } from '../utils/cleanNativeOptions'\n\nconst DEFAULT_PREVIEW_BARS = 100\nconst DEFAULT_PREVIEW_END_TIME_MS = 30000\nconst DEFAULT_SILENCE_THRESHOLD = 0.01\n\ninterface NativePreviewBarsModule {\n extractPreviewBars: (\n options: Record<string, unknown>\n ) => Promise<PreviewBarsResult>\n}\n\nfunction hasNativePreviewBars(\n module: unknown\n): module is NativePreviewBarsModule {\n return (\n module !== null &&\n (typeof module === 'object' || typeof module === 'function') &&\n typeof (module as NativePreviewBarsModule).extractPreviewBars ===\n 'function'\n )\n}\n\nfunction clamp01(value: number): number {\n if (!Number.isFinite(value)) return 0\n return Math.max(0, Math.min(1, value))\n}\n\nfunction getPointTimeMs(value: number | undefined, fallbackMs: number): number {\n return typeof value === 'number' && Number.isFinite(value)\n ? Math.round(value)\n : fallbackMs\n}\n\nfunction pointToPreviewBar(\n point: DataPoint,\n index: number,\n fallbackBarDurationMs: number,\n silenceRmsThreshold: number\n): PreviewBar {\n const fallbackStartTimeMs = Math.round(index * fallbackBarDurationMs)\n const fallbackEndTimeMs = Math.round((index + 1) * fallbackBarDurationMs)\n const startTimeMs = getPointTimeMs(point.startTime, fallbackStartTimeMs)\n const endTimeMs = getPointTimeMs(point.endTime, fallbackEndTimeMs)\n const rms = clamp01(point.rms)\n\n return {\n id: point.id ?? index,\n amplitude: clamp01(point.amplitude),\n rms,\n silent: point.silent ?? rms < silenceRmsThreshold,\n startTimeMs,\n endTimeMs: Math.max(startTimeMs, endTimeMs),\n }\n}\n\nfunction calculateRange(values: number[]): { min: number; max: number } {\n if (values.length === 0) {\n return { min: 0, max: 0 }\n }\n\n let min = Number.POSITIVE_INFINITY\n let max = Number.NEGATIVE_INFINITY\n\n for (const value of values) {\n const safeValue = clamp01(value)\n min = Math.min(min, safeValue)\n max = Math.max(max, safeValue)\n }\n\n return { min, max }\n}\n\nfunction fromAudioAnalysis(\n analysis: AudioAnalysis,\n requestedNumberOfBars: number,\n silenceRmsThreshold: number\n): PreviewBarsResult {\n const barDurationMs =\n analysis.segmentDurationMs ||\n Math.max(\n 1,\n analysis.durationMs / Math.max(1, analysis.dataPoints.length)\n )\n const bars = analysis.dataPoints.map((point, index) =>\n pointToPreviewBar(point, index, barDurationMs, silenceRmsThreshold)\n )\n\n return {\n bars,\n durationMs: analysis.durationMs,\n sampleRate: analysis.sampleRate,\n numberOfChannels: analysis.numberOfChannels,\n bitDepth: analysis.bitDepth,\n samples: analysis.samples,\n requestedNumberOfBars,\n barDurationMs,\n amplitudeRange: calculateRange(bars.map((bar) => bar.amplitude)),\n rmsRange: calculateRange(bars.map((bar) => bar.rms)),\n extractionTimeMs: analysis.extractionTimeMs,\n }\n}\n\nfunction emitBarsProgressively(\n bars: PreviewBar[],\n onBarReady: NonNullable<PreviewBarsOptions['onBarReady']>\n): void {\n const total = bars.length\n for (let index = 0; index < total; index++) {\n onBarReady(bars[index], index, total)\n }\n}\n\n/**\n * Extracts compact waveform preview bars for UI rendering.\n *\n * Native platforms may provide a compact `extractPreviewBars` bridge. Until that\n * bridge is available, this safely falls back to the existing `extractPreview`\n * compatibility path and adapts `DataPoint` objects into compact bars.\n *\n * @throws {AudioExtractionError} when the underlying extraction fails.\n */\nexport async function extractPreviewBars({\n fileUri,\n numberOfBars = DEFAULT_PREVIEW_BARS,\n startTimeMs = 0,\n endTimeMs = DEFAULT_PREVIEW_END_TIME_MS,\n decodingOptions,\n logger,\n onBarReady,\n}: PreviewBarsOptions): Promise<PreviewBarsResult> {\n const requestedNumberOfBars = Math.max(1, Math.floor(numberOfBars))\n const nativeOptions = {\n fileUri,\n numberOfBars: requestedNumberOfBars,\n startTimeMs,\n endTimeMs,\n decodingOptions,\n }\n\n const nativeModule = AudioStudioModule as unknown\n if (hasNativePreviewBars(nativeModule)) {\n let result: PreviewBarsResult\n try {\n result = await nativeModule.extractPreviewBars(\n cleanNativeOptions(nativeOptions)\n )\n } catch (err) {\n throw mapExtractionError(err, fileUri)\n }\n\n if (onBarReady) {\n emitBarsProgressively(result.bars, onBarReady)\n }\n return result\n }\n\n let analysis: AudioAnalysis\n try {\n analysis = await extractPreview({\n fileUri,\n numberOfPoints: requestedNumberOfBars,\n startTimeMs,\n endTimeMs,\n decodingOptions,\n logger,\n })\n } catch (err) {\n throw mapExtractionError(err, fileUri)\n }\n\n const result = fromAudioAnalysis(\n analysis,\n requestedNumberOfBars,\n decodingOptions?.silenceRmsThreshold ?? DEFAULT_SILENCE_THRESHOLD\n )\n\n if (onBarReady) {\n emitBarsProgressively(result.bars, onBarReady)\n }\n\n return result\n}\n"]}