Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .package-manifests/siteed__audio-studio.txt
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ ios/PrimaryWriteFailurePolicy.swift
ios/RecordingResult.swift
ios/RecordingSettings.swift
ios/SafeFilename.swift
ios/TrimFormatResolution.swift
ios/WaveformExtractor.swift
package.json
plugin/build/index.cjs
Expand Down
22 changes: 22 additions & 0 deletions apps/playground/src/agentic-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2113,6 +2113,28 @@ if (__DEV__) {
return { op, status: 'pending' }
},

// Parameterized trim, for validating trim changes on device with arbitrary
// options (#451). Fire-and-store like the other test methods: CDP uses
// awaitPromise: false, so poll getLastResult().
testTrimAudioWith: (options: Record<string, unknown>) => {
const op = 'trimAudioWith'
_lastAsyncResult = { op, status: 'pending' }
void (async () => {
try {
const fileUri =
(options.fileUri as string) ?? (await loadSampleFileUri())
const result = await trimAudio({
...(stripFunctions(options) as object),
fileUri,
} as never)
_lastAsyncResult = { op, status: 'success', result }
} catch (e) {
_lastAsyncResult = { op, status: 'error', error: String(e) }
}
})()
return _lastAsyncResult
},

testTrimAudio: () => {
const op = 'trimAudio'
_lastAsyncResult = { op, status: 'pending' }
Expand Down
15 changes: 9 additions & 6 deletions packages/audio-studio/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- iOS `trimAudio` AAC output now honours the requested sample rate. Writing source-format buffers to a target-rate writer mis-timed the output — one second at 44.1kHz came back as 2.0s at 22.05kHz and 0.92s at 48kHz. The AAC path now converts against the writer's resolved format, the same treatment the WAV path received. An explicitly requested bitrate the encoder cannot serve is now an error rather than a silent substitution; only the library's own default gives way. A requested rate the AAC encoder would resolve to something else — 1Hz to 8kHz, 384kHz to 192kHz — is refused rather than returned as success (#451).
- iOS `trimAudio` preserves the input's bit depth when none is requested, and honours a bitDepth-only request instead of ignoring it via the fast path. An omitted depth previously forced 16-bit during any rate or channel change, contrary to the documented contract (#451).

- `trimAudio` and `startRecording` now reject a filename that is not a single path component, on both platforms. Each appended the caller's value to a directory, so `../../../../tmp/pwned` wrote outside it — `File(filesDir, ...)` and `appendingPathComponent` resolve `..` alike. Android trim rejects with `INVALID_OUTPUT_FILENAME`, Android recording with `INVALID_CONFIG`, iOS with an invalid-settings failure (#452).
- iOS `trimAudio` now rejects an `outputFileName` that is not a single filename. The value is appended to the output directory, so a path could traverse out of it — `../../../../tmp/pwned` resolved to `/var/tmp/pwned.wav` and the trim wrote there (#433).
- iOS `extractMelSpectrogram` no longer crashes on a large `windowSizeMs`, `hopLengthMs`, `startTimeMs` or `endTimeMs`. Those options are validated as representable, but the value is then multiplied by the file's sample rate, and the product can overflow the type it is narrowed to — `windowSizeMs: 100000000` is 4.41e9 samples at 44.1 kHz, which fits `Int` and traps the `Int32` the native wrapper takes. The conversions now clamp where the sample rate is known, since no bound on the option alone can be correct (#433).

### Added

- `addRecordingErrorListener()` subscribes to errors raised while recording is already running. iOS emitted these all along with no typed way to subscribe, so a failure that does not reject a call — the stalled WAV in #420, for one — was unobservable. Both platforms now emit it: Android declares the event and reports the AudioRecord leaving its initialized state, a read returning an error code, the primary WAV failing to flush, and the recording loop dying (#447).
Expand All @@ -19,12 +28,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Breaking for error handling:** iOS `startRecording` and `prepareRecording` no longer reject with the code `"ERROR"` and a fixed message. Callers matching on that code or on the exact string must switch on `error.code` instead; use the exported `startRecordingErrorCode()` helper to narrow it.
- iOS `startRecording` now rejects with a specific reason instead of the single message "Failed to start recording.". Six distinct failures — an active phone call, a recording already in progress, missing settings, a failed audio tap, preparation failure, and the audio engine refusing to start — previously collapsed into one string. The codes match the ones Android already emits (`ONGOING_CALL`, `ALREADY_RECORDING`, `FILE_CREATION_FAILED`, `START_FAILED`), so callers switch on one vocabulary rather than one per platform. New `startRecordingErrorCode()` helper and `StartRecordingErrorCode` type narrow an unknown rejection.

### Fixed

- `trimAudio` and `startRecording` now reject a filename that is not a single path component, on both platforms. Each appended the caller's value to a directory, so `../../../../tmp/pwned` wrote outside it — `File(filesDir, ...)` and `appendingPathComponent` resolve `..` alike. Android trim rejects with `INVALID_OUTPUT_FILENAME`, Android recording with `INVALID_CONFIG`, iOS with an invalid-settings failure (#452).
- iOS `trimAudio` now rejects an `outputFileName` that is not a single filename. The value is appended to the output directory, so a path could traverse out of it — `../../../../tmp/pwned` resolved to `/var/tmp/pwned.wav` and the trim wrote there (#433).
- iOS `extractMelSpectrogram` no longer crashes on a large `windowSizeMs`, `hopLengthMs`, `startTimeMs` or `endTimeMs`. Those options are validated as representable, but the value is then multiplied by the file's sample rate, and the product can overflow the type it is narrowed to — `windowSizeMs: 100000000` is 4.41e9 samples at 44.1 kHz, which fits `Int` and traps the `Int32` the native wrapper takes. The conversions now clamp where the sample rate is known, since no bound on the option alone can be correct (#433).

## [3.2.1] - 2026-06-20

Stable release of the 3.2.1 beta fixes plus recent release-blocker validation.
Expand Down
2 changes: 2 additions & 0 deletions packages/audio-studio/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ let package = Package(
"BridgedNarrowing.swift",
"OutputPromotion.swift",
"SafeFilename.swift",
"TrimFormatResolution.swift",
]
),
.testTarget(
Expand All @@ -40,6 +41,7 @@ let package = Package(
"ConverterCapabilityTests.swift",
"OutputPromotionTests.swift",
"SafeFilenameTests.swift",
"TrimFormatResolutionTests.swift",
]
),
]
Expand Down
Loading
Loading