From 11817148e45e90820b7cdc0337ed21115b313d58 Mon Sep 17 00:00:00 2001 From: Hung Om <28216544+HungOm@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:41:03 +0800 Subject: [PATCH] Build the engine on macOS The crate did not compile for macOS. HwDriver, HwOptions, MidiHub and HwWorker are imported per target from a backend module -- alsa on Linux, oss on FreeBSD, sndio on OpenBSD, wasapi on Windows -- but the struct fields and signatures that use them are not gated, and macOS had no backend, so every reference was unresolved. discover_midi_hw_devices and hw_profile_backend_label likewise had an arm per target and no macOS arm, leaving their bindings undefined. Add hw::coreaudio as the macOS backend. Maolan has no native CoreAudio driver yet, so HwDriver::new_with_options reports that and points at JACK, which hw::jack already provides on any unix; MidiHub and HwOptions are the shared platform-neutral types the other backends re-export. workers:: coreaudio_worker instantiates the generic HwWorker over it. Also on macOS: - MidiInputWaiter's kqueue implementation is shared with FreeBSD and OpenBSD; macOS has the same kqueue interface, so it applies unchanged. - pthread_setname_np takes only the name and applies to the calling thread, unlike the Linux two-argument form and the BSD pthread_set_name_np. - MIDI endpoints come from CoreMIDI rather than device nodes, so hardware MIDI discovery returns nothing until a CoreMIDI backend exists. - hw::latency and hw::ports exist to support a native driver and have no consumer without one. Every change is either macOS-only or a cfg widened to include macOS, so no other target's build changes. Verified on macOS: clippy --all-targets -D warnings clean, and 344 tests pass with MAOLAN_PLUGIN_HOST pointing at a locally built plugin host. A Linux cross-check could not be run here: jack-sys needs pkg-config configured for cross-compilation. --- src/engine/hardware.rs | 20 ++++- src/engine/mod.rs | 2 + src/engine/runtime.rs | 2 + src/hw/common.rs | 3 + src/hw/config.rs | 2 + src/hw/coreaudio.rs | 147 ++++++++++++++++++++++++++++++++ src/hw/midi_hub.rs | 8 +- src/hw/mod.rs | 4 + src/workers/coreaudio_worker.rs | 18 ++++ src/workers/hw_worker.rs | 4 + src/workers/mod.rs | 2 + 11 files changed, 206 insertions(+), 6 deletions(-) create mode 100644 src/hw/coreaudio.rs create mode 100644 src/workers/coreaudio_worker.rs diff --git a/src/engine/hardware.rs b/src/engine/hardware.rs index 50e69d9..3f73970 100644 --- a/src/engine/hardware.rs +++ b/src/engine/hardware.rs @@ -1,6 +1,8 @@ use super::*; #[cfg(target_os = "linux")] use crate::hw::alsa::{HwDriver, HwOptions, MidiHub}; +#[cfg(target_os = "macos")] +use crate::hw::coreaudio::{HwDriver, HwOptions, MidiHub}; #[cfg(unix)] use crate::hw::jack::JackRuntime; #[cfg(target_os = "windows")] @@ -16,6 +18,8 @@ use crate::hw::traits::HwWorkerDriver; use crate::hw::wasapi::{self, HwDriver}; #[cfg(target_os = "linux")] use crate::workers::alsa_worker::HwWorker; +#[cfg(target_os = "macos")] +use crate::workers::coreaudio_worker::HwWorker; #[cfg(target_os = "freebsd")] use crate::workers::oss_worker::HwWorker; #[cfg(target_os = "openbsd")] @@ -29,6 +33,7 @@ use crate::{ message::{Action, Message}, }; #[cfg(unix)] +#[cfg(all(unix, not(target_os = "macos")))] use std::fs::read_dir; use std::sync::Arc; use tokio::sync::mpsc::channel; @@ -41,7 +46,7 @@ impl Engine { devices } - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "macos")))] pub(crate) fn discover_midi_hw_devices_from_dir(path: &str, prefixes: &[&str]) -> Vec { let devices = read_dir(path) .map(|rd| { @@ -67,6 +72,10 @@ impl Engine { let devices = Self::discover_midi_hw_devices_from_dir("/dev/snd", &["midiC"]); #[cfg(target_os = "openbsd")] let devices = Self::discover_midi_hw_devices_from_dir("/dev", &["midi"]); + // macOS exposes MIDI endpoints through CoreMIDI rather than device + // nodes, and no CoreMIDI backend exists yet. + #[cfg(target_os = "macos")] + let devices = Self::finalize_midi_hw_devices(Vec::new()); #[cfg(target_os = "windows")] let devices = { let mut devices = wasapi::list_midi_input_devices(); @@ -83,7 +92,12 @@ impl Engine { bits: i32, hw_opts: HwOptions, ) -> Result { - #[cfg(any(target_os = "windows", target_os = "freebsd", target_os = "linux"))] + #[cfg(any( + target_os = "windows", + target_os = "freebsd", + target_os = "linux", + target_os = "macos" + ))] { HwDriver::new_with_options(device, _input_device, sample_rate_hz, bits, hw_opts) .map_err(|e| e.to_string()) @@ -104,6 +118,8 @@ impl Engine { let label = "OSS"; #[cfg(target_os = "openbsd")] let label = "sndio"; + #[cfg(target_os = "macos")] + let label = "CoreAudio"; label } diff --git a/src/engine/mod.rs b/src/engine/mod.rs index d1c2c0a..23f5add 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -35,6 +35,8 @@ pub fn parse_automation_lanes( #[cfg(target_os = "linux")] use crate::hw::alsa::{HwDriver, MidiHub}; +#[cfg(target_os = "macos")] +use crate::hw::coreaudio::{HwDriver, MidiHub}; #[cfg(unix)] use crate::hw::jack::JackRuntime; #[cfg(target_os = "freebsd")] diff --git a/src/engine/runtime.rs b/src/engine/runtime.rs index 6635d24..5448801 100644 --- a/src/engine/runtime.rs +++ b/src/engine/runtime.rs @@ -1,6 +1,8 @@ use super::*; #[cfg(target_os = "linux")] use crate::hw::alsa::MidiHub; +#[cfg(target_os = "macos")] +use crate::hw::coreaudio::MidiHub; #[cfg(target_os = "freebsd")] use crate::hw::oss::MidiHub; #[cfg(target_os = "openbsd")] diff --git a/src/hw/common.rs b/src/hw/common.rs index efa75eb..56b9448 100644 --- a/src/hw/common.rs +++ b/src/hw/common.rs @@ -11,6 +11,9 @@ pub fn channel_balance_gain(ch_count: usize, channel_idx: usize, balance: f32) - } } +// Fallback meter for the native drivers. macOS has no native driver yet, but +// the helper stays available to tests so its coverage does not vary by platform. +#[cfg(any(not(target_os = "macos"), test))] pub fn output_meter_linear(port_count: usize, gain: f32, balance: f32) -> Vec { (0..port_count) .map(|channel_idx| 0.0 * gain * channel_balance_gain(port_count, channel_idx, balance)) diff --git a/src/hw/config.rs b/src/hw/config.rs index e19cbee..d5e2bd1 100644 --- a/src/hw/config.rs +++ b/src/hw/config.rs @@ -3,6 +3,8 @@ pub const HW_PROFILE_ENV: &str = "MAOLAN_HW_PROFILE"; pub const OSS_ASSIST_AUTONOMOUS_ENV: &str = "MAOLAN_OSS_ASSIST_AUTONOMOUS"; #[cfg(target_os = "linux")] pub const ALSA_ASSIST_AUTONOMOUS_ENV: &str = "MAOLAN_ALSA_ASSIST_AUTONOMOUS"; +#[cfg(target_os = "macos")] +pub const COREAUDIO_ASSIST_AUTONOMOUS_ENV: &str = "MAOLAN_COREAUDIO_ASSIST_AUTONOMOUS"; #[cfg(target_os = "openbsd")] pub const SNDIO_ASSIST_AUTONOMOUS_ENV: &str = "MAOLAN_SNDIO_ASSIST_AUTONOMOUS"; #[cfg(target_os = "windows")] diff --git a/src/hw/coreaudio.rs b/src/hw/coreaudio.rs new file mode 100644 index 0000000..54e3fd3 --- /dev/null +++ b/src/hw/coreaudio.rs @@ -0,0 +1,147 @@ +//! macOS hardware backend. +//! +//! Maolan has no native CoreAudio driver yet. This module exists so the engine +//! builds and runs on macOS, where audio I/O goes through JACK (see +//! [`crate::hw::jack`]) rather than a native device. Opening a native device +//! reports that clearly instead of failing to compile. +//! +//! MIDI and the option struct are platform-neutral and are re-exported from the +//! shared modules, exactly as the other backends do. + +use crate::audio::io::AudioIO; +use crate::hw::traits::{HwDevice, HwWorkerDriver}; +use std::sync::Arc; + +pub use super::midi_hub::MidiHub; +pub use super::options::HwOptions; + +impl Default for HwOptions { + fn default() -> Self { + Self { + exclusive: false, + period_frames: 1024, + nperiods: 2, + ignore_hwbuf: false, + sync_mode: false, + input_latency_frames: 0, + output_latency_frames: 0, + } + } +} + +pub const UNSUPPORTED: &str = + "Maolan has no native CoreAudio backend yet; run a JACK server and select a JACK device"; + +/// Placeholder native device. [`HwDriver::new_with_options`] always fails, so a +/// value of this type is never constructed; it exists to satisfy the backend +/// surface the engine and [`crate::workers::hw_worker::HwWorker`] expect. +#[derive(Debug)] +pub struct HwDriver { + _private: (), +} + +impl HwDriver { + pub fn new_with_options( + _device: &str, + _input_device: Option<&str>, + _sample_rate_hz: i32, + _bits: i32, + _options: HwOptions, + ) -> Result { + Err(UNSUPPORTED.to_string()) + } +} + +impl HwDriver { + pub fn input_channels(&self) -> usize { + 0 + } + + pub fn output_channels(&self) -> usize { + 0 + } + + pub fn sample_rate(&self) -> i32 { + 0 + } + + pub fn cycle_samples(&self) -> usize { + 0 + } + + pub fn sample_bits(&self) -> i32 { + 0 + } + + pub fn frame_size_bytes(&self) -> usize { + 0 + } + + pub fn latency_ranges(&self) -> ((usize, usize), (usize, usize)) { + ((0, 0), (0, 0)) + } + + pub fn input_port(&self, _idx: usize) -> Option> { + None + } + + pub fn output_port(&self, _idx: usize) -> Option> { + None + } + + pub fn close_fds(&mut self) {} + + pub fn set_playing(&mut self, _playing: bool) {} + + pub fn set_output_gain_balance(&mut self, _gain: f32, _balance: f32) {} +} + +impl HwWorkerDriver for HwDriver { + fn cycle_samples(&self) -> usize { + Self::cycle_samples(self) + } + + fn sample_rate(&self) -> i32 { + Self::sample_rate(self) + } + + fn close_fds(&mut self) { + Self::close_fds(self) + } + + fn set_playing(&mut self, playing: bool) { + Self::set_playing(self, playing) + } + + fn set_output_gain_balance(&mut self, gain: f32, balance: f32) { + Self::set_output_gain_balance(self, gain, balance) + } + + fn run_cycle_for_worker(&mut self) -> Result<(), String> { + Err(UNSUPPORTED.to_string()) + } + + fn run_assist_step_for_worker(&mut self) -> Result { + Err(UNSUPPORTED.to_string()) + } +} + +impl HwDevice for HwDriver { + fn input_channels(&self) -> usize { + Self::input_channels(self) + } + + fn output_channels(&self) -> usize { + Self::output_channels(self) + } + + fn sample_rate(&self) -> i32 { + Self::sample_rate(self) + } + + fn latency_ranges(&self) -> ((usize, usize), (usize, usize)) { + Self::latency_ranges(self) + } +} + +crate::impl_hw_midi_hub_traits!(MidiHub); diff --git a/src/hw/midi_hub.rs b/src/hw/midi_hub.rs index 43cf563..735a8ac 100644 --- a/src/hw/midi_hub.rs +++ b/src/hw/midi_hub.rs @@ -196,7 +196,7 @@ struct MidiInputDevice { parser: MidiParser, } -#[cfg(any(target_os = "freebsd", target_os = "openbsd"))] +#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "macos"))] #[derive(Debug)] struct MidiInputWaiter { kq: i32, @@ -205,7 +205,7 @@ struct MidiInputWaiter { wake_write_fd: RawFd, } -#[cfg(any(target_os = "freebsd", target_os = "openbsd"))] +#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "macos"))] impl MidiInputWaiter { fn new() -> Result { let kq = unsafe { libc::kqueue() }; @@ -325,14 +325,14 @@ impl MidiInputWaiter { } } -#[cfg(any(target_os = "freebsd", target_os = "openbsd"))] +#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "macos"))] impl Drop for MidiInputWaiter { fn drop(&mut self) { self.close(); } } -#[cfg(any(target_os = "freebsd", target_os = "openbsd"))] +#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "macos"))] unsafe impl Send for MidiInputWaiter {} #[cfg(target_os = "linux")] diff --git a/src/hw/mod.rs b/src/hw/mod.rs index ada75db..56392c2 100644 --- a/src/hw/mod.rs +++ b/src/hw/mod.rs @@ -3,15 +3,19 @@ pub mod alsa; pub mod common; pub mod config; pub mod convert_policy; +#[cfg(target_os = "macos")] +pub mod coreaudio; pub mod error_fmt; #[cfg(unix)] pub mod jack; +#[cfg(not(target_os = "macos"))] pub mod latency; #[cfg(unix)] pub mod midi_hub; pub mod options; #[cfg(target_os = "freebsd")] pub mod oss; +#[cfg(not(target_os = "macos"))] pub mod ports; #[cfg(target_os = "openbsd")] pub mod sndio; diff --git a/src/workers/coreaudio_worker.rs b/src/workers/coreaudio_worker.rs new file mode 100644 index 0000000..574eab7 --- /dev/null +++ b/src/workers/coreaudio_worker.rs @@ -0,0 +1,18 @@ +use super::hw_worker::Backend; +use crate::hw::config; +use crate::hw::coreaudio; + +#[derive(Debug)] +pub struct CoreAudioBackend; + +impl Backend for CoreAudioBackend { + type Driver = coreaudio::HwDriver; + type MidiHub = coreaudio::MidiHub; + + const LABEL: &'static str = "CoreAudio"; + const WORKER_THREAD_NAME: &'static str = "coreaudio-worker"; + const ASSIST_THREAD_NAME: &'static str = "coreaudio-assist"; + const ASSIST_AUTONOMOUS_ENV: &'static str = config::COREAUDIO_ASSIST_AUTONOMOUS_ENV; +} + +pub type HwWorker = super::hw_worker::HwWorker; diff --git a/src/workers/hw_worker.rs b/src/workers/hw_worker.rs index 5da899c..1d93b44 100644 --- a/src/workers/hw_worker.rs +++ b/src/workers/hw_worker.rs @@ -80,6 +80,10 @@ impl HwWorker { unsafe { libc::pthread_set_name_np(thread, c_name.as_ptr()); } + #[cfg(target_os = "macos")] + unsafe { + let _ = libc::pthread_setname_np(c_name.as_ptr()); + } let param = unsafe { let mut p = std::mem::zeroed::(); diff --git a/src/workers/mod.rs b/src/workers/mod.rs index 4ab0e51..79c7065 100644 --- a/src/workers/mod.rs +++ b/src/workers/mod.rs @@ -1,5 +1,7 @@ #[cfg(target_os = "linux")] pub mod alsa_worker; +#[cfg(target_os = "macos")] +pub mod coreaudio_worker; pub mod hw_worker; #[cfg(target_os = "freebsd")] pub mod oss_worker;