Skip to content

Commit d534c3c

Browse files
committed
feat(core): 添加 API 用于跳过重采样器直接获取原始 PCM 数据
1 parent 708b62f commit d534c3c

3 files changed

Lines changed: 188 additions & 2 deletions

File tree

crates/ffmpeg_audio/src/core/frame.rs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,27 @@ use std::{
55
};
66

77
use crate::{
8+
AudioError,
9+
AudioSample,
10+
Result,
811
TimeBase,
912
sys,
1013
};
1114

15+
/// A safe enum representing the memory layout of underlying FFmpeg PCM data.
16+
#[derive(Debug, Clone)]
17+
pub enum RawAudioData<'a, T> {
18+
/// Packed (Interleaved) layout.
19+
/// All channel data is interleaved in a single contiguous memory block, e.g., `[L, R, L, R, L,
20+
/// R]`.
21+
Packed(&'a [T]),
22+
23+
/// Planar layout.
24+
/// Data for each channel is stored in independent, contiguous memory blocks, e.g., `[L, L, L]`
25+
/// and `[R, R, R]`. The length of the outer Vec represents the number of channels.
26+
Planar(Vec<&'a [T]>),
27+
}
28+
1229
/// A safe, zero-copy wrapper around FFmpeg's raw `AVFrame`.
1330
///
1431
/// This wrapper is useful for 1-to-N zero-copy dispatching to multiple downstream
@@ -21,7 +38,7 @@ pub struct AudioFrame<'a> {
2138
_marker: PhantomData<&'a mut ()>,
2239
}
2340

24-
impl AudioFrame<'_> {
41+
impl<'a> AudioFrame<'a> {
2542
/// Creates a new `AudioFrame` wrapper.
2643
///
2744
/// # Safety
@@ -136,6 +153,57 @@ impl AudioFrame<'_> {
136153
duration
137154
})
138155
}
156+
157+
/// Zero-copy extraction of raw PCM audio data directly from the underlying FFmpeg AVFrame.
158+
///
159+
/// # Returns
160+
/// * `Ok(RawAudioData)` - The corresponding memory slice enum.
161+
/// * `Err(AudioError::FormatMismatch)` - The requested `T` does not match the type actually
162+
/// output by the underlying decoder.
163+
pub fn raw_data<T: AudioSample>(&self) -> Result<RawAudioData<'a, T>> {
164+
let fmt = self.sample_fmt();
165+
let is_packed = fmt == T::PACKED_FORMAT;
166+
let is_planar = fmt == T::PLANAR_FORMAT;
167+
168+
if !is_packed && !is_planar {
169+
return Err(AudioError::FormatMismatch);
170+
}
171+
172+
let logical_samples = self.samples();
173+
let channels = unsafe { (*self.ptr.as_ptr()).ch_layout.nb_channels } as usize;
174+
let offset = self.offset();
175+
let extended_data = unsafe { (*self.ptr.as_ptr()).extended_data };
176+
177+
if logical_samples == 0 || extended_data.is_null() {
178+
return if is_packed {
179+
Ok(RawAudioData::Packed(&[]))
180+
} else {
181+
Ok(RawAudioData::Planar(vec![&[]; channels]))
182+
};
183+
}
184+
185+
if is_packed {
186+
unsafe {
187+
let base_ptr = (*extended_data).cast::<T>();
188+
let adjusted_ptr = base_ptr.add(offset * channels);
189+
let slice = std::slice::from_raw_parts(adjusted_ptr, logical_samples * channels);
190+
191+
Ok(RawAudioData::Packed(slice))
192+
}
193+
} else {
194+
let mut planes = Vec::with_capacity(channels);
195+
unsafe {
196+
for ch in 0..channels {
197+
let base_ptr = (*extended_data.add(ch)).cast::<T>();
198+
let adjusted_ptr = base_ptr.add(offset);
199+
let slice = std::slice::from_raw_parts(adjusted_ptr, logical_samples);
200+
201+
planes.push(slice);
202+
}
203+
}
204+
Ok(RawAudioData::Planar(planes))
205+
}
206+
}
139207
}
140208

141209
#[cfg(test)]

crates/ffmpeg_audio/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ pub mod resample;
88
pub use core::http::HttpAudioSource;
99
pub use core::{
1010
format::AudioSample,
11-
frame::AudioFrame,
11+
frame::{
12+
AudioFrame,
13+
RawAudioData,
14+
},
1215
info::SourceAudioInfo,
1316
time::TimeBase,
1417
};

crates/ffmpeg_audio/tests/integration_test.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::{
55

66
use ffmpeg_audio::{
77
AudioReader,
8+
RawAudioData,
89
ResampleOptions,
910
SeekMode,
1011
};
@@ -419,6 +420,79 @@ fn test_planar_format_mismatch_rejection_planar_to_packed() {
419420
);
420421
}
421422

423+
#[test]
424+
fn test_raw_data_extraction_packed_and_type_safety() {
425+
let wav_data = generate_sine_wav(0.1);
426+
let mut reader = AudioReader::new(Cursor::new(wav_data)).unwrap();
427+
428+
let channels = reader.source_info().channels as usize;
429+
430+
let frame = reader.receive_frame().unwrap().expect("应能读取到 WAV 帧");
431+
432+
let err_result = frame.raw_data::<f32>();
433+
assert!(
434+
matches!(err_result, Err(ffmpeg_audio::AudioError::FormatMismatch)),
435+
"期望类型不匹配错误,但得到了: {err_result:?}"
436+
);
437+
438+
let raw = frame.raw_data::<i16>().expect("提取 i16 原始数据失败");
439+
440+
match raw {
441+
RawAudioData::Packed(data) => {
442+
let expected_len = frame.samples() * channels;
443+
444+
assert_eq!(
445+
data.len(),
446+
expected_len,
447+
"Packed 切片的元素总数 ({}) 应等于逻辑样本数 ({}) 乘以声道数 ({})",
448+
data.len(),
449+
frame.samples(),
450+
channels
451+
);
452+
453+
let has_signal = data.iter().any(|&s| s != 0);
454+
assert!(has_signal, "捞出的原始 PCM 不应全为静音");
455+
}
456+
RawAudioData::Planar(_) => panic!("WAV 应该被解码为 Packed 布局,却得到了 Planar"),
457+
}
458+
}
459+
460+
#[test]
461+
fn test_raw_data_signal_integrity_across_frames() {
462+
let sample_rate = 44100;
463+
let freq: f32 = 440.0;
464+
let duration = 0.1;
465+
466+
let wav_data = generate_sine_wav(duration);
467+
let mut reader = AudioReader::new(Cursor::new(wav_data)).unwrap();
468+
469+
let mut global_sample_index = 0;
470+
471+
while let Some(frame) = reader.receive_frame().unwrap() {
472+
let raw = frame.raw_data::<i16>().expect("提取 i16 失败");
473+
474+
if let RawAudioData::Packed(data) = raw {
475+
for &actual_sample in data {
476+
let t = global_sample_index as f32 / sample_rate as f32;
477+
let expected_sample =
478+
(f32::sin(2.0 * std::f32::consts::PI * freq * t) * 16000.0) as i16;
479+
480+
let diff = (i32::from(actual_sample) - i32::from(expected_sample)).abs();
481+
assert!(
482+
diff <= 1,
483+
"信号失真!在全局样本索引 {global_sample_index} 期望 {expected_sample}, 实际得到 {actual_sample}"
484+
);
485+
486+
global_sample_index += 1;
487+
}
488+
} else {
489+
panic!("WAV 应为 Packed 格式");
490+
}
491+
}
492+
493+
assert_eq!(global_sample_index, 4410, "解码出的样本总数不对");
494+
}
495+
422496
#[cfg(not(target_arch = "wasm32"))]
423497
mod file_tests {
424498
use std::fs::File;
@@ -495,4 +569,45 @@ mod file_tests {
495569
"Should have successfully decoded mutated stream"
496570
);
497571
}
572+
573+
#[test]
574+
fn test_raw_data_extraction_planar_with_seek_offset() {
575+
let file = File::open(AAC_SEEK_PATH).expect("Failed to open AAC test asset");
576+
let mut reader = AudioReader::new(file).unwrap();
577+
578+
let expected_channels = reader.source_info().channels as usize;
579+
580+
let target = Duration::from_millis(501);
581+
reader.seek(target, SeekMode::Accurate).unwrap();
582+
583+
let frame = reader.receive_frame().unwrap().expect("Seek 后读取帧失败");
584+
585+
assert!(frame.samples() > 0, "修剪后应仍有剩余数据");
586+
587+
let raw = frame.raw_data::<f32>().expect("提取 f32 原始数据失败");
588+
589+
match raw {
590+
RawAudioData::Planar(planes) => {
591+
assert_eq!(
592+
planes.len(),
593+
expected_channels,
594+
"返回的平面切片数量 ({}) 应等于文件的物理声道数 ({})",
595+
planes.len(),
596+
expected_channels
597+
);
598+
599+
for (ch_idx, plane) in planes.iter().enumerate() {
600+
assert_eq!(
601+
plane.len(),
602+
frame.samples(),
603+
"声道 {ch_idx} 的切片长度未与修剪后的样本数对齐"
604+
);
605+
606+
let has_signal = plane.iter().any(|&s| s.abs() > 0.0);
607+
assert!(has_signal, "提取出来的声道 {ch_idx} 数据异常");
608+
}
609+
}
610+
RawAudioData::Packed(_) => panic!("AAC 应该被解码为 Planar 布局,却得到了 Packed"),
611+
}
612+
}
498613
}

0 commit comments

Comments
 (0)