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
5 changes: 5 additions & 0 deletions src-tauri/native/CATapCapture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ private final class Tap {
var format: (rate: Double, channels: Int) {
lock.lock()
defer { lock.unlock() }
if aggregateID != 0,
let rate = readValue(aggregateID, kAudioDevicePropertyNominalSampleRate, Double(0)),
rate > 0 {
sampleRate = rate
}
return (sampleRate, tapChannels)
}

Expand Down
2 changes: 1 addition & 1 deletion src-tauri/native/virtual_driver/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<key>CFBundleShortVersionString</key>
<string>2.0</string>
<key>CFBundleVersion</key>
<string>4</string>
<string>5</string>
<key>CFPlugInFactories</key>
<dict>
<key>8C69103F-A4D0-44EA-97DC-A928A89637BF</key>
Expand Down
25 changes: 17 additions & 8 deletions src-tauri/native/virtual_driver/SplitAudioDriver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class SplitIOHandler : public aspl::IORequestHandler,
}
};

struct DeviceConfig { std::string id; std::string name; uint32_t channels; };
struct DeviceConfig { std::string id; std::string name; uint32_t channels; uint32_t sampleRate; };

static std::string CFStr(CFStringRef s) {
if (!s) return {};
Expand Down Expand Up @@ -126,7 +126,14 @@ static std::vector<DeviceConfig> ReadConfig() {
CFNumberGetValue(ch, kCFNumberIntType, &v);
if (v >= 1 && v <= 256) channels = (uint32_t)v;
}
if (!id.empty() && !name.empty()) out.push_back({id, name, channels});
uint32_t sampleRate = 48000;
CFNumberRef sr = (CFNumberRef)CFDictionaryGetValue(d, CFSTR("sampleRate"));
if (sr && CFGetTypeID(sr) == CFNumberGetTypeID()) {
int v = 0;
CFNumberGetValue(sr, kCFNumberIntType, &v);
if (v >= 8000 && v <= 384000) sampleRate = (uint32_t)v;
}
if (!id.empty() && !name.empty()) out.push_back({id, name, channels, sampleRate});
}
CFRelease(plist);
return out;
Expand All @@ -136,6 +143,7 @@ struct DeviceEntry {
std::shared_ptr<aspl::Device> device;
std::shared_ptr<SplitIOHandler> handler;
uint32_t channels;
uint32_t sampleRate;
};

static std::shared_ptr<aspl::Context> gContext;
Expand All @@ -144,9 +152,9 @@ static std::map<std::string, DeviceEntry> gDevices;
static std::mutex gDevicesMutex;

// libASPL streams default to 16-bit int; our IO is float.
static AudioStreamBasicDescription FloatFormat(UInt32 channels) {
static AudioStreamBasicDescription FloatFormat(UInt32 channels, Float64 sampleRate) {
AudioStreamBasicDescription f = {};
f.mSampleRate = 48000;
f.mSampleRate = sampleRate;
f.mFormatID = kAudioFormatLinearPCM;
f.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagsNativeEndian |
kAudioFormatFlagIsPacked;
Expand All @@ -167,7 +175,7 @@ static DeviceEntry BuildDevice(const DeviceConfig& cfg) {
params.Manufacturer = "Splitwave";
params.DeviceUID = "com.horuse.splitwave.audio." + cfg.id;
params.ModelUID = "com.horuse.splitwave.audio.model";
params.SampleRate = 48000;
params.SampleRate = cfg.sampleRate;
params.ChannelCount = cfg.channels;
params.EnableMixing = true;

Expand All @@ -177,15 +185,15 @@ static DeviceEntry BuildDevice(const DeviceConfig& cfg) {

aspl::StreamParameters outStream;
outStream.Direction = aspl::Direction::Output;
outStream.Format = FloatFormat(params.ChannelCount);
outStream.Format = FloatFormat(params.ChannelCount, params.SampleRate);
device->AddStreamWithControlsAsync(outStream);

aspl::StreamParameters inStream;
inStream.Direction = aspl::Direction::Input;
inStream.Format = FloatFormat(params.ChannelCount);
inStream.Format = FloatFormat(params.ChannelCount, params.SampleRate);
device->AddStreamWithControlsAsync(inStream);

return {device, handler, cfg.channels};
return {device, handler, cfg.channels, cfg.sampleRate};
}

// Reconciles the live device set against the config file. A channel count change
Expand All @@ -200,6 +208,7 @@ static void SyncDevices() {
return nullptr;
}();
if (!cfg || cfg->channels != it->second.channels
|| cfg->sampleRate != it->second.sampleRate
|| cfg->name != it->second.device->GetName()) {
gPlugin->RemoveDevice(it->second.device);
it = gDevices.erase(it);
Expand Down
8 changes: 8 additions & 0 deletions src-tauri/src/audio/capture/macos_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,18 @@ impl Capture {
}
}

#[allow(dead_code)]
pub fn sample_rate(&self) -> u32 {
match self {
Capture::Tap(tap) => tap.sample_rate(),
Capture::Sck(_) => SCK_RATE,
}
}

pub fn tap_rate_probe(&self) -> Option<macos_tap::TapRateProbe> {
match self {
Capture::Tap(tap) => Some(tap.rate_probe()),
Capture::Sck(_) => None,
}
}
}
29 changes: 29 additions & 0 deletions src-tauri/src/audio/capture/macos_tap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ pub struct TapCapture {
handle: *mut c_void,
state: Arc<CallbackState>,
channels: u32,
#[allow(dead_code)]
sample_rate: u32,
}

Expand Down Expand Up @@ -224,9 +225,37 @@ impl TapCapture {
self.channels
}

#[allow(dead_code)]
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}

pub fn rate_probe(&self) -> TapRateProbe {
TapRateProbe {
handle: self.handle,
}
}
}

/// Non-owning format probe used only by the normalizer thread. `TapCapture`
/// outlives that thread (NormalizedInput joins it before dropping capture), so
/// the native handle remains valid for every query.
#[derive(Clone, Copy)]
pub struct TapRateProbe {
handle: *mut c_void,
}

unsafe impl Send for TapRateProbe {}

impl TapRateProbe {
pub fn sample_rate(&self) -> Option<u32> {
let mut sample_rate = 0.0f64;
let mut channels = 0i32;
let rc = ResultCode::from_raw(unsafe {
ba_tap_format(self.handle, &mut sample_rate, &mut channels)
});
(rc == ResultCode::Ok && sample_rate > 0.0 && channels > 0).then_some(sample_rate as u32)
}
}

impl Drop for TapCapture {
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/audio/capture/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mod macos;
#[cfg(target_os = "macos")]
mod macos_backend;
#[cfg(target_os = "macos")]
mod macos_tap;
pub(crate) mod macos_tap;
#[cfg(target_os = "macos")]
pub use macos_backend::{capture_rate, uses_taps, Capture};

Expand Down
33 changes: 19 additions & 14 deletions src-tauri/src/audio/clock.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Pacing source for the DSP worker.

use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
Expand Down Expand Up @@ -99,8 +99,9 @@ const FILL_CLOCK_MAX_SLEEP: Duration = Duration::from_millis(5);
/// target, so the worker produces the next block immediately and never loses
/// the notion of "how far behind" the way a deadline reset would.
pub struct DeviceFillClock {
sample_rate: u32,
block_frames: usize,
pipeline_sample_rate: u32,
device_sample_rate: Arc<AtomicU32>,
engine_block_frames: usize,
level: Arc<AtomicI64>,
/// Fill target, sized to the device's own buffer by the audio callback
/// (see `speaker_ring`). Read here every tick so the ring always bridges
Expand All @@ -113,14 +114,16 @@ pub struct DeviceFillClock {

impl DeviceFillClock {
pub fn new(
sample_rate: u32,
block_frames: usize,
pipeline_sample_rate: u32,
device_sample_rate: Arc<AtomicU32>,
engine_block_frames: usize,
level: Arc<AtomicI64>,
target: Arc<AtomicI64>,
) -> Self {
Self {
sample_rate,
block_frames,
pipeline_sample_rate,
device_sample_rate,
engine_block_frames,
level,
target,
primed: false,
Expand All @@ -136,24 +139,26 @@ impl ClockSource for DeviceFillClock {
}
let target_frames = self.target.load(Ordering::Relaxed).max(0) as usize;
let queued = self.level.load(Ordering::Relaxed).max(0) as usize;
if queued + self.block_frames <= target_frames {
let dev_sr = self.device_sample_rate.load(Ordering::Relaxed).max(1);
let pipe_sr = self.pipeline_sample_rate.max(1) as u64;
let block_frames = ((self.engine_block_frames as u64 * dev_sr as u64 + pipe_sr / 2)
/ pipe_sr) as usize;
if queued + block_frames <= target_frames {
// Less than one block of headroom left in the ring -- the
// worker isn't staying ahead of the device.
if self.primed && queued < self.block_frames {
if self.primed && queued < block_frames {
health::bump(&health::CLOCK_LATE_BLOCKS, 1);
}
return true;
}
self.primed = true;
let overshoot = queued + self.block_frames - target_frames;
let drain = Duration::from_nanos(
(overshoot as u64 * 1_000_000_000) / self.sample_rate.max(1) as u64,
);
let overshoot = queued + block_frames - target_frames;
let drain = Duration::from_nanos((overshoot as u64 * 1_000_000_000) / dev_sr as u64);
thread::sleep(drain.min(FILL_CLOCK_MAX_SLEEP));
}
}

fn sample_rate(&self) -> u32 {
self.sample_rate
self.device_sample_rate.load(Ordering::Relaxed)
}
}
Loading
Loading