#![allow(unsafe_code)]
use std::{ffi::c_void, ptr, time::Duration};
use kithara_bufpool::PcmBuf;
use kithara_stream::AudioCodec;
use super::{
consts::{Consts, os_status_to_string},
converter::{
ConverterInputState, converter_input_callback, gapless_info_from_prime_info,
log_gapless_prime_info, prime_info_from_converter,
},
ffi::{
AudioBuffer, AudioBufferList, AudioConverterDispose, AudioConverterFillComplexBuffer,
AudioConverterNew, AudioConverterPrimeInfo, AudioConverterRef, AudioConverterReset,
AudioConverterSetProperty, AudioStreamBasicDescription, AudioStreamPacketDescription,
UInt32,
},
};
use crate::{
codec::FrameCodec,
demuxer::TrackInfo,
error::{DecodeError, DecodeResult},
types::{DecoderTrackInfo, PcmSpec},
};
pub(crate) struct AppleCodec {
converter: AudioConverterRef,
input_state: Box<ConverterInputState>,
track_info: DecoderTrackInfo,
last_prime_info: Option<AudioConverterPrimeInfo>,
spec: PcmSpec,
gapless_enabled: bool,
prime_info_refresh_pending: bool,
frames_per_packet: u32,
input_bytes_per_packet: u32,
}
unsafe impl Send for AppleCodec {}
impl AppleCodec {
fn open(track: &TrackInfo) -> DecodeResult<Self> {
let AppleInputFormat {
asbd: input_format,
frames_per_packet,
cookie,
} = build_input_format(track)?;
let input_bytes_per_packet = input_format.mBytesPerPacket;
let output_format = build_pcm_output_format(track.sample_rate, track.channels);
let mut converter: AudioConverterRef = ptr::null_mut();
let status = unsafe { AudioConverterNew(&input_format, &output_format, &mut converter) };
if status != Consts::noErr {
return Err(DecodeError::Backend(Box::new(std::io::Error::other(
format!("AudioConverterNew failed: {}", os_status_to_string(status)),
))));
}
if let Some(cookie) = cookie.as_ref().filter(|c| !c.is_empty()) {
#[expect(
clippy::cast_possible_truncation,
reason = "magic cookie length fits in u32 for valid configs"
)]
let status = unsafe {
AudioConverterSetProperty(
converter,
Consts::kAudioConverterDecompressionMagicCookie,
cookie.len() as UInt32,
cookie.as_ptr() as *const c_void,
)
};
if status != Consts::noErr {
tracing::warn!(
status,
err = %os_status_to_string(status),
cookie_size = cookie.len(),
"AppleCodec: AudioConverterSetProperty(MagicCookie) returned non-zero (continuing)",
);
}
}
let spec = PcmSpec {
channels: track.channels,
sample_rate: track.sample_rate,
};
Ok(Self {
converter,
spec,
frames_per_packet,
input_bytes_per_packet,
input_state: Box::new(ConverterInputState::new()),
track_info: DecoderTrackInfo::default(),
last_prime_info: None,
gapless_enabled: false,
prime_info_refresh_pending: false,
})
}
pub(crate) fn open_with_config(track: &TrackInfo, gapless: bool) -> DecodeResult<Self> {
let mut codec = Self::open(track)?;
if gapless {
codec.gapless_enabled = true;
let prime_info = prime_info_from_converter(codec.converter);
let gapless = prime_info.and_then(gapless_info_from_prime_info);
log_gapless_prime_info("init", prime_info, gapless);
codec.last_prime_info = prime_info;
let resolved = track.gapless.or(gapless);
codec.track_info = DecoderTrackInfo {
gapless: resolved,
..DecoderTrackInfo::default()
};
codec.prime_info_refresh_pending = resolved.is_none();
}
Ok(codec)
}
fn refresh_gapless_after_first_chunk(&mut self) {
if !self.gapless_enabled || !self.prime_info_refresh_pending {
return;
}
self.prime_info_refresh_pending = false;
let prime_info = prime_info_from_converter(self.converter);
let gapless = prime_info.and_then(gapless_info_from_prime_info);
log_gapless_prime_info("post_first_chunk", prime_info, gapless);
if let Some(prime_info) = prime_info {
self.last_prime_info = Some(prime_info);
self.track_info.gapless = gapless;
}
}
#[must_use]
pub(crate) fn supports(codec: AudioCodec) -> bool {
matches!(
codec,
AudioCodec::AacLc
| AudioCodec::AacHe
| AudioCodec::AacHeV2
| AudioCodec::Flac
| AudioCodec::Pcm
| AudioCodec::Mp3
| AudioCodec::Alac
)
}
}
impl Drop for AppleCodec {
fn drop(&mut self) {
if !self.converter.is_null() {
let _ = unsafe { AudioConverterDispose(self.converter) };
}
}
}
impl FrameCodec for AppleCodec {
fn decode_frame(
&mut self,
frame_data: &[u8],
_pts: Duration,
packet_desc: &[u8],
out: &mut PcmBuf,
) -> DecodeResult<u32> {
if frame_data.is_empty() {
out.clear();
return Ok(0);
}
let desc = if packet_desc.len() == size_of::<AudioStreamPacketDescription>() {
let mut d = AudioStreamPacketDescription::default();
unsafe {
ptr::copy_nonoverlapping(
packet_desc.as_ptr(),
&mut d as *mut _ as *mut u8,
size_of::<AudioStreamPacketDescription>(),
);
}
d
} else {
AudioStreamPacketDescription {
mStartOffset: 0,
mVariableFramesInPacket: 0,
#[expect(
clippy::cast_possible_truncation,
reason = "frame size fits in u32 for any realistic codec packet"
)]
mDataByteSize: frame_data.len() as UInt32,
}
};
self.input_state.set(frame_data, desc);
let channels = self.spec.channels as usize;
#[expect(
clippy::cast_possible_truncation,
reason = "frame_data length divided by mBytesPerPacket is bounded by packet count"
)]
let target_frames = if self.input_bytes_per_packet > 0 {
(frame_data.len() / self.input_bytes_per_packet as usize) as u32
} else {
self.frames_per_packet.max(Consts::AAC_FRAMES_PER_PACKET)
};
if target_frames == 0 {
out.clear();
return Ok(0);
}
let needed_samples = target_frames as usize * channels;
out.ensure_len(needed_samples)
.map_err(|e| DecodeError::Backend(Box::new(e)))?;
let mut output_packets = target_frames;
#[expect(
clippy::cast_possible_truncation,
reason = "PCM buffer byte size fits in u32 for one packet"
)]
let mut buffer_list = AudioBufferList {
mNumberBuffers: 1,
mBuffers: [AudioBuffer {
mNumberChannels: u32::from(self.spec.channels),
mDataByteSize: (out.len() * Consts::BYTES_PER_F32_SAMPLE as usize) as u32,
mData: out.as_mut_ptr() as *mut c_void,
}],
};
let input_ptr = self.input_state.as_mut() as *mut ConverterInputState as *mut c_void;
let status = unsafe {
AudioConverterFillComplexBuffer(
self.converter,
converter_input_callback,
input_ptr,
&mut output_packets,
&mut buffer_list,
ptr::null_mut(),
)
};
if status != Consts::noErr
&& status != Consts::kAudioConverterErr_NoDataNow
&& output_packets == 0
{
return Err(DecodeError::Backend(Box::new(std::io::Error::other(
format!(
"AudioConverterFillComplexBuffer failed: {}",
os_status_to_string(status)
),
))));
}
let frames = output_packets;
let samples_len = frames as usize * channels;
out.truncate(samples_len);
self.refresh_gapless_after_first_chunk();
Ok(frames)
}
fn flush(&mut self) -> DecodeResult<()> {
let status = unsafe { AudioConverterReset(self.converter) };
if status != Consts::noErr {
return Err(DecodeError::Backend(Box::new(std::io::Error::other(
format!(
"AudioConverterReset failed: {}",
os_status_to_string(status)
),
))));
}
self.input_state.clear();
Ok(())
}
fn spec(&self) -> PcmSpec {
self.spec
}
fn track_info(&self) -> DecoderTrackInfo {
self.track_info.clone()
}
}
struct AppleInputFormat {
asbd: AudioStreamBasicDescription,
cookie: Option<Vec<u8>>,
frames_per_packet: u32,
}
fn build_input_format(track: &TrackInfo) -> DecodeResult<AppleInputFormat> {
match track.codec {
AudioCodec::AacLc | AudioCodec::AacHe | AudioCodec::AacHeV2 => {
let format_id = match track.codec {
AudioCodec::AacHe => Consts::kAudioFormatMPEG4AAC_HE,
AudioCodec::AacHeV2 => Consts::kAudioFormatMPEG4AAC_HE_V2,
_ => Consts::kAudioFormatMPEG4AAC,
};
let asbd = AudioStreamBasicDescription {
mSampleRate: f64::from(track.sample_rate),
mFormatID: format_id,
mFramesPerPacket: Consts::AAC_FRAMES_PER_PACKET,
mChannelsPerFrame: u32::from(track.channels),
..Default::default()
};
let cookie = (!track.extra_data.is_empty()).then(|| track.extra_data.clone());
Ok(AppleInputFormat {
asbd,
cookie,
frames_per_packet: Consts::AAC_FRAMES_PER_PACKET,
})
}
AudioCodec::Flac => {
if track.extra_data.len() < Consts::FLAC_STREAMINFO_LEN {
return Err(DecodeError::InvalidData(format!(
"flac: STREAMINFO too short ({} bytes, need {})",
track.extra_data.len(),
Consts::FLAC_STREAMINFO_LEN
)));
}
let streaminfo = &track.extra_data[..Consts::FLAC_STREAMINFO_LEN];
let max_block = u32::from(u16::from_be_bytes([streaminfo[2], streaminfo[3]]))
.max(Consts::AAC_FRAMES_PER_PACKET);
let mut cookie =
Vec::with_capacity(Consts::FLAC_COOKIE_PREFIX_LEN + Consts::FLAC_STREAMINFO_LEN);
cookie.extend_from_slice(b"fLaC");
cookie.push(0x80);
cookie.push(0x00);
cookie.push(0x00);
#[expect(
clippy::cast_possible_truncation,
reason = "FLAC_STREAMINFO_LEN is 34, fits in u8"
)]
{
cookie.push(Consts::FLAC_STREAMINFO_LEN as u8);
}
cookie.extend_from_slice(streaminfo);
let asbd = AudioStreamBasicDescription {
mSampleRate: f64::from(track.sample_rate),
mFormatID: Consts::kAudioFormatFLAC,
mFramesPerPacket: max_block,
mChannelsPerFrame: u32::from(track.channels),
..Default::default()
};
Ok(AppleInputFormat {
asbd,
frames_per_packet: max_block,
cookie: Some(cookie),
})
}
AudioCodec::Pcm => {
let asbd = parse_pcm_extra_data(&track.extra_data)?;
Ok(AppleInputFormat {
asbd,
cookie: None,
frames_per_packet: 1,
})
}
AudioCodec::Mp3 => {
let asbd = AudioStreamBasicDescription {
mSampleRate: f64::from(track.sample_rate),
mFormatID: Consts::kAudioFormatMPEGLayer3,
mFramesPerPacket: 0,
mChannelsPerFrame: u32::from(track.channels),
..Default::default()
};
Ok(AppleInputFormat {
asbd,
cookie: None,
frames_per_packet: 1152,
})
}
AudioCodec::Alac => {
if track.extra_data.is_empty() {
return Err(DecodeError::InvalidData(
"alac: missing magic cookie (kAudioFilePropertyMagicCookieData)".to_string(),
));
}
let asbd = AudioStreamBasicDescription {
mSampleRate: f64::from(track.sample_rate),
mFormatID: Consts::kAudioFormatAppleLossless,
mFramesPerPacket: 0,
mChannelsPerFrame: u32::from(track.channels),
..Default::default()
};
Ok(AppleInputFormat {
asbd,
cookie: Some(track.extra_data.clone()),
frames_per_packet: 4096,
})
}
other => Err(DecodeError::UnsupportedCodec(other)),
}
}
fn parse_pcm_extra_data(extra: &[u8]) -> DecodeResult<AudioStreamBasicDescription> {
if extra.len() < size_of::<AudioStreamBasicDescription>() {
return Err(DecodeError::InvalidData(format!(
"pcm: extra_data too short ({} bytes, need {})",
extra.len(),
size_of::<AudioStreamBasicDescription>()
)));
}
let mut asbd = AudioStreamBasicDescription::default();
unsafe {
ptr::copy_nonoverlapping(
extra.as_ptr(),
&mut asbd as *mut _ as *mut u8,
size_of::<AudioStreamBasicDescription>(),
);
}
Ok(asbd)
}
fn build_pcm_output_format(sample_rate: u32, channels: u16) -> AudioStreamBasicDescription {
AudioStreamBasicDescription {
mSampleRate: f64::from(sample_rate),
mFormatID: Consts::kAudioFormatLinearPCM,
mFormatFlags: Consts::kAudioFormatFlagsNativeFloatPacked,
mBytesPerPacket: Consts::BYTES_PER_F32_SAMPLE * u32::from(channels),
mFramesPerPacket: 1,
mBytesPerFrame: Consts::BYTES_PER_F32_SAMPLE * u32::from(channels),
mChannelsPerFrame: u32::from(channels),
mBitsPerChannel: Consts::BITS_PER_F32_SAMPLE,
..Default::default()
}
}