// 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.
///|
/// ALSA backend (Linux).
///
/// Configuration queries are best-effort and are implemented via libasound `hw_params` probing.
///|
pub struct Host {
_unit : Unit
} derive(Debug, Eq)
///|
pub struct Device {
id : String
desc : String?
direction : @core.DeviceDirection
} derive(Debug, Eq)
///|
pub fn default_host() -> Host {
{ _unit: () }
}
///|
fn default_device() -> Device {
// Mirrors typical ALSA "default" PCM.
{ id: "default", desc: None, direction: @core.DeviceDirection::duplex() }
}
///|
pub fn Host::devices(_self : Host) -> Array[Device] {
if @core.native_os() == Linux {
let bytes = devices_utf8()
let s = @core.utf8_bytes_to_string(bytes, bytes.length())
let out : Array[Device] = []
for v in s[:].split("\n"[:]) {
let id = v.trim().to_owned()
if id.length() > 0 {
// Each line is: "\t\t".
let fields = Array::new()
for p in id.split("\t") {
fields.push(p)
}
let pcm_id = if fields.length() > 0 {
fields[0].trim().to_owned()
} else {
"".to_string()
}
if pcm_id.length() == 0 {
continue
}
let dir_tag = if fields.length() > 1 {
fields[1].trim().to_owned()
} else {
"?".to_string()
}
let desc_esc = if fields.length() > 2 {
fields[2].to_owned()
} else {
""
}
let dir = match dir_tag {
"i" => @core.DeviceDirection::input()
"o" => @core.DeviceDirection::output()
"d" => @core.DeviceDirection::duplex()
_ => @core.DeviceDirection::unknown()
}
let desc = if desc_esc.length() == 0 {
None
} else {
Some(unescape(desc_esc))
}
out.push({ id: pcm_id, desc, direction: dir })
}
}
if out.length() == 0 {
return [default_device()]
}
out
} else {
[]
}
}
///|
pub fn Host::default_output_device(_self : Host) -> Device? {
if @core.native_os() == Linux {
// Match upstream CPAL ALSA host: always return the "default" PCM, even though the actual
// availability of output formats can only be determined by attempting to open a stream.
Some(default_device())
} else {
None
}
}
///|
pub fn Host::default_input_device(_self : Host) -> Device? {
if @core.native_os() == Linux {
// Match upstream CPAL ALSA host: always return the "default" PCM (see above).
Some(default_device())
} else {
None
}
}
///|
pub fn Device::name(_self : Device) -> String {
if @core.native_os() != Linux {
return _self.id
}
// Match upstream ALSA: `name()` returns the PCM id.
_self.id
}
///|
pub fn Device::description(
_self : Device,
) -> @core.DeviceDescription raise @core.DeviceNameError {
let name = match _self.desc {
None => _self.id
Some(desc) => {
let mut first = _self.id
for line in desc[:].split("\n"[:]) {
let s = line.trim().to_owned()
if s.length() > 0 {
first = s
break
}
}
first
}
}
if name.length() == 0 {
raise @core.device_name_error_backend_specific(
"alsa device description: empty name",
)
}
let extended : Array[String] = []
match _self.desc {
None => ()
Some(desc) =>
for line in desc[:].split("\n"[:]) {
let t = line.trim().to_owned()
if t.length() > 0 {
extended.push(t)
}
}
}
DeviceDescription(
name,
driver=Some(_self.id),
device_type=@core.DeviceType::unknown(),
interface_type=@core.InterfaceType::unknown(),
direction=_self.direction,
address=None,
extended~,
)
}
///|
pub fn Device::id(_self : Device) -> @core.DeviceId {
DeviceId(Alsa, _self.id)
}
///|
pub fn Device::supports_input(_self : Device) -> Bool {
match _self.direction {
Input => true
Duplex => true
Unknown => true
_ => false
}
}
///|
pub fn Device::supports_output(_self : Device) -> Bool {
match _self.direction {
Output => true
Duplex => true
Unknown => true
_ => false
}
}
///|
fn unescape(s : String) -> String {
let sb = StringBuilder::new()
let mut i = 0
while i < s.length() {
let cu = s.code_unit_at(i).to_int()
if cu == 92 && i + 1 < s.length() {
let n = s.code_unit_at(i + 1).to_int()
if n == 110 {
sb.write_char((10 : Int).unsafe_to_char())
i = i + 2
continue
}
if n == 116 {
sb.write_char((9 : Int).unsafe_to_char())
i = i + 2
continue
}
if n == 92 {
sb.write_char((92 : Int).unsafe_to_char())
i = i + 2
continue
}
}
sb.write_char(cu.unsafe_to_char())
i = i + 1
}
sb.to_string()
}
///|
pub fn Device::supported_input_configs(
_self : Device,
) -> Array[@core.SupportedStreamConfigRange] raise @core.SupportedStreamConfigsError {
if @core.native_os() != Linux {
return []
}
if !_self.supports_input() {
return []
}
supported_configs(_self, true)
}
///|
pub fn Device::supported_output_configs(
_self : Device,
) -> Array[@core.SupportedStreamConfigRange] raise @core.SupportedStreamConfigsError {
if @core.native_os() != Linux {
return []
}
if !_self.supports_output() {
return []
}
supported_configs(_self, false)
}
///|
pub fn Device::default_input_config(
_self : Device,
) -> @core.SupportedStreamConfig raise @core.DefaultStreamConfigError {
if @core.native_os() != Linux {
raise @core.default_stream_config_error_backend_specific(
"alsa default_input_config: not available on this OS",
)
}
if !_self.supports_input() {
raise @core.default_stream_config_error_stream_type_not_supported()
}
default_config(_self, true)
}
///|
pub fn Device::default_output_config(
_self : Device,
) -> @core.SupportedStreamConfig raise @core.DefaultStreamConfigError {
if @core.native_os() != Linux {
raise @core.default_stream_config_error_backend_specific(
"alsa default_output_config: not available on this OS",
)
}
if !_self.supports_output() {
raise @core.default_stream_config_error_stream_type_not_supported()
}
default_config(_self, false)
}
///|
fn u32_le_to_int(b : Bytes, off : Int) -> Int {
if off + 4 > b.length() {
return 0
}
let b0 = b[off].to_int()
let b1 = b[off + 1].to_int()
let b2 = b[off + 2].to_int()
let b3 = b[off + 3].to_int()
b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
}
///|
fn i32_le_to_int(b : Bytes, off : Int) -> Int {
// `Int` is i32 on native targets; our `u32_le_to_int` already reconstructs the 32-bit
// bit-pattern, which naturally yields an i32 value (including negative errno-style values).
u32_le_to_int(b, off)
}
///|
fn sample_format_from_bin_tag(tag : Int) -> @core.SampleFormat? {
match tag {
1 => Some(I8)
2 => Some(U8)
3 => Some(I16)
4 => Some(U16)
5 => Some(I24)
6 => Some(U24)
7 => Some(I32)
8 => Some(U32)
9 => Some(F32)
10 => Some(F64)
11 => Some(DsdU8)
12 => Some(DsdU16)
13 => Some(DsdU32)
_ => None
}
}
///|
fn parse_supported_configs_bin(
b : Bytes,
) -> Array[@core.SupportedStreamConfigRange] {
if b.length() < 8 {
return []
}
// Header: i32 status, u32 record_count.
let n = u32_le_to_int(b, 4)
if n <= 0 {
return []
}
let out : Array[@core.SupportedStreamConfigRange] = []
let mut off = 8
for _i in 0.. b.length() {
break
}
let tag = u32_le_to_int(b, off)
let ch = u32_le_to_int(b, off + 4)
let min_sr = u32_le_to_int(b, off + 8)
let max_sr = u32_le_to_int(b, off + 12)
let buf_min = u32_le_to_int(b, off + 16)
let buf_max = u32_le_to_int(b, off + 20)
off = off + 24
match sample_format_from_bin_tag(tag) {
None => ()
Some(fmt) => {
let buf = @core.SupportedBufferSize::Range(
min=if buf_min > 0 { buf_min } else { 1 },
max=if buf_max >= buf_min && buf_max > 0 { buf_max } else { buf_min },
)
out.push(SupportedStreamConfigRange(ch, min_sr, max_sr, buf, fmt))
}
}
}
out
}
///|
fn supported_configs(
d : Device,
is_input : Bool,
) -> Array[@core.SupportedStreamConfigRange] raise @core.SupportedStreamConfigsError {
let b = supported_configs_bin(d.id, is_input)
if b.length() == 0 {
return []
}
if b.length() < 8 {
raise @core.supported_stream_configs_error_backend_specific(
"alsa supported_configs: malformed header",
)
}
let status = i32_le_to_int(b, 0)
if status != 0 {
if status == -19 {
raise @core.supported_stream_configs_error_device_not_available()
} else if status == -16 {
raise @core.supported_stream_configs_error_device_busy()
} else if status == -22 {
raise @core.supported_stream_configs_error_invalid_argument()
} else {
raise @core.supported_stream_configs_error_backend_specific(
"alsa supported_configs: errno \{status}",
)
}
}
parse_supported_configs_bin(b)
}
///|
fn default_config(
d : Device,
is_input : Bool,
) -> @core.SupportedStreamConfig raise @core.DefaultStreamConfigError {
let ranges = try supported_configs(d, is_input) catch {
DeviceNotAvailable =>
raise @core.default_stream_config_error_device_not_available()
DeviceBusy => raise @core.default_stream_config_error_device_busy()
InvalidArgument =>
raise @core.default_stream_config_error_stream_type_not_supported()
BackendSpecific(err) =>
raise @core.default_stream_config_error_backend_specific(
err.description(),
)
} noraise {
xs => xs
}
if ranges.length() == 0 {
raise @core.default_stream_config_error_stream_type_not_supported()
}
// Mirror upstream ALSA default selection:
// - Choose the "best" supported range using heuristics.
// - Use 44100Hz when within the chosen range, otherwise the max sample rate.
let mut best = ranges[0]
for r in ranges {
if r.cmp_default_heuristics(best) > 0 {
best = r
}
}
let hz_44100 = 44100
if best.min_sample_rate() <= hz_44100 && hz_44100 <= best.max_sample_rate() {
best.with_sample_rate(hz_44100)
} else {
best.with_max_sample_rate()
}
}