diff --git a/CHANGELOG.md b/CHANGELOG.md index e522d1849..0fe4cb9bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,18 @@ # Unreleased - Add `Sample::bits_per_sample` method. -- ALSA(process_output): Pass `silent=true` to `PCM.try_recover`, so it doesn't write to stderr. - ALSA: Fix `BufferSize::Fixed` by selecting the nearest supported frame count. - ALSA: Change `BufferSize::Default` to use the device defaults. - ALSA: Change card enumeration to work like `aplay -L` does. +- ALSA: Add `I24` and `U24` sample format support (24-bit samples stored in 4 bytes). +- ALSA(process_output): Pass `silent=true` to `PCM.try_recover`, so it doesn't write to stderr. - ASIO: Fix linker flags for MinGW cross-compilation. - CoreAudio: Change `Device::supported_configs` to return a single element containing the available sample rate range when all elements have the same `mMinimum` and `mMaximum` values. - CoreAudio: Change default audio device detection to be lazy when building a stream, instead of during device enumeration. +- CoreAudio: Add `i8`, `i32` and `I24` sample format support (24-bit samples stored in 4 bytes). - iOS: Fix example by properly activating audio session. - WASAPI: Expose `IMMDevice` from WASAPI host Device. +- WASAPI: Add `I24` and `U24` sample format support (24-bit samples stored in 4 bytes). # Version 0.16.0 (2025-06-07) diff --git a/examples/synth_tones.rs b/examples/synth_tones.rs index 8dab746d6..fb641b815 100644 --- a/examples/synth_tones.rs +++ b/examples/synth_tones.rs @@ -8,7 +8,7 @@ extern crate cpal; use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, - SizedSample, I24, + SizedSample, I24, U24, }; use cpal::{FromSample, Sample}; @@ -103,6 +103,7 @@ where cpal::SampleFormat::I64 => make_stream::(&device, &config.into()), cpal::SampleFormat::U8 => make_stream::(&device, &config.into()), cpal::SampleFormat::U16 => make_stream::(&device, &config.into()), + cpal::SampleFormat::U24 => make_stream::(&device, &config.into()), cpal::SampleFormat::U32 => make_stream::(&device, &config.into()), cpal::SampleFormat::U64 => make_stream::(&device, &config.into()), cpal::SampleFormat::F32 => make_stream::(&device, &config.into()), diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index b1a29f348..8ba9e61a8 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -328,16 +328,16 @@ impl Device { let hw_params = alsa::pcm::HwParams::any(handle)?; // TODO: check endianness - const FORMATS: [(SampleFormat, alsa::pcm::Format); 8] = [ + const FORMATS: [(SampleFormat, alsa::pcm::Format); 10] = [ (SampleFormat::I8, alsa::pcm::Format::S8), (SampleFormat::U8, alsa::pcm::Format::U8), (SampleFormat::I16, alsa::pcm::Format::S16LE), //SND_PCM_FORMAT_S16_BE, (SampleFormat::U16, alsa::pcm::Format::U16LE), //SND_PCM_FORMAT_U16_BE, - //SND_PCM_FORMAT_S24_LE, + (SampleFormat::I24, alsa::pcm::Format::S24LE), //SND_PCM_FORMAT_S24_BE, - //SND_PCM_FORMAT_U24_LE, + (SampleFormat::U24, alsa::pcm::Format::U24LE), //SND_PCM_FORMAT_U24_BE, (SampleFormat::I32, alsa::pcm::Format::S32LE), //SND_PCM_FORMAT_S32_BE, @@ -1072,13 +1072,13 @@ fn set_hw_params_from_format( match sample_format { SampleFormat::I8 => alsa::pcm::Format::S8, SampleFormat::I16 => alsa::pcm::Format::S16BE, - // SampleFormat::I24 => alsa::pcm::Format::S24BE, + SampleFormat::I24 => alsa::pcm::Format::S24BE, SampleFormat::I32 => alsa::pcm::Format::S32BE, // SampleFormat::I48 => alsa::pcm::Format::S48BE, // SampleFormat::I64 => alsa::pcm::Format::S64BE, SampleFormat::U8 => alsa::pcm::Format::U8, SampleFormat::U16 => alsa::pcm::Format::U16BE, - // SampleFormat::U24 => alsa::pcm::Format::U24BE, + SampleFormat::U24 => alsa::pcm::Format::U24BE, SampleFormat::U32 => alsa::pcm::Format::U32BE, // SampleFormat::U48 => alsa::pcm::Format::U48BE, // SampleFormat::U64 => alsa::pcm::Format::U64BE, @@ -1096,13 +1096,13 @@ fn set_hw_params_from_format( match sample_format { SampleFormat::I8 => alsa::pcm::Format::S8, SampleFormat::I16 => alsa::pcm::Format::S16LE, - // SampleFormat::I24 => alsa::pcm::Format::S24LE, + SampleFormat::I24 => alsa::pcm::Format::S24LE, SampleFormat::I32 => alsa::pcm::Format::S32LE, // SampleFormat::I48 => alsa::pcm::Format::S48LE, // SampleFormat::I64 => alsa::pcm::Format::S64LE, SampleFormat::U8 => alsa::pcm::Format::U8, SampleFormat::U16 => alsa::pcm::Format::U16LE, - // SampleFormat::U24 => alsa::pcm::Format::U24LE, + SampleFormat::U24 => alsa::pcm::Format::U24LE, SampleFormat::U32 => alsa::pcm::Format::U32LE, // SampleFormat::U48 => alsa::pcm::Format::U48LE, // SampleFormat::U64 => alsa::pcm::Format::U64LE, diff --git a/src/host/coreaudio/macos/mod.rs b/src/host/coreaudio/macos/mod.rs index c0809fa2f..d83734d92 100644 --- a/src/host/coreaudio/macos/mod.rs +++ b/src/host/coreaudio/macos/mod.rs @@ -237,8 +237,8 @@ impl Device { n_channels += buffer.mNumberChannels as usize; } - // TODO: macOS should support U8, I16, I32, F32 and F64. This should allow for using - // I16 but just use F32 for now as it's the default anyway. + // TODO: macOS should support I8, I16, I24, I32, F32 and F64. This should allow for + // using I16 but just use F32 for now as it's the default anyway. let sample_format = SampleFormat::F32; // Get available sample rate ranges. @@ -404,7 +404,10 @@ impl Device { ); match maybe_sample_format { Some(coreaudio::audio_unit::SampleFormat::F32) => SampleFormat::F32, + Some(coreaudio::audio_unit::SampleFormat::I8) => SampleFormat::I8, Some(coreaudio::audio_unit::SampleFormat::I16) => SampleFormat::I16, + Some(coreaudio::audio_unit::SampleFormat::I24) => SampleFormat::I24, + Some(coreaudio::audio_unit::SampleFormat::I32) => SampleFormat::I32, _ => return Err(DefaultStreamConfigError::StreamTypeNotSupported), } }; diff --git a/src/host/coreaudio/mod.rs b/src/host/coreaudio/mod.rs index a22b86ee6..4310b6940 100644 --- a/src/host/coreaudio/mod.rs +++ b/src/host/coreaudio/mod.rs @@ -51,9 +51,11 @@ fn asbd_from_config( let bytes_per_packet = frames_per_packet * bytes_per_frame; let format_flags = match sample_format { SampleFormat::F32 | SampleFormat::F64 => kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked, - SampleFormat::I16 | SampleFormat::I32 | SampleFormat::I64 => { - kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked - } + SampleFormat::I8 + | SampleFormat::I16 + | SampleFormat::I24 + | SampleFormat::I32 + | SampleFormat::I64 => kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked, _ => kAudioFormatFlagIsPacked, }; AudioStreamBasicDescription { diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 4e4cf14b8..bce6bc0cd 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -205,6 +205,7 @@ unsafe fn format_from_waveformatex_ptr( match n_bits { 8 => SampleFormat::U8, 16 => SampleFormat::I16, + 24 => SampleFormat::I24, 32 => SampleFormat::I32, 64 => SampleFormat::I64, _ => return None, @@ -426,6 +427,8 @@ impl Device { for sample_format in [ SampleFormat::U8, SampleFormat::I16, + SampleFormat::I24, + SampleFormat::U24, SampleFormat::I32, SampleFormat::I64, SampleFormat::F32, @@ -948,9 +951,11 @@ fn config_to_waveformatextensible( let format_tag = match sample_format { SampleFormat::U8 | SampleFormat::I16 => Audio::WAVE_FORMAT_PCM, - SampleFormat::I32 | SampleFormat::I64 | SampleFormat::F32 => { - KernelStreaming::WAVE_FORMAT_EXTENSIBLE - } + SampleFormat::I24 + | SampleFormat::U24 + | SampleFormat::I32 + | SampleFormat::I64 + | SampleFormat::F32 => KernelStreaming::WAVE_FORMAT_EXTENSIBLE, _ => return None, }; @@ -959,7 +964,11 @@ fn config_to_waveformatextensible( let sample_bytes = sample_format.sample_size() as u16; let avg_bytes_per_sec = u32::from(channels) * sample_rate * u32::from(sample_bytes); let block_align = channels * sample_bytes; - let bits_per_sample = 8 * sample_bytes; + let bits_per_sample = match sample_format { + // 24-bit formats use 32-bit storage but only 24 valid bits + SampleFormat::I24 | SampleFormat::U24 => 24, + _ => 8 * sample_bytes, + }; let cb_size = if format_tag == Audio::WAVE_FORMAT_PCM { 0 @@ -983,9 +992,12 @@ fn config_to_waveformatextensible( let channel_mask = KernelStreaming::KSAUDIO_SPEAKER_DIRECTOUT; let sub_format = match sample_format { - SampleFormat::U8 | SampleFormat::I16 | SampleFormat::I32 | SampleFormat::I64 => { - KernelStreaming::KSDATAFORMAT_SUBTYPE_PCM - } + SampleFormat::U8 + | SampleFormat::I16 + | SampleFormat::I24 + | SampleFormat::U24 + | SampleFormat::I32 + | SampleFormat::I64 => KernelStreaming::KSDATAFORMAT_SUBTYPE_PCM, SampleFormat::F32 => Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, _ => return None, diff --git a/src/lib.rs b/src/lib.rs index efea3a379..bfcd61f7a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -704,7 +704,7 @@ impl SupportedStreamConfigRange { /// - Max sample rate pub fn cmp_default_heuristics(&self, other: &Self) -> std::cmp::Ordering { use std::cmp::Ordering::Equal; - use SampleFormat::{F32, I16, U16}; + use SampleFormat::{F32, I16, I24, I32, U16, U24, U32}; let cmp_stereo = (self.channels == 2).cmp(&(other.channels == 2)); if cmp_stereo != Equal { @@ -726,6 +726,26 @@ impl SupportedStreamConfigRange { return cmp_f32; } + let cmp_i32 = (self.sample_format == I32).cmp(&(other.sample_format == I32)); + if cmp_i32 != Equal { + return cmp_i32; + } + + let cmp_u32 = (self.sample_format == U32).cmp(&(other.sample_format == U32)); + if cmp_u32 != Equal { + return cmp_u32; + } + + let cmp_i24 = (self.sample_format == I24).cmp(&(other.sample_format == I24)); + if cmp_i24 != Equal { + return cmp_i24; + } + + let cmp_u24 = (self.sample_format == U24).cmp(&(other.sample_format == U24)); + if cmp_u24 != Equal { + return cmp_u24; + } + let cmp_i16 = (self.sample_format == I16).cmp(&(other.sample_format == I16)); if cmp_i16 != Equal { return cmp_i16; diff --git a/src/samples_formats.rs b/src/samples_formats.rs index cab8e0e6a..6f39fb084 100644 --- a/src/samples_formats.rs +++ b/src/samples_formats.rs @@ -47,7 +47,7 @@ pub enum SampleFormat { U16, /// `U24` with a valid range of '0..16777216' with `1 << 23 == 8388608` being the origin - // U24, + U24, /// `u32` with a valid range of `u32::MIN..=u32::MAX` with `1 << 31` being the origin. U32, @@ -76,7 +76,7 @@ impl SampleFormat { SampleFormat::I16 => mem::size_of::(), SampleFormat::U16 => mem::size_of::(), SampleFormat::I24 => mem::size_of::(), - // SampleFormat::U24 => mem::size_of::(), + SampleFormat::U24 => mem::size_of::(), SampleFormat::I32 => mem::size_of::(), SampleFormat::U32 => mem::size_of::(), // SampleFormat::I48 => mem::size_of::(), @@ -100,7 +100,7 @@ impl SampleFormat { SampleFormat::I16 => i16::BITS, SampleFormat::U16 => u16::BITS, SampleFormat::I24 => 24, - // SampleFormat::U24 => 24, + SampleFormat::U24 => 24, SampleFormat::I32 => i32::BITS, SampleFormat::U32 => u32::BITS, // SampleFormat::I48 => 48, @@ -133,7 +133,7 @@ impl SampleFormat { *self, SampleFormat::U8 | SampleFormat::U16 - // | SampleFormat::U24 + | SampleFormat::U24 | SampleFormat::U32 // | SampleFormat::U48 | SampleFormat::U64 @@ -158,7 +158,7 @@ impl Display for SampleFormat { SampleFormat::I64 => "i64", SampleFormat::U8 => "u8", SampleFormat::U16 => "u16", - // SampleFormat::U24 => "u24", + SampleFormat::U24 => "u24", SampleFormat::U32 => "u32", // SampleFormat::U48 => "u48", SampleFormat::U64 => "u64", @@ -205,9 +205,9 @@ impl SizedSample for u16 { const FORMAT: SampleFormat = SampleFormat::U16; } -// impl SizedSample for U24 { -// const FORMAT: SampleFormat = SampleFormat::U24; -// } +impl SizedSample for U24 { + const FORMAT: SampleFormat = SampleFormat::U24; +} impl SizedSample for u32 { const FORMAT: SampleFormat = SampleFormat::U32;