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 src-tauri/native/siri_tts_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ bool berd_pocket_audio_player_enqueue(
uint32_t frame_count,
char **error_out
);
bool berd_pocket_audio_player_set_rate(void *player, float rate, char **error_out);
uint64_t berd_pocket_audio_player_completed_source_frames(void *player);
uint64_t berd_pocket_audio_player_pending_buffers(void *player);
bool berd_pocket_audio_player_failed(void *player);
Expand Down
51 changes: 39 additions & 12 deletions src-tauri/native/siri_tts_bridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@ - (instancetype)initWithSampleRate:(double)sampleRate
- (BOOL)enqueueSamples:(const float *)samples
frameCount:(AVAudioFrameCount)frameCount
error:(NSError **)error;
- (BOOL)setPlaybackRate:(float)rate error:(NSError **)error;
- (uint64_t)completedSourceFramesSnapshot;
- (void)stop;
@end
Expand All @@ -601,11 +602,10 @@ - (instancetype)initWithSampleRate:(double)sampleRate

_engine = [AVAudioEngine new];
_player = [AVAudioPlayerNode new];
if (fabsf(rate - 1.0f) > 0.0001f) {
_timePitch = [AVAudioUnitTimePitch new];
_timePitch.rate = rate;
_timePitch.pitch = 0.0f;
}
_timePitch = [AVAudioUnitTimePitch new];
_timePitch.rate = rate;
_timePitch.pitch = 0.0f;
_timePitch.bypass = fabsf(rate - 1.0f) <= 0.0001f;
_format = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
sampleRate:sampleRate
channels:1
Expand All @@ -616,13 +616,9 @@ - (instancetype)initWithSampleRate:(double)sampleRate
}

[_engine attachNode:_player];
if (_timePitch) {
[_engine attachNode:_timePitch];
[_engine connect:_player to:_timePitch format:_format];
[_engine connect:_timePitch to:_engine.mainMixerNode format:_format];
} else {
[_engine connect:_player to:_engine.mainMixerNode format:_format];
}
[_engine attachNode:_timePitch];
[_engine connect:_player to:_timePitch format:_format];
[_engine connect:_timePitch to:_engine.mainMixerNode format:_format];

if (outputDeviceID != kAudioObjectUnknown) {
AudioUnit outputUnit = _engine.outputNode.audioUnit;
Expand Down Expand Up @@ -684,6 +680,22 @@ - (BOOL)enqueueSamples:(const float *)samples
return YES;
}

- (BOOL)setPlaybackRate:(float)rate error:(NSError **)error {
if (!isfinite(rate) || rate < 0.75f || rate > 2.0f) {
if (error) *error = BerdError(38, @"Pocket playback speed is invalid.");
return NO;
}
@synchronized (self) {
if (self.stopped) {
if (error) *error = BerdError(NSUserCancelledError, @"Pocket playback stopped.");
return NO;
}
self.timePitch.rate = rate;
self.timePitch.bypass = fabsf(rate - 1.0f) <= 0.0001f;
}
return YES;
}

- (uint64_t)completedSourceFramesSnapshot {
@synchronized (self) { return self.completedSourceFrames; }
}
Expand Down Expand Up @@ -1321,6 +1333,21 @@ bool berd_pocket_audio_player_enqueue(
}
}

bool berd_pocket_audio_player_set_rate(void *playerValue, float rate, char **errorOut) {
@autoreleasepool {
if (errorOut) *errorOut = NULL;
if (!playerValue) {
BerdSetError(errorOut, BerdError(36, @"Pocket playback is unavailable."));
return false;
}
NSError *error = nil;
BOOL updated = [(__bridge BerdPocketAudioPlayer *)playerValue
setPlaybackRate:rate error:&error];
if (!updated) BerdSetError(errorOut, error ?: BerdError(38, @"Could not update Pocket playback speed."));
return updated;
}
}

uint64_t berd_pocket_audio_player_completed_source_frames(void *playerValue) {
if (!playerValue) return 0;
return [(__bridge BerdPocketAudioPlayer *)playerValue completedSourceFramesSnapshot];
Expand Down
35 changes: 33 additions & 2 deletions src-tauri/src/commands/pocket_audio_player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

use std::ffi::{c_char, c_void, CStr};

const MAX_POCKET_PLAYBACK_SPEED: f32 = 2.0;

unsafe extern "C" {
fn berd_pocket_audio_player_create(
sample_rate: u32,
Expand All @@ -15,6 +17,11 @@ unsafe extern "C" {
frame_count: u32,
error_out: *mut *mut c_char,
) -> bool;
fn berd_pocket_audio_player_set_rate(
player: *mut c_void,
rate: f32,
error_out: *mut *mut c_char,
) -> bool;
fn berd_pocket_audio_player_completed_source_frames(player: *mut c_void) -> u64;
fn berd_pocket_audio_player_pending_buffers(player: *mut c_void) -> u64;
fn berd_pocket_audio_player_failed(player: *mut c_void) -> bool;
Expand Down Expand Up @@ -52,10 +59,25 @@ impl PocketAudioPlayer {
}
Ok(Self {
raw,
delivery_safety_frames: delivery_safety_frames(sample_rate, rate),
delivery_safety_frames: delivery_safety_frames(sample_rate, MAX_POCKET_PLAYBACK_SPEED),
})
}

pub(super) fn set_rate(&self, rate: f32) -> Result<(), String> {
let mut error = std::ptr::null_mut();
// SAFETY: `self.raw` is a live retained player and the bridge validates
// the rate before updating the connected time-pitch unit.
let updated = unsafe { berd_pocket_audio_player_set_rate(self.raw, rate, &mut error) };
if updated {
Ok(())
} else {
Err(take_error(
error,
"Could not update native Pocket playback speed",
))
}
}

pub(super) fn enqueue(&self, samples: &[f32]) -> Result<(), String> {
if samples.is_empty() {
return Ok(());
Expand Down Expand Up @@ -140,7 +162,9 @@ fn take_error(error: *mut c_char, fallback: &str) -> String {

#[cfg(test)]
mod tests {
use super::{apply_delivery_safety, delivery_safety_frames, playback_health};
use super::{
apply_delivery_safety, delivery_safety_frames, playback_health, MAX_POCKET_PLAYBACK_SPEED,
};

#[test]
fn delivery_safety_tracks_playback_rate_in_source_frames() {
Expand All @@ -165,6 +189,13 @@ mod tests {
);
}

#[test]
fn live_rate_changes_reserve_maximum_delivery_safety() {
let safety = delivery_safety_frames(24_000, MAX_POCKET_PLAYBACK_SPEED);
assert_eq!(safety, 4_800);
assert_eq!(apply_delivery_safety(10_000, safety), 5_200);
}

#[test]
fn unexpected_output_stops_fail_playback() {
assert!(playback_health(false).is_ok());
Expand Down
Loading