// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// CoreAudio device discovery (macOS, native backend only).
///
/// This is intentionally a small, testable slice: device enumeration and names.
pub type DeviceId = UInt
///|
extern "C" fn ca_default_output_device_id() -> UInt = "moon_cpal_ca_default_output_device_id"
///|
extern "C" fn ca_default_input_device_id() -> UInt = "moon_cpal_ca_default_input_device_id"
///|
extern "C" fn ca_device_count() -> Int = "moon_cpal_ca_device_count"
///|
#borrow(out)
extern "C" fn ca_get_devices(out : FixedArray[UInt], max : Int) -> Int = "moon_cpal_ca_get_devices"
///|
extern "C" fn ca_device_name_utf8_len(device_id : UInt) -> Int = "moon_cpal_ca_device_name_utf8_len"
///|
#borrow(out)
extern "C" fn ca_device_name_utf8(
device_id : UInt,
out : Bytes,
out_len : Int,
) -> Int = "moon_cpal_ca_device_name_utf8"
///|
extern "C" fn ca_device_uid_utf8_len(device_id : UInt) -> Int = "moon_cpal_ca_device_uid_utf8_len"
///|
#borrow(out)
extern "C" fn ca_device_uid_utf8(
device_id : UInt,
out : Bytes,
out_len : Int,
) -> Int = "moon_cpal_ca_device_uid_utf8"
///|
extern "C" fn ca_device_manufacturer_utf8_len(device_id : UInt) -> Int = "moon_cpal_ca_device_manufacturer_utf8_len"
///|
#borrow(out)
extern "C" fn ca_device_manufacturer_utf8(
device_id : UInt,
out : Bytes,
out_len : Int,
) -> Int = "moon_cpal_ca_device_manufacturer_utf8"
///|
extern "C" fn ca_device_transport_type_u32(device_id : UInt) -> UInt = "moon_cpal_ca_device_transport_type_u32"
///|
extern "C" fn ca_device_is_aggregate(device_id : UInt) -> Int = "moon_cpal_ca_device_is_aggregate"
///|
extern "C" fn ca_device_interface_type_tag(device_id : UInt) -> Int = "moon_cpal_ca_device_interface_type_tag"
///|
extern "C" fn ca_input_channel_count(device_id : UInt) -> Int = "moon_cpal_ca_input_channel_count"
///|
extern "C" fn ca_output_channel_count(device_id : UInt) -> Int = "moon_cpal_ca_output_channel_count"
///|
extern "C" fn ca_input_sample_rate_ranges_count(device_id : UInt) -> Int = "moon_cpal_ca_input_sample_rate_ranges_count"
///|
extern "C" fn ca_output_sample_rate_ranges_count(device_id : UInt) -> Int = "moon_cpal_ca_output_sample_rate_ranges_count"
///|
#borrow(out_mins, out_maxs)
extern "C" fn ca_input_sample_rate_ranges(
device_id : UInt,
out_mins : FixedArray[Double],
out_maxs : FixedArray[Double],
max : Int,
) -> Int = "moon_cpal_ca_input_sample_rate_ranges"
///|
#borrow(out_mins, out_maxs)
extern "C" fn ca_output_sample_rate_ranges(
device_id : UInt,
out_mins : FixedArray[Double],
out_maxs : FixedArray[Double],
max : Int,
) -> Int = "moon_cpal_ca_output_sample_rate_ranges"
///|
#borrow(out)
extern "C" fn ca_buffer_frame_size_range(
device_id : UInt,
out : FixedArray[UInt],
out_len : Int,
) -> Int = "moon_cpal_ca_buffer_frame_size_range"
///|
#borrow(out)
extern "C" fn ca_default_stream_config(
device_id : UInt,
input : Int,
out : FixedArray[UInt],
out_len : Int,
) -> Int = "moon_cpal_ca_default_stream_config"
///|
extern "C" fn ca_set_nominal_sample_rate(
device_id : UInt,
sample_rate : Double,
) -> Int = "moon_cpal_ca_set_nominal_sample_rate"
///|
extern "C" fn ca_osstatus_kind(status : Int) -> Int = "moon_cpal_ca_osstatus_kind"
///|
fn osstatus_kind(status : Int) -> Int {
ca_osstatus_kind(status)
}
///|
fn utf8_bytes_to_mbt_string(bytes : Bytes, length : Int) -> String {
let res = StringBuilder::new()
let len = if length < bytes.length() { length } else { bytes.length() }
let mut i = 0
while i < len {
let mut c = bytes[i].to_int()
if c < 0x80 {
res.write_char(c.unsafe_to_char())
i += 1
} else if c < 0xE0 {
if i + 1 >= len {
break
}
c = ((c & 0x1F) << 6) | (bytes[i + 1].to_int() & 0x3F)
res.write_char(c.unsafe_to_char())
i += 2
} else if c < 0xF0 {
if i + 2 >= len {
break
}
c = ((c & 0x0F) << 12) |
((bytes[i + 1].to_int() & 0x3F) << 6) |
(bytes[i + 2].to_int() & 0x3F)
res.write_char(c.unsafe_to_char())
i += 3
} else {
if i + 3 >= len {
break
}
c = ((c & 0x07) << 18) |
((bytes[i + 1].to_int() & 0x3F) << 12) |
((bytes[i + 2].to_int() & 0x3F) << 6) |
(bytes[i + 3].to_int() & 0x3F)
c -= 0x10000
res.write_char(((c >> 10) + 0xD800).unsafe_to_char())
res.write_char(((c & 0x3FF) + 0xDC00).unsafe_to_char())
i += 4
}
}
res.to_string()
}
///|
fn get_buffer_frame_size_range(
device_id : DeviceId,
) -> @core.SupportedBufferSize raise CoreAudioError {
let out = FixedArray::make(2, (0 : UInt))
let st = ca_buffer_frame_size_range(device_id, out, 2)
if st < 0 {
raise_osstatus("ca_buffer_frame_size_range", st)
}
Range(min=out[0].reinterpret_as_int(), max=out[1].reinterpret_as_int())
}
///|
fn get_buffer_frame_size_range_default(
device_id : DeviceId,
) -> @core.SupportedBufferSize raise @core.DefaultStreamConfigError {
let out = FixedArray::make(2, (0 : UInt))
let st = ca_buffer_frame_size_range(device_id, out, 2)
if st < 0 {
raise_default_osstatus("ca_buffer_frame_size_range", st)
}
Range(min=out[0].reinterpret_as_int(), max=out[1].reinterpret_as_int())
}
///|
fn default_stream_config(
device_id : DeviceId,
input~ : Bool,
) -> @core.SupportedStreamConfig raise @core.DefaultStreamConfigError {
// Layout: [sample_rate, channels, sample_format_tag]
// - sample_format_tag: 1 => F32, 2 => I16, other => unsupported
let out = FixedArray::make(3, (0 : UInt))
let st = ca_default_stream_config(
device_id,
if input {
1
} else {
0
},
out,
3,
)
if st < 0 {
raise_default_osstatus("ca_default_stream_config", st)
}
let sample_rate = out[0].reinterpret_as_int()
let channels = out[1].reinterpret_as_int()
let sample_format = match out[2] {
1 => @core.SampleFormat::F32
2 => I16
_ => raise @core.default_stream_config_error_stream_type_not_supported()
}
let buffer_size = get_buffer_frame_size_range_default(device_id)
SupportedStreamConfig(channels, sample_rate, buffer_size, sample_format)
}
///|
fn set_nominal_sample_rate(
device_id : DeviceId,
sample_rate : Int,
) -> Unit raise CoreAudioError {
if sample_rate <= 0 {
raise CoreAudioError("ca_set_nominal_sample_rate", -50)
}
let st = ca_set_nominal_sample_rate(device_id, sample_rate.to_double())
if st < 0 {
raise_osstatus("ca_set_nominal_sample_rate", st)
}
}
///|
fn sample_rate_ranges(
device_id : DeviceId,
input~ : Bool,
) -> Array[(Double, Double)] raise CoreAudioError {
let n = if input {
ca_input_sample_rate_ranges_count(device_id)
} else {
ca_output_sample_rate_ranges_count(device_id)
}
if n < 0 {
raise_osstatus("ca_sample_rate_ranges_count", n)
}
if n == 0 {
return []
}
let mins = FixedArray::make(n, 0.0)
let maxs = FixedArray::make(n, 0.0)
let got = if input {
ca_input_sample_rate_ranges(device_id, mins, maxs, n)
} else {
ca_output_sample_rate_ranges(device_id, mins, maxs, n)
}
if got < 0 {
raise_osstatus("ca_sample_rate_ranges", got)
}
let got = if got < n { got } else { n }
let out = Array::new(capacity=got)
for i in 0.. Array[@core.SupportedStreamConfigRange] raise CoreAudioError {
let channels = if input {
ca_input_channel_count(device_id)
} else {
ca_output_channel_count(device_id)
}
if channels < 0 {
raise_osstatus(
if input {
"ca_input_channel_count"
} else {
"ca_output_channel_count"
},
channels,
)
}
if channels == 0 {
return []
}
// NOTE: Upstream cpal currently returns F32 for macOS supported configs.
let sample_format = @core.SampleFormat::F32
let buffer_size = get_buffer_frame_size_range(device_id)
let ranges = sample_rate_ranges(device_id, input~)
if ranges.is_empty() {
return []
}
let mut contains_different = false
for i in 0.. max_sr {
max_sr = mx
}
}
[
SupportedStreamConfigRange(
channels, min_sr, max_sr, buffer_size, sample_format,
),
]
}
}
///|
pub fn default_output_device_id() -> DeviceId? {
let id = ca_default_output_device_id()
if id == 0 {
None
} else {
Some(id)
}
}
///|
pub fn default_input_device_id() -> DeviceId? {
let id = ca_default_input_device_id()
if id == 0 {
None
} else {
Some(id)
}
}
///|
pub fn device_ids() -> Array[DeviceId] raise CoreAudioError {
let n = ca_device_count()
if n < 0 {
raise_osstatus("ca_device_count", n)
}
if n == 0 {
return []
}
let buf = FixedArray::make(n, (0 : UInt))
let got = ca_get_devices(buf, n)
if got < 0 {
raise_osstatus("ca_get_devices", got)
}
if got == 0 {
[]
} else if got == n {
buf[:].to_owned()
} else {
buf[:got].to_owned()
}
}
///|
pub fn device_name(device_id : DeviceId) -> String raise CoreAudioError {
let need = ca_device_name_utf8_len(device_id)
if need < 0 {
raise_osstatus("ca_device_name_utf8_len", need)
}
let mut out = Bytes::new(need)
let mut wrote = ca_device_name_utf8(device_id, out, need)
if wrote < 0 {
raise_osstatus("ca_device_name_utf8", wrote)
}
// Some CoreAudio strings may change between the length probe and the copy.
// If the buffer was too small, the C stub returns the required length.
if wrote > out.length() {
out = Bytes::new(wrote)
wrote = ca_device_name_utf8(device_id, out, wrote)
if wrote < 0 {
raise_osstatus("ca_device_name_utf8 (retry)", wrote)
}
if wrote > out.length() {
raise_osstatus("ca_device_name_utf8 (retry overflow)", -1)
}
}
utf8_bytes_to_mbt_string(out, wrote)
}
///|
pub fn device_uid(device_id : DeviceId) -> String? {
let need = ca_device_uid_utf8_len(device_id)
if need <= 0 {
return None
}
let mut out = Bytes::new(need)
let mut wrote = ca_device_uid_utf8(device_id, out, need)
if wrote <= 0 {
return None
}
if wrote > out.length() {
out = Bytes::new(wrote)
wrote = ca_device_uid_utf8(device_id, out, wrote)
if wrote <= 0 || wrote > out.length() {
return None
}
}
Some(utf8_bytes_to_mbt_string(out, wrote))
}
///|
pub fn device_manufacturer(device_id : DeviceId) -> String? {
let need = ca_device_manufacturer_utf8_len(device_id)
if need <= 0 {
return None
}
let mut out = Bytes::new(need)
let mut wrote = ca_device_manufacturer_utf8(device_id, out, need)
if wrote <= 0 {
return None
}
if wrote > out.length() {
out = Bytes::new(wrote)
wrote = ca_device_manufacturer_utf8(device_id, out, wrote)
if wrote <= 0 || wrote > out.length() {
return None
}
}
Some(utf8_bytes_to_mbt_string(out, wrote))
}
///|
pub fn device_transport_type_u32(device_id : DeviceId) -> UInt {
ca_device_transport_type_u32(device_id)
}
///|
pub fn device_is_aggregate(device_id : DeviceId) -> Bool {
ca_device_is_aggregate(device_id) > 0
}
///|
pub fn device_interface_type_tag(device_id : DeviceId) -> Int {
ca_device_interface_type_tag(device_id)
}
///|
pub fn supported_input_configs(
device_id : DeviceId,
) -> Array[@core.SupportedStreamConfigRange] raise CoreAudioError {
supported_configs(device_id, input=true)
}
///|
pub fn supported_output_configs(
device_id : DeviceId,
) -> Array[@core.SupportedStreamConfigRange] raise CoreAudioError {
supported_configs(device_id, input=false)
}
///|
pub fn default_input_config(
device_id : DeviceId,
) -> @core.SupportedStreamConfig raise @core.DefaultStreamConfigError {
default_stream_config(device_id, input=true)
}
///|
pub fn default_output_config(
device_id : DeviceId,
) -> @core.SupportedStreamConfig raise @core.DefaultStreamConfigError {
default_stream_config(device_id, input=false)
}