#![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, AudioFormatGetProperty, AudioFormatGetPropertyInfo,
AudioFormatInfo, AudioFormatListItem, AudioStreamBasicDescription,
AudioStreamPacketDescription, UInt32,
},
};
use crate::{
codec::{CodecPriming, 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_msg(format!(
"AudioConverterNew failed: {}",
os_status_to_string(status)
)));
}
if let Some(cookie) = cookie.as_ref().filter(|c| !c.is_empty()) {
let cookie_size = UInt32::try_from(cookie.len())?;
let status = unsafe {
AudioConverterSetProperty(
converter,
Consts::kAudioConverterDecompressionMagicCookie,
cookie_size,
cookie.as_ptr().cast::<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::default()),
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(),
ptr::from_mut(&mut d).cast::<u8>(),
size_of::<AudioStreamPacketDescription>(),
);
}
d
} else {
let frame_bytes = UInt32::try_from(frame_data.len())?;
AudioStreamPacketDescription {
mStartOffset: 0,
mVariableFramesInPacket: 0,
mDataByteSize: frame_bytes,
}
};
self.input_state.set(frame_data, desc);
let channels = usize::from(self.spec.channels);
let target_frames: u32 = if self.input_bytes_per_packet > 0 {
let packets = frame_data.len() / usize::try_from(self.input_bytes_per_packet)?;
u32::try_from(packets)?
} else {
self.frames_per_packet.max(Consts::AAC_FRAMES_PER_PACKET)
};
if target_frames == 0 {
out.clear();
return Ok(0);
}
let needed_samples = usize::try_from(target_frames)? * channels;
out.ensure_len(needed_samples)?;
let mut output_packets = target_frames;
let buffer_bytes =
u32::try_from(out.len() * usize::try_from(Consts::BYTES_PER_F32_SAMPLE).unwrap_or(0))?;
let mut buffer_list = AudioBufferList {
mNumberBuffers: 1,
mBuffers: [AudioBuffer {
mNumberChannels: u32::from(self.spec.channels),
mDataByteSize: buffer_bytes,
mData: out.as_mut_ptr().cast::<c_void>(),
}],
};
let input_ptr =
ptr::from_mut::<ConverterInputState>(self.input_state.as_mut()).cast::<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_msg(format!(
"AudioConverterFillComplexBuffer failed: {}",
os_status_to_string(status)
)));
}
let frames = output_packets;
let samples_len = usize::try_from(frames)? * channels;
out.truncate(samples_len);
self.refresh_gapless_after_first_chunk();
Ok(frames)
}
fn decoder_algo_delay(&self, codec: AudioCodec) -> u64 {
apple_decoder_algo_delay(codec)
}
fn flush(&mut self) -> DecodeResult<()> {
let status = unsafe { AudioConverterReset(self.converter) };
if status != Consts::noErr {
return Err(DecodeError::backend_msg(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()
}
fn priming(&self, codec: AudioCodec) -> CodecPriming {
apple_codec_priming(codec)
}
}
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 => {
build_aac_input_format(track)
}
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);
cookie.push(Consts::FLAC_STREAMINFO_LEN_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(),
ptr::from_mut(&mut asbd).cast::<u8>(),
size_of::<AudioStreamBasicDescription>(),
);
}
Ok(asbd)
}
fn build_aac_input_format(track: &TrackInfo) -> DecodeResult<AppleInputFormat> {
if track.extra_data.is_empty() {
let asbd = AudioStreamBasicDescription {
mSampleRate: f64::from(track.sample_rate),
mFormatID: Consts::kAudioFormatMPEG4AAC,
mFramesPerPacket: Consts::AAC_FRAMES_PER_PACKET,
mChannelsPerFrame: u32::from(track.channels),
..Default::default()
};
return Ok(AppleInputFormat {
asbd,
cookie: None,
frames_per_packet: Consts::AAC_FRAMES_PER_PACKET,
});
}
let esds = if track.extra_data.first() == Some(&0x03) {
track.extra_data.clone()
} else {
esds_wrap_asc(&track.extra_data)?
};
let asbd = derive_aac_asbd_from_esds(&esds, track)?;
let frames_per_packet = if asbd.mFramesPerPacket > 0 {
asbd.mFramesPerPacket
} else {
Consts::AAC_FRAMES_PER_PACKET
};
Ok(AppleInputFormat {
asbd,
frames_per_packet,
cookie: Some(esds),
})
}
fn derive_aac_asbd_from_esds(
esds: &[u8],
track: &TrackInfo,
) -> DecodeResult<AudioStreamBasicDescription> {
let cookie_size = UInt32::try_from(esds.len())?;
let specifier_size = UInt32::try_from(size_of::<AudioFormatInfo>())?;
let format_info = AudioFormatInfo {
mASBD: AudioStreamBasicDescription {
mFormatID: Consts::kAudioFormatMPEG4AAC,
..Default::default()
},
mMagicCookie: esds.as_ptr().cast::<c_void>(),
mMagicCookieSize: cookie_size,
};
let mut list_bytes: UInt32 = 0;
let status = unsafe {
AudioFormatGetPropertyInfo(
Consts::kAudioFormatProperty_FormatList,
specifier_size,
ptr::from_ref(&format_info).cast::<c_void>(),
&mut list_bytes,
)
};
if status != Consts::noErr || list_bytes == 0 {
return Err(DecodeError::backend_msg(format!(
"AudioFormatGetPropertyInfo(FormatList) failed: {} (size={}, esds_len={})",
os_status_to_string(status),
list_bytes,
esds.len()
)));
}
let item_size = size_of::<AudioFormatListItem>();
let item_count = usize::try_from(list_bytes)? / item_size;
if item_count == 0 {
return Err(DecodeError::backend_msg(format!(
"FormatList returned {list_bytes} bytes (< 1 item)"
)));
}
let mut items: Vec<AudioFormatListItem> = vec![AudioFormatListItem::default(); item_count];
let mut io_size = list_bytes;
let status = unsafe {
AudioFormatGetProperty(
Consts::kAudioFormatProperty_FormatList,
specifier_size,
ptr::from_ref(&format_info).cast::<c_void>(),
&mut io_size,
items.as_mut_ptr().cast::<c_void>(),
)
};
if status != Consts::noErr {
return Err(DecodeError::backend_msg(format!(
"AudioFormatGetProperty(FormatList) failed: {}",
os_status_to_string(status)
)));
}
let returned = usize::try_from(io_size)? / item_size;
let chosen = items
.first()
.copied()
.ok_or_else(|| DecodeError::backend_msg("FormatList returned zero items"))?;
tracing::debug!(
format_id = format!("{:#010x}", chosen.mASBD.mFormatID),
sample_rate = chosen.mASBD.mSampleRate,
channels = chosen.mASBD.mChannelsPerFrame,
frames_per_packet = chosen.mASBD.mFramesPerPacket,
channel_layout = format!("{:#010x}", chosen.mChannelLayoutTag),
item_count = returned,
esds_len = esds.len(),
track_codec = ?track.codec,
track_sample_rate = track.sample_rate,
track_channels = track.channels,
"AppleCodec: AAC ASBD derived from FormatList"
);
Ok(chosen.mASBD)
}
fn esds_wrap_asc(asc: &[u8]) -> DecodeResult<Vec<u8>> {
let too_long = |scope: &str, n: usize| {
DecodeError::InvalidData(format!(
"aac: {scope} too long for short-form ESDS size field ({n} > 127)"
))
};
let dsi_body: u8 = asc
.len()
.try_into()
.map_err(|_| too_long("ASC", asc.len()))?;
let dcd_body_len = 1 + 1 + 3 + 4 + 4 + 2 + asc.len();
let dcd_body: u8 = dcd_body_len
.try_into()
.map_err(|_| too_long("DecoderConfigDescriptor", dcd_body_len))?;
let esd_body_len = 2 + 1 + 2 + dcd_body_len + 3;
let esd_body: u8 = esd_body_len
.try_into()
.map_err(|_| too_long("ES_Descriptor", esd_body_len))?;
let header: [u8; 22] = [
0x03, esd_body, 0x00, 0x00, 0x00, 0x04, dcd_body, 0x40, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, dsi_body, ];
let trailer: [u8; 3] = [0x06, 0x01, 0x02];
Ok(header
.into_iter()
.chain(asc.iter().copied())
.chain(trailer)
.collect())
}
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()
}
}
#[must_use]
pub(crate) fn apple_codec_priming(codec: AudioCodec) -> CodecPriming {
match codec {
AudioCodec::AacHeV2 => CodecPriming {
frames: 4096,
packets: 3,
byte_margin: 32768,
},
AudioCodec::AacHe => CodecPriming {
frames: 2048,
packets: 2,
byte_margin: 16384,
},
AudioCodec::AacLc => CodecPriming {
frames: 1024,
packets: 2,
byte_margin: 8192,
},
AudioCodec::Mp3 => CodecPriming {
frames: 1152,
packets: 1,
byte_margin: 4608,
},
_ => CodecPriming::default(),
}
}
#[must_use]
pub(crate) fn apple_decoder_algo_delay(codec: AudioCodec) -> u64 {
match codec {
AudioCodec::Mp3 => 529,
_ => 0,
}
}
#[cfg(test)]
mod algo_delay_tests {
use kithara_stream::AudioCodec;
use kithara_test_utils::kithara;
use super::apple_decoder_algo_delay;
#[kithara::test]
fn apple_decoder_algo_delay_mp3_is_529() {
assert_eq!(apple_decoder_algo_delay(AudioCodec::Mp3), 529);
}
#[kithara::test]
fn apple_decoder_algo_delay_non_mp3_codecs_zero() {
assert_eq!(apple_decoder_algo_delay(AudioCodec::AacLc), 0);
assert_eq!(apple_decoder_algo_delay(AudioCodec::AacHe), 0);
assert_eq!(apple_decoder_algo_delay(AudioCodec::AacHeV2), 0);
assert_eq!(apple_decoder_algo_delay(AudioCodec::Flac), 0);
assert_eq!(apple_decoder_algo_delay(AudioCodec::Opus), 0);
}
}
#[cfg(test)]
mod priming_table_tests {
use kithara_stream::AudioCodec;
use kithara_test_utils::kithara;
use super::apple_codec_priming;
use crate::codec::CodecPriming;
#[kithara::test]
fn apple_priming_aac_he_v2() {
assert_eq!(
apple_codec_priming(AudioCodec::AacHeV2),
CodecPriming {
frames: 4096,
packets: 3,
byte_margin: 32768
}
);
}
#[kithara::test]
fn apple_priming_aac_he() {
assert_eq!(
apple_codec_priming(AudioCodec::AacHe),
CodecPriming {
frames: 2048,
packets: 2,
byte_margin: 16384
}
);
}
#[kithara::test]
fn apple_priming_aac_lc() {
assert_eq!(
apple_codec_priming(AudioCodec::AacLc),
CodecPriming {
frames: 1024,
packets: 2,
byte_margin: 8192
}
);
}
#[kithara::test]
fn apple_priming_mp3() {
assert_eq!(
apple_codec_priming(AudioCodec::Mp3),
CodecPriming {
frames: 1152,
packets: 1,
byte_margin: 4608
}
);
}
#[kithara::test]
fn apple_priming_flac_is_default() {
assert_eq!(
apple_codec_priming(AudioCodec::Flac),
CodecPriming::default()
);
}
}