refactor: drop internal bridge/ffi layers; exporter family lands
Single-lib cleanup: the per-crate src/bridge/ and src/ffi.rs layers are gone (oakundo/oakcommon/oaknode/oaktimeline/oakcodec/oakaudio/ oakrender/oaktask/oakplugin/oakstorage); cross-crate calls are plain Rust, CHandle marshalling shrinks to the oakengine boundary, and tests call the Rust APIs directly (pure C-ABI wrapper tests removed where the domain layer already covers the behavior). exporter.h family implemented: oakengine_export_render (CLI contract), oakengine_export_render_with_params (was a stub), last_error and progress callback; synchronous path reuses task_create_export + start_sync. Fixes on the way: oaktask video ticket self-deadlock, audio params dropped on the export path, codec encoder AAC slicing and H.264 time base. Real-mp4 tests cover both entry points, progress and the illegal-argument matrix. Also: oakstorage session maps null project handles to None (version- info path), configstore test double literal 3.14 -> 3.15 (clippy PI lint), oakaudio output callback scratch buffer + env-aware P1 test, cli media round-trip test uses a generated 16-frame clip (no more minute-long debug runs).
This commit is contained in:
@@ -14,20 +14,15 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! AudioProcessor contract tests (processor.rs), through the C ABI. The
|
||||
//! conversion runs a real FFmpeg filter graph (aresample/aformat/atempo),
|
||||
//! so resampling and time-stretch have filter latency: the exact frame
|
||||
//! counts are drained after `flush`, while identity conversion is an
|
||||
//! immediate passthrough.
|
||||
//! AudioProcessor contract tests (processor.rs), calling the public Rust
|
||||
//! API. The conversion runs a real FFmpeg filter graph
|
||||
//! (aresample/aformat/atempo), so resampling and time-stretch have filter
|
||||
//! latency: the exact frame counts are drained after `flush`, while
|
||||
//! identity conversion is an immediate passthrough.
|
||||
|
||||
mod common;
|
||||
|
||||
use oakaudio::error::{OAKAUDIO_E_INVALID, OAKAUDIO_E_STATE, OAKAUDIO_OK};
|
||||
use oakaudio::ffi::processor::{
|
||||
oakaudio_processor_close, oakaudio_processor_convert, oakaudio_processor_flush,
|
||||
oakaudio_processor_free, oakaudio_processor_init, oakaudio_processor_is_open,
|
||||
oakaudio_processor_open,
|
||||
};
|
||||
use oakaudio::error::{Error, OAKAUDIO_E_INVALID, OAKAUDIO_E_STATE, OAKAUDIO_OK};
|
||||
use oakaudio::params::{AudioParams, SampleFormat};
|
||||
use oakaudio::processor::Processor;
|
||||
|
||||
/// Stereo f32_p planes of `frames` ramp samples.
|
||||
fn ramp_planes(frames: usize) -> Vec<Vec<f32>> {
|
||||
@@ -37,37 +32,61 @@ fn ramp_planes(frames: usize) -> Vec<Vec<f32>> {
|
||||
]
|
||||
}
|
||||
|
||||
fn open_identity(h: oakaudio::handle::CHandle) -> i32 {
|
||||
unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 4, 1.0) }
|
||||
/// Stereo planar-f32 params at `rate`.
|
||||
fn params(rate: i32) -> AudioParams {
|
||||
AudioParams {
|
||||
sample_rate: rate,
|
||||
channel_layout: 0x3,
|
||||
format: SampleFormat::F32Planar,
|
||||
}
|
||||
}
|
||||
|
||||
/// init yields a valid handle; is_open is false before open and true after;
|
||||
/// close returns it to closed without error.
|
||||
fn open_identity(p: &Processor) -> Result<(), Box<dyn std::error::Error>> {
|
||||
p.open(params(48000), params(48000), 1.0)
|
||||
}
|
||||
|
||||
/// Pointer arrays for the convert call.
|
||||
fn plane_ptrs(planes: &[Vec<f32>]) -> Vec<*const f32> {
|
||||
planes.iter().map(|p| p.as_ptr()).collect()
|
||||
}
|
||||
|
||||
fn plane_mut_ptrs(planes: &mut [Vec<f32>]) -> Vec<*mut f32> {
|
||||
planes.iter_mut().map(|p| p.as_mut_ptr()).collect()
|
||||
}
|
||||
|
||||
/// The crate error code of a failed call (the API surfaces
|
||||
/// `Box<dyn std::error::Error>`).
|
||||
fn code(err: Box<dyn std::error::Error>) -> i32 {
|
||||
err.downcast_ref::<oakaudio::error::Error>()
|
||||
.map(|e| e.code())
|
||||
.unwrap_or(-1)
|
||||
}
|
||||
|
||||
/// init yields a closed processor; is_open is false before open and true
|
||||
/// after; close returns it to closed without error.
|
||||
#[test]
|
||||
fn processor_open_isopen_close() {
|
||||
let mut h = unsafe { oakaudio_processor_init() };
|
||||
assert!(!h.ctx.is_null());
|
||||
assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0);
|
||||
assert_eq!(open_identity(h), OAKAUDIO_OK);
|
||||
assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 1);
|
||||
assert_eq!(unsafe { oakaudio_processor_close(h) }, OAKAUDIO_OK);
|
||||
assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0);
|
||||
unsafe { oakaudio_processor_free(&mut h) };
|
||||
let p = Processor::init();
|
||||
assert_eq!(p.is_open().unwrap(), false);
|
||||
open_identity(&p).unwrap();
|
||||
assert_eq!(p.is_open().unwrap(), true);
|
||||
p.close().unwrap();
|
||||
assert_eq!(p.is_open().unwrap(), false);
|
||||
}
|
||||
|
||||
/// open with matching in/out rate and format is an identity passthrough:
|
||||
/// convert returns the same frame count and samples within 1e-6.
|
||||
#[test]
|
||||
fn identity_convert_passthrough() {
|
||||
let mut h = unsafe { oakaudio_processor_init() };
|
||||
assert_eq!(open_identity(h), OAKAUDIO_OK);
|
||||
let p = Processor::init();
|
||||
open_identity(&p).unwrap();
|
||||
|
||||
let planes = ramp_planes(32);
|
||||
let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
|
||||
let in_ptrs = plane_ptrs(&planes);
|
||||
let mut out = vec![vec![0f32; 32]; 2];
|
||||
let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect();
|
||||
let mut out_ptrs = plane_mut_ptrs(&mut out);
|
||||
|
||||
let n = unsafe { oakaudio_processor_convert(h, in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 32) };
|
||||
let n = p.convert(in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 32).unwrap();
|
||||
assert_eq!(n, 32);
|
||||
for ch in 0..2 {
|
||||
for i in 0..32 {
|
||||
@@ -79,22 +98,21 @@ fn identity_convert_passthrough() {
|
||||
);
|
||||
}
|
||||
}
|
||||
unsafe { oakaudio_processor_free(&mut h) };
|
||||
}
|
||||
|
||||
/// convert with an output capacity smaller than the produced frames returns
|
||||
/// the produced count clamped to capacity and fills up to capacity.
|
||||
#[test]
|
||||
fn convert_capacity_truncation() {
|
||||
let mut h = unsafe { oakaudio_processor_init() };
|
||||
assert_eq!(open_identity(h), OAKAUDIO_OK);
|
||||
let p = Processor::init();
|
||||
open_identity(&p).unwrap();
|
||||
|
||||
let planes = ramp_planes(32);
|
||||
let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
|
||||
let in_ptrs = plane_ptrs(&planes);
|
||||
let mut out = vec![vec![9.9f32; 10]; 2];
|
||||
let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect();
|
||||
let mut out_ptrs = plane_mut_ptrs(&mut out);
|
||||
|
||||
let n = unsafe { oakaudio_processor_convert(h, in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 10) };
|
||||
let n = p.convert(in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 10).unwrap();
|
||||
assert_eq!(n, 10);
|
||||
for ch in 0..2 {
|
||||
for i in 0..10 {
|
||||
@@ -104,56 +122,40 @@ fn convert_capacity_truncation() {
|
||||
|
||||
// The graph has already drained; nothing further to pull.
|
||||
let mut out2 = vec![vec![0f32; 32]; 2];
|
||||
let mut out2_ptrs: Vec<*mut f32> = out2.iter_mut().map(|p| p.as_mut_ptr()).collect();
|
||||
let n = unsafe { oakaudio_processor_convert(h, in_ptrs.as_ptr(), 0, out2_ptrs.as_ptr(), 32) };
|
||||
let mut out2_ptrs = plane_mut_ptrs(&mut out2);
|
||||
let n = p.convert(in_ptrs.as_ptr(), 0, out2_ptrs.as_ptr(), 32).unwrap();
|
||||
assert_eq!(n, 0);
|
||||
|
||||
unsafe { oakaudio_processor_free(&mut h) };
|
||||
}
|
||||
|
||||
/// open with a zero/negative rate or a wrong output format returns
|
||||
/// OAKAUDIO_E_INVALID and leaves the processor closed; an empty handle is
|
||||
/// OAKAUDIO_E_INVALID everywhere.
|
||||
/// open with a zero/negative rate or a wrong output format is rejected and
|
||||
/// leaves the processor closed; convert before open is a state error.
|
||||
#[test]
|
||||
fn open_invalid_params() {
|
||||
let mut h = unsafe { oakaudio_processor_init() };
|
||||
let p = Processor::init();
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_open(h, 0, 3, 4, 48000, 3, 4, 1.0) },
|
||||
code(p.open(params(0), params(48000), 1.0).unwrap_err()),
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0);
|
||||
assert_eq!(p.is_open().unwrap(), false);
|
||||
// The output format is forced to planar f32 by the processor contract.
|
||||
let mut wrong_out = params(48000);
|
||||
wrong_out.format = SampleFormat::F32;
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 0, 1.0) },
|
||||
code(p.open(params(48000), wrong_out, 1.0).unwrap_err()),
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 0);
|
||||
assert_eq!(p.is_open().unwrap(), false);
|
||||
|
||||
let empty = oakaudio::handle::CHandle::null();
|
||||
assert_eq!(open_identity(empty), OAKAUDIO_E_INVALID);
|
||||
// convert before open is a state error; a non-positive speed is
|
||||
// rejected on open.
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_is_open(empty) },
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_close(empty) },
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_flush(empty) },
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
let mut out_ptrs: Vec<*mut f32> = Vec::new();
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_convert(empty, std::ptr::null(), 0, out_ptrs.as_ptr(), 0) },
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
|
||||
// convert before open is a state error.
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_convert(h, std::ptr::null(), 0, out_ptrs.as_ptr(), 0) },
|
||||
code(p.convert(std::ptr::null(), 0, std::ptr::null(), 0).unwrap_err()),
|
||||
OAKAUDIO_E_STATE
|
||||
);
|
||||
unsafe { oakaudio_processor_free(&mut h) };
|
||||
assert_eq!(
|
||||
code(p.open(params(48000), params(48000), 0.0).unwrap_err()),
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
/// Resampling to half rate halves the frame count (44100 -> 22050). The
|
||||
@@ -161,34 +163,23 @@ fn open_invalid_params() {
|
||||
/// drained after `flush`; flush then keeps the processor open.
|
||||
#[test]
|
||||
fn resample_and_flush() {
|
||||
let mut h = unsafe { oakaudio_processor_init() };
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_open(h, 44100, 3, 4, 22050, 3, 4, 1.0) },
|
||||
OAKAUDIO_OK
|
||||
);
|
||||
let p = Processor::init();
|
||||
p.open(params(44100), params(22050), 1.0).unwrap();
|
||||
|
||||
// 1 second of input keeps the resampler delay well below the signal.
|
||||
let frames = 44100;
|
||||
let planes = ramp_planes(frames);
|
||||
let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
|
||||
let in_ptrs = plane_ptrs(&planes);
|
||||
let mut out = vec![vec![0f32; frames]; 2];
|
||||
let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect();
|
||||
let mut out_ptrs = plane_mut_ptrs(&mut out);
|
||||
|
||||
let mut total = unsafe {
|
||||
oakaudio_processor_convert(
|
||||
h,
|
||||
in_ptrs.as_ptr(),
|
||||
frames as i32,
|
||||
out_ptrs.as_ptr(),
|
||||
frames as i32,
|
||||
)
|
||||
};
|
||||
assert_eq!(unsafe { oakaudio_processor_flush(h) }, OAKAUDIO_OK);
|
||||
let mut total = p
|
||||
.convert(in_ptrs.as_ptr(), frames as i32, out_ptrs.as_ptr(), frames as i32)
|
||||
.unwrap();
|
||||
p.flush().unwrap();
|
||||
// Drain the resampler delay after end-of-input.
|
||||
while total < frames as i32 {
|
||||
let n = unsafe {
|
||||
oakaudio_processor_convert(h, std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32)
|
||||
};
|
||||
let n = p.convert(std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap();
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
@@ -198,8 +189,7 @@ fn resample_and_flush() {
|
||||
(total - 22050).abs() <= 2,
|
||||
"half-rate output must halve the frame count (got {total})"
|
||||
);
|
||||
assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 1);
|
||||
unsafe { oakaudio_processor_free(&mut h) };
|
||||
assert_eq!(p.is_open().unwrap(), true);
|
||||
}
|
||||
|
||||
/// A tempo factor != 1.0 time-stretches: tempo 2.0 halves the frame count
|
||||
@@ -207,33 +197,22 @@ fn resample_and_flush() {
|
||||
/// produces output) and the processor stays open.
|
||||
#[test]
|
||||
fn tempo_stretch() {
|
||||
let mut h = unsafe { oakaudio_processor_init() };
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 4, 2.0) },
|
||||
OAKAUDIO_OK
|
||||
);
|
||||
let p = Processor::init();
|
||||
p.open(params(48000), params(48000), 2.0).unwrap();
|
||||
|
||||
// 1 second of input: many atempo windows (1024 samples at 48 kHz).
|
||||
let frames = 48000;
|
||||
let planes = ramp_planes(frames);
|
||||
let in_ptrs: Vec<*const f32> = planes.iter().map(|p| p.as_ptr()).collect();
|
||||
let in_ptrs = plane_ptrs(&planes);
|
||||
let mut out = vec![vec![0f32; frames]; 2];
|
||||
let mut out_ptrs: Vec<*mut f32> = out.iter_mut().map(|p| p.as_mut_ptr()).collect();
|
||||
let mut out_ptrs = plane_mut_ptrs(&mut out);
|
||||
|
||||
let mut total = unsafe {
|
||||
oakaudio_processor_convert(
|
||||
h,
|
||||
in_ptrs.as_ptr(),
|
||||
frames as i32,
|
||||
out_ptrs.as_ptr(),
|
||||
frames as i32,
|
||||
)
|
||||
};
|
||||
assert_eq!(unsafe { oakaudio_processor_flush(h) }, OAKAUDIO_OK);
|
||||
let mut total = p
|
||||
.convert(in_ptrs.as_ptr(), frames as i32, out_ptrs.as_ptr(), frames as i32)
|
||||
.unwrap();
|
||||
p.flush().unwrap();
|
||||
while total < frames as i32 {
|
||||
let n = unsafe {
|
||||
oakaudio_processor_convert(h, std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32)
|
||||
};
|
||||
let n = p.convert(std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap();
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
@@ -243,24 +222,20 @@ fn tempo_stretch() {
|
||||
(total - 24000).abs() <= 2400,
|
||||
"tempo 2.0 must halve the frame count (got {total})"
|
||||
);
|
||||
assert_eq!(unsafe { oakaudio_processor_is_open(h) }, 1);
|
||||
unsafe { oakaudio_processor_close(h) };
|
||||
assert_eq!(p.is_open().unwrap(), true);
|
||||
p.close().unwrap();
|
||||
|
||||
// A non-positive speed is rejected (on a closed processor).
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_processor_open(h, 48000, 3, 4, 48000, 3, 4, 0.0) },
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(open_identity(h), OAKAUDIO_OK);
|
||||
// Already open -> state error.
|
||||
assert_eq!(open_identity(h), OAKAUDIO_E_STATE);
|
||||
unsafe { oakaudio_processor_free(&mut h) };
|
||||
// Re-opening a closed processor works; opening an open one is a state
|
||||
// error.
|
||||
open_identity(&p).unwrap();
|
||||
assert_eq!(code(open_identity(&p).unwrap_err()), OAKAUDIO_E_STATE);
|
||||
}
|
||||
|
||||
/// free(NULL)/free(empty) are no-ops.
|
||||
/// The error mapping is intact.
|
||||
#[test]
|
||||
fn free_null_noop() {
|
||||
let mut h = oakaudio::handle::CHandle::null();
|
||||
unsafe { oakaudio_processor_free(&mut h) };
|
||||
unsafe { oakaudio_processor_free(std::ptr::null_mut()) };
|
||||
fn error_codes() {
|
||||
assert_eq!(Error::Invalid.code(), OAKAUDIO_E_INVALID);
|
||||
assert_eq!(Error::State.code(), OAKAUDIO_E_STATE);
|
||||
assert_eq!(Error::Failed("x".to_string()).code(), -60003);
|
||||
assert_eq!(oakaudio::error::OAKAUDIO_OK, OAKAUDIO_OK);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user