feat(engine,app): source-monitor frames + real audio meter
source monitor: - new facade exports oakengine_renderer_create_for_node (render any node, not just sequences) and oakengine_project_footage_at (fetch a footage node by index); it_render covers the e2e render plus the NULL/illegal matrix - RealEngine renders the selected footage's real frames to the source viewer (per-node renderer slot, frame cache invalidated on selection change, synthetic fallback kept) - known gap (documented): pixels stay transparent black until oakrender's footage decode hook lands — the render surface itself is real end to end audio meter: - oakaudio manager can now report per-channel linear peaks of the buffered output (PreviewAudioDevice::peek_tail + levelmeter analysis of the newest 8192 frames, packed/planar F32) - new facade export oakengine_audio_output_levels (negative codes pass through); RealEngine's AudioMeterDataSource reads it instead of returning hardcoded silence - tests: module peak readback + facade validation/readback matrix
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
//! unwrap handles, call safe Rust, and map results through
|
||||
//! [`crate::handle::guard*`].
|
||||
|
||||
use std::ffi::{c_char, c_double, c_int, CStr};
|
||||
use std::ffi::{c_char, c_double, c_float, c_int, CStr};
|
||||
|
||||
use oakcore_rs::Rational;
|
||||
|
||||
@@ -217,6 +217,23 @@ pub mod manager {
|
||||
guard(|| crate::manager::stop_output(&_self))
|
||||
}
|
||||
|
||||
/// `oakaudio_manager_output_levels` — per-channel linear peaks of the
|
||||
/// buffered output into `peaks` (up to `capacity` entries). Returns
|
||||
/// the channel count (0 when nothing is buffered or no output is
|
||||
/// configured). OAKAUDIO_E_INVALID for NULL/zero-capacity out args.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakaudio_manager_output_levels(
|
||||
_self: CHandle,
|
||||
peaks: *mut c_float,
|
||||
capacity: c_int,
|
||||
) -> c_int {
|
||||
guard_int(|| {
|
||||
invalid_if(peaks.is_null() || capacity <= 0)?;
|
||||
let slice = unsafe { std::slice::from_raw_parts_mut(peaks, capacity as usize) };
|
||||
crate::manager::output_levels(&_self, slice)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakaudio_manager_seconds`: write elapsed playback seconds into `out`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakaudio_manager_seconds(_self: CHandle, out: *mut c_double) -> c_int {
|
||||
|
||||
@@ -372,6 +372,73 @@ pub fn find_device_by_name_s(name: &std::ffi::CStr, _is_output_device: bool) ->
|
||||
PA_NO_DEVICE
|
||||
}
|
||||
|
||||
/// Peak level (linear, 0..1 and above) of each channel of the buffered,
|
||||
/// not-yet-consumed output, written to `peaks` in channel order.
|
||||
/// Returns the channel count (0 when no output is configured or the
|
||||
/// layout is unknown). Only packed/planar F32 buffers are analyzed —
|
||||
/// other formats report zeroed peaks. The analysis window is the most
|
||||
/// recent 8192 frames of the queue.
|
||||
///
|
||||
/// There is no C++ counterpart (the Qt side metered inside the audio
|
||||
/// output callback); with the output callback unbridged this is how the
|
||||
/// UI reads levels.
|
||||
pub fn output_levels(self_: &CHandle, peaks: &mut [f32]) -> Result<i32> {
|
||||
let m = with_instance(self_)?;
|
||||
let Some(params) = m.output_params else {
|
||||
return Ok(0);
|
||||
};
|
||||
let channels = params.channel_count();
|
||||
if channels <= 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let n = (channels as usize).min(peaks.len());
|
||||
for p in &mut peaks[..n] {
|
||||
*p = 0.0;
|
||||
}
|
||||
use crate::params::SampleFormat;
|
||||
let packed = params.format == SampleFormat::F32;
|
||||
let planar = params.format == SampleFormat::F32Planar;
|
||||
if !packed && !planar {
|
||||
return Ok(channels);
|
||||
}
|
||||
let frames_max = 8192i64;
|
||||
let bpf = channels as i64 * 4;
|
||||
let mut buf = vec![0u8; (bpf * frames_max) as usize];
|
||||
let got = m.output_buffer.peek_tail(&mut buf);
|
||||
// Whole frames only; the tail is what we hold, so leading partial
|
||||
// bytes (when the queue is not frame-aligned) are dropped.
|
||||
let frames = got / bpf;
|
||||
if frames == 0 {
|
||||
return Ok(channels);
|
||||
}
|
||||
let bytes = (frames * bpf) as usize;
|
||||
let buf = &buf[..bytes];
|
||||
let frame_count = frames as usize;
|
||||
let channel_count = channels as usize;
|
||||
let mut planes: Vec<Vec<f32>> = vec![Vec::with_capacity(frame_count); channel_count];
|
||||
if packed {
|
||||
for frame in buf.chunks_exact(bpf as usize) {
|
||||
for ch in 0..channel_count {
|
||||
let b = &frame[ch * 4..ch * 4 + 4];
|
||||
planes[ch].push(f32::from_le_bytes([b[0], b[1], b[2], b[3]]));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (ch, plane) in planes.iter_mut().enumerate() {
|
||||
let start = ch * frame_count * 4;
|
||||
for b in buf[start..start + frame_count * 4].chunks_exact(4) {
|
||||
plane.push(f32::from_le_bytes([b[0], b[1], b[2], b[3]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
let views: Vec<&[f32]> = planes.iter().map(Vec::as_slice).collect();
|
||||
let stats = crate::levelmeter::analyze_sample_buffer(&views);
|
||||
for (i, p) in peaks[..n].iter_mut().enumerate() {
|
||||
*p = stats.channels[i].peak_linear as f32;
|
||||
}
|
||||
Ok(channels)
|
||||
}
|
||||
|
||||
/// Number of live oakaudio reference-counted objects (leak check).
|
||||
pub fn debug_alive_count() -> i32 {
|
||||
crate::handle::alive_count()
|
||||
|
||||
@@ -192,3 +192,19 @@ impl Default for PreviewAudioDevice {
|
||||
PreviewAudioDevice::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PreviewAudioDevice {
|
||||
/// Copy up to `data.len()` bytes from the TAIL of the queued buffer
|
||||
/// without consuming them (the level meter peeks at what is about to
|
||||
/// play; the oldest bytes are irrelevant for that). Returns the byte
|
||||
/// count copied.
|
||||
pub fn peek_tail(&self, data: &mut [u8]) -> i64 {
|
||||
let inner = self.lock.lock().unwrap();
|
||||
let copy = (data.len() as i64).min(inner.buffer.len() as i64);
|
||||
if copy > 0 {
|
||||
let start = inner.buffer.len() - copy as usize;
|
||||
data[..copy as usize].copy_from_slice(&inner.buffer[start..]);
|
||||
}
|
||||
copy
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ use oakaudio::ffi::manager::{
|
||||
oakaudio_manager_hard_reset, oakaudio_manager_instance, oakaudio_manager_push_to_output,
|
||||
oakaudio_manager_reset_output_clock, oakaudio_manager_seconds,
|
||||
oakaudio_manager_set_input_device, oakaudio_manager_set_output_device,
|
||||
oakaudio_manager_set_output_notify_interval, oakaudio_manager_start_recording,
|
||||
oakaudio_manager_output_levels, oakaudio_manager_set_output_notify_interval,
|
||||
oakaudio_manager_start_recording,
|
||||
oakaudio_manager_stop_output, oakaudio_manager_stop_recording,
|
||||
};
|
||||
|
||||
@@ -407,3 +408,71 @@ fn free_null_noop() {
|
||||
|
||||
unsafe { oakaudio_manager_destroy_instance() };
|
||||
}
|
||||
|
||||
/// output_levels: validation, the no-output case, and a real peak
|
||||
/// readback over pushed packed-F32 stereo samples (left ramps to 0.25,
|
||||
/// right to ~1.0 — the per-channel linear peaks).
|
||||
#[test]
|
||||
fn output_levels_reports_buffered_peaks() {
|
||||
let _guard = lock();
|
||||
unsafe { oakaudio_manager_destroy_instance() };
|
||||
assert_eq!(unsafe { oakaudio_manager_create_instance() }, OAKAUDIO_OK);
|
||||
let m = instance();
|
||||
|
||||
// Invalid out args.
|
||||
let mut peaks = [0.0f32; 4];
|
||||
assert_eq!(
|
||||
unsafe { oakaudio_manager_output_levels(m, std::ptr::null_mut(), 4) },
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(unsafe { oakaudio_manager_output_levels(m, peaks.as_mut_ptr(), 0) }, OAKAUDIO_E_INVALID);
|
||||
|
||||
// The manager singleton retains output params across tests in this
|
||||
// binary, so the "no output" case is not reachable here; instead
|
||||
// verify a cleared buffer reports zeroed peaks (channel count from
|
||||
// the configured layout, 0 only when nothing was ever configured).
|
||||
assert_eq!(unsafe { oakaudio_manager_set_output_device(m, 42) }, OAKAUDIO_OK);
|
||||
assert_eq!(unsafe { oakaudio_manager_clear_buffered_output(m) }, OAKAUDIO_OK);
|
||||
let cleared = unsafe { oakaudio_manager_output_levels(m, peaks.as_mut_ptr(), 4) };
|
||||
assert!(cleared >= 0);
|
||||
for p in &peaks[..cleared as usize] {
|
||||
assert_eq!(*p, 0.0, "cleared buffer must have silent peaks");
|
||||
}
|
||||
|
||||
// Push 480 frames of packed F32 stereo (format 10), stereo layout 0x3.
|
||||
let frames = 480usize;
|
||||
let mut samples = Vec::with_capacity(frames * 2);
|
||||
for i in 0..frames {
|
||||
let t = i as f32 / frames as f32;
|
||||
samples.push(0.25f32 * t);
|
||||
samples.push(t);
|
||||
}
|
||||
let rc = unsafe {
|
||||
oakaudio_manager_push_to_output(
|
||||
m,
|
||||
48000,
|
||||
0x3,
|
||||
10,
|
||||
samples.as_ptr() as *const c_char,
|
||||
(samples.len() * 4) as i64,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
assert_eq!(rc, OAKAUDIO_OK);
|
||||
|
||||
let n = unsafe { oakaudio_manager_output_levels(m, peaks.as_mut_ptr(), 4) };
|
||||
assert_eq!(n, 2);
|
||||
let last = (frames - 1) as f32 / frames as f32;
|
||||
assert!((peaks[0] - 0.25 * last).abs() < 1e-6, "left peak: {}", peaks[0]);
|
||||
assert!((peaks[1] - last).abs() < 1e-6, "right peak: {}", peaks[1]);
|
||||
|
||||
// Undersized buffer truncates the write, not the reported count.
|
||||
let mut one = [0.0f32; 1];
|
||||
assert_eq!(unsafe { oakaudio_manager_output_levels(m, one.as_mut_ptr(), 1) }, 2);
|
||||
|
||||
// Leave the singleton as we found it: the push flipped
|
||||
// `output_started`, which other tests' seconds() assertions depend on.
|
||||
assert_eq!(unsafe { oakaudio_manager_stop_output(m) }, OAKAUDIO_OK);
|
||||
assert_eq!(unsafe { oakaudio_manager_clear_buffered_output(m) }, OAKAUDIO_OK);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@ use std::ffi::{c_char, c_double, c_int, c_void};
|
||||
use crate::bridge::audio as a;
|
||||
use crate::error::Error;
|
||||
use crate::handle::{
|
||||
box_handle, free_box, guard, guard_i64, guard_void, unbox, CHandle, OakEngineAudioProcessor,
|
||||
box_handle, free_box, guard, guard_i64, guard_int, guard_void, unbox, CHandle,
|
||||
OakEngineAudioProcessor,
|
||||
};
|
||||
|
||||
/// paNoDevice — no audio device selected.
|
||||
@@ -150,6 +151,31 @@ pub extern "C" fn oakengine_audio_clear_buffered_output() -> c_int {
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_audio_output_levels` — per-channel linear peaks of the
|
||||
/// buffered, not-yet-consumed output into `peaks` (up to `capacity`
|
||||
/// entries). Returns the channel count (0 when nothing is buffered).
|
||||
/// The UI's audio meter reads this; there is no C++ counterpart (the Qt
|
||||
/// side metered inside the output callback, which is not bridged).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_audio_output_levels(peaks: *mut f32, capacity: c_int) -> c_int {
|
||||
guard_int(|| {
|
||||
if peaks.is_null() || capacity <= 0 {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let m = manager();
|
||||
if m.is_null() {
|
||||
return Err(Error::Failed("no AudioManager instance".into()));
|
||||
}
|
||||
// The module returns the channel count (>= 0) or a negative
|
||||
// OAKAUDIO_E_* code, which passes through untouched.
|
||||
let n = a::oakaudio_manager_output_levels(m, peaks, capacity);
|
||||
if n < 0 {
|
||||
return Err(Error::Module(n));
|
||||
}
|
||||
Ok(n)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_audio_push_to_output` — queue interleaved samples described
|
||||
/// by the borrowed `OakAudioParams*` handle.
|
||||
#[no_mangle]
|
||||
|
||||
@@ -216,6 +216,12 @@ pub fn oakaudio_manager_seconds(_self: CHandle, out: *mut c_double) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_seconds(_self, out) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_manager_output_levels(_self: CHandle, peaks: *mut f32, capacity: c_int) -> c_int {
|
||||
unsafe { oakaudio::ffi::manager::oakaudio_manager_output_levels(_self, peaks, capacity) }
|
||||
}
|
||||
|
||||
/// Direct call into the `oakaudio` crate (single-lib unification; the
|
||||
/// `#[no_mangle]` export stays for the external C ABI).
|
||||
pub fn oakaudio_processor_init() -> CHandle {
|
||||
|
||||
@@ -692,6 +692,25 @@ pub unsafe extern "C" fn oakengine_project_footage_is_online(
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_project_footage_at` — the footage node at `index`, boxed
|
||||
/// (freed with `oakengine_node_free`); NULL for an invalid index or
|
||||
/// project. The node is borrowed from the project graph, so it stays
|
||||
/// valid until the project is freed.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_project_footage_at(
|
||||
self_: *const OakEngineProject,
|
||||
index: c_int,
|
||||
) -> *mut OakEngineNode {
|
||||
guard_ptr(|| unsafe {
|
||||
let h = unbox(self_)?;
|
||||
let footage = project_node_at_of_type(h, TYPE_ID_FOOTAGE, index);
|
||||
if footage.is_null() {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
Ok(box_handle::<OakEngineNode>(footage))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_project_can_undo`.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakengine_project_can_undo(self_: *const OakEngineProject) -> c_int {
|
||||
|
||||
@@ -17,12 +17,14 @@
|
||||
//! `engine/include/oakengine/{renderer,color,lut}.h` over the oakrender
|
||||
//! module.
|
||||
//!
|
||||
//! - The **renderer** is a facade-owned box binding a sequence handle to
|
||||
//! an output geometry; each render call submits an oakrender ticket
|
||||
//! (`OakVideoTicketParams`), waits for it and returns the produced
|
||||
//! frame (`OakCodecFrame` wrapped in `OakEngineFrame`). Audio rendering
|
||||
//! submits the ticket but the crate's samples path is unimplemented, so
|
||||
//! it fails with the reason in `last_error`.
|
||||
//! - The **renderer** is a facade-owned box binding an output node
|
||||
//! (usually a sequence, or any single node via
|
||||
//! `oakengine_renderer_create_for_node`) to an output geometry; each
|
||||
//! render call submits an oakrender ticket (`OakVideoTicketParams`),
|
||||
//! waits for it and returns the produced frame (`OakCodecFrame` wrapped
|
||||
//! in `OakEngineFrame`). Audio rendering submits the ticket but the
|
||||
//! crate's samples path is unimplemented, so it fails with the reason
|
||||
//! in `last_error`.
|
||||
//! - The **frame accessors** read the wrapped `OakCodecFrame`
|
||||
//! (`channel_count` has no crate accessor and reports 0).
|
||||
//! - The **color processor** family maps onto
|
||||
@@ -110,10 +112,11 @@ pub unsafe extern "C" fn oakengine_render_cache_set_multicam_node(
|
||||
// Renderer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Facade-side renderer box: the bound sequence + output geometry.
|
||||
/// Facade-side renderer box: the bound output node + output geometry.
|
||||
struct RendererBox {
|
||||
/// Unboxed sequence node handle (borrowed).
|
||||
seq: CHandle,
|
||||
/// Unboxed output node handle (borrowed): a sequence, or any node the
|
||||
/// module can evaluate (footage, generator, ...).
|
||||
output_node: CHandle,
|
||||
/// Output width.
|
||||
width: c_int,
|
||||
/// Output height.
|
||||
@@ -180,20 +183,20 @@ unsafe fn make_video_params(b: &RendererBox) -> Result<CHandle> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakengine_renderer_create` — NULL for invalid arguments.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_renderer_create(
|
||||
seq: *mut OakEngineSequence,
|
||||
/// Build a renderer box for `output_node` (a sequence or any node the
|
||||
/// module can evaluate) with the given geometry. Returns NULL for
|
||||
/// non-positive geometry/rate or a pixel format outside the oakcore enum.
|
||||
unsafe fn make_renderer_box(
|
||||
output_node: CHandle,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
pixel_format: c_int,
|
||||
frame_rate_num: c_int,
|
||||
frame_rate_den: c_int,
|
||||
output_colorspace: *const c_char,
|
||||
) -> *mut OakEngineRenderer {
|
||||
guard_ptr(|| unsafe {
|
||||
if seq.is_null() || width <= 0 || height <= 0 || frame_rate_num <= 0 || frame_rate_den <= 0
|
||||
{
|
||||
) -> Result<*mut OakEngineRenderer> {
|
||||
unsafe {
|
||||
if width <= 0 || height <= 0 || frame_rate_num <= 0 || frame_rate_den <= 0 {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
// Validate the pixel format against the oakcore enum. The
|
||||
@@ -207,9 +210,8 @@ pub unsafe extern "C" fn oakengine_renderer_create(
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let _ = crate::handle::read_cstr(output_colorspace); // resolved by the module at render time
|
||||
let seq_handle = unbox(seq)?;
|
||||
let boxed = Box::new(RendererBox {
|
||||
seq: seq_handle,
|
||||
output_node,
|
||||
width,
|
||||
height,
|
||||
pixel_format,
|
||||
@@ -219,6 +221,67 @@ pub unsafe extern "C" fn oakengine_renderer_create(
|
||||
last_error: String::new(),
|
||||
});
|
||||
Ok(Box::into_raw(boxed) as *mut OakEngineRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakengine_renderer_create` — NULL for invalid arguments.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_renderer_create(
|
||||
seq: *mut OakEngineSequence,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
pixel_format: c_int,
|
||||
frame_rate_num: c_int,
|
||||
frame_rate_den: c_int,
|
||||
output_colorspace: *const c_char,
|
||||
) -> *mut OakEngineRenderer {
|
||||
guard_ptr(|| unsafe {
|
||||
if seq.is_null() {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let seq_handle = unbox(seq)?;
|
||||
make_renderer_box(
|
||||
seq_handle,
|
||||
width,
|
||||
height,
|
||||
pixel_format,
|
||||
frame_rate_num,
|
||||
frame_rate_den,
|
||||
output_colorspace,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakengine_renderer_create_for_node` — like `oakengine_renderer_create`,
|
||||
/// but binds any node instead of a sequence: the surface for rendering a
|
||||
/// single footage/generator node (the source monitor). The renderer is
|
||||
/// freed with `oakengine_renderer_free` and renders with
|
||||
/// `oakengine_renderer_render_frame`, exactly like the sequence renderer.
|
||||
/// NULL for invalid arguments.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_renderer_create_for_node(
|
||||
node: *mut OakEngineNode,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
pixel_format: c_int,
|
||||
frame_rate_num: c_int,
|
||||
frame_rate_den: c_int,
|
||||
output_colorspace: *const c_char,
|
||||
) -> *mut OakEngineRenderer {
|
||||
guard_ptr(|| unsafe {
|
||||
if node.is_null() {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let node_handle = unbox(node)?;
|
||||
make_renderer_box(
|
||||
node_handle,
|
||||
width,
|
||||
height,
|
||||
pixel_format,
|
||||
frame_rate_num,
|
||||
frame_rate_den,
|
||||
output_colorspace,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -272,7 +335,7 @@ pub unsafe extern "C" fn oakengine_renderer_render_frame(
|
||||
let b = renderer_mut(self_)?;
|
||||
let video_params = make_video_params(b)?;
|
||||
let params = OakVideoTicketParams {
|
||||
output_node: b.seq,
|
||||
output_node: b.output_node,
|
||||
video_params,
|
||||
audio_params: std::ptr::null(),
|
||||
time_num: timestamp * i64::from(b.frame_rate_den),
|
||||
@@ -325,7 +388,7 @@ pub unsafe extern "C" fn oakengine_renderer_render_audio(
|
||||
let end_num = (start_timestamp + length_timestamp) * i64::from(b.frame_rate_den);
|
||||
let den = i64::from(b.frame_rate_num);
|
||||
let ticket = r::oakrender_ticket_render_audio(
|
||||
b.seq,
|
||||
b.output_node,
|
||||
start_num,
|
||||
den,
|
||||
end_num,
|
||||
|
||||
@@ -54,6 +54,7 @@ use std::sync::Mutex;
|
||||
|
||||
use oakengine::audio::{
|
||||
oakengine_audio_clear_buffered_output, oakengine_audio_create_instance,
|
||||
oakengine_audio_output_levels,
|
||||
oakengine_audio_destroy_instance, oakengine_audio_estimate_envelope_offset,
|
||||
oakengine_audio_estimate_stretch_and_offset, oakengine_audio_get_input_device,
|
||||
oakengine_audio_get_output_device, oakengine_audio_hard_reset, oakengine_audio_manager_handle,
|
||||
@@ -1222,3 +1223,58 @@ fn processor_full_open_convert_cycle() {
|
||||
unsafe { oakengine_audio_processor_free(p) };
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// oakengine_audio_output_levels
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The output-level meter export: validation, the no-output case, and a
|
||||
/// real peak readback over pushed F32 stereo samples.
|
||||
#[test]
|
||||
fn audio_output_levels() {
|
||||
with_manager(|| unsafe {
|
||||
// Out-arg validation (facade E_INVALID; no manager needed).
|
||||
let mut peaks = [0.0f32; 4];
|
||||
assert_eq!(oakengine_audio_output_levels(std::ptr::null_mut(), 4), OAKENGINE_E_INVALID);
|
||||
assert_eq!(oakengine_audio_output_levels(peaks.as_mut_ptr(), 0), OAKENGINE_E_INVALID);
|
||||
assert_eq!(oakengine_audio_output_levels(peaks.as_mut_ptr(), -1), OAKENGINE_E_INVALID);
|
||||
|
||||
// Fresh-ish manager, nothing buffered: 0 channels.
|
||||
assert_eq!(oakengine_audio_destroy_instance(), 0);
|
||||
assert_eq!(oakengine_audio_create_instance(), 0);
|
||||
assert_eq!(oakengine_audio_output_levels(peaks.as_mut_ptr(), 4), 0);
|
||||
|
||||
// Push 480 frames of packed F32 stereo: left ramps to 0.25, right
|
||||
// ramps to ~1.0. The levels are the per-channel linear peaks.
|
||||
assert_eq!(oakengine_audio_set_output_device(42), 0);
|
||||
assert_eq!(oakengine_audio_clear_buffered_output(), 0);
|
||||
let frames = 480usize;
|
||||
let mut samples = Vec::with_capacity(frames * 2);
|
||||
for i in 0..frames {
|
||||
let t = i as f32 / frames as f32;
|
||||
samples.push(0.25f32 * t);
|
||||
samples.push(t);
|
||||
}
|
||||
let params = audio_params(48000, 0x3, 10); // 10 = packed F32
|
||||
let rc = oakengine_audio_push_to_output(
|
||||
params as *const c_void,
|
||||
samples.as_ptr() as *const std::ffi::c_char,
|
||||
(samples.len() * 4) as i64,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
);
|
||||
common::oakcore_audioparams_free(params);
|
||||
assert_eq!(rc, 0);
|
||||
|
||||
let n = oakengine_audio_output_levels(peaks.as_mut_ptr(), 4);
|
||||
assert_eq!(n, 2);
|
||||
let last = (frames - 1) as f32 / frames as f32;
|
||||
assert!((peaks[0] - 0.25 * last).abs() < 1e-6, "left peak: {}", peaks[0]);
|
||||
assert!((peaks[1] - last).abs() < 1e-6, "right peak: {}", peaks[1]);
|
||||
|
||||
// Capacity smaller than the channel count truncates the write but
|
||||
// still reports the real channel count.
|
||||
let mut one = [0.0f32; 1];
|
||||
assert_eq!(oakengine_audio_output_levels(one.as_mut_ptr(), 1), 2);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,8 +45,9 @@ use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
use oakengine::handle::OakEngineAudioBuffer;
|
||||
use oakengine::node::{
|
||||
oakengine_node_factory_create_from_id, oakengine_node_free, oakengine_project_create,
|
||||
oakengine_project_free, oakengine_project_new,
|
||||
oakengine_footage_free, oakengine_node_factory_create_from_id, oakengine_node_free,
|
||||
oakengine_project_create, oakengine_project_footage_at, oakengine_project_footage_count,
|
||||
oakengine_project_free, oakengine_project_import_footage, oakengine_project_new,
|
||||
};
|
||||
use oakengine::render::{
|
||||
oakengine_audio_channel_count, oakengine_audio_data, oakengine_audio_free,
|
||||
@@ -73,9 +74,9 @@ use oakengine::render::{
|
||||
oakengine_render_cache_set_display_color_processor, oakengine_render_cache_set_multicam_node,
|
||||
oakengine_render_manager_backend_to_string, oakengine_render_manager_requested_backend,
|
||||
oakengine_render_manager_set_aggressive_garbage_collection, oakengine_renderer_cancel,
|
||||
oakengine_renderer_create, oakengine_renderer_free, oakengine_renderer_last_error,
|
||||
oakengine_renderer_render_audio, oakengine_renderer_render_frame, oakengine_renderer_set_mode,
|
||||
OakColorTransformPod,
|
||||
oakengine_renderer_create, oakengine_renderer_create_for_node, oakengine_renderer_free,
|
||||
oakengine_renderer_last_error, oakengine_renderer_render_audio,
|
||||
oakengine_renderer_render_frame, oakengine_renderer_set_mode, OakColorTransformPod,
|
||||
};
|
||||
use oakengine::timeline::oakengine_sequence_new;
|
||||
|
||||
@@ -417,6 +418,154 @@ fn err_buf() -> [c_char; 128] {
|
||||
[0 as c_char; 128]
|
||||
}
|
||||
|
||||
/// End-to-end single-node CPU render: `oakengine_renderer_create_for_node`
|
||||
/// binds a footage node (instead of a sequence) and `render_frame` produces
|
||||
/// a real F32 frame through the module's eval pipeline. The footage node
|
||||
/// comes from `oakengine_project_footage_at` (the import registered it in
|
||||
/// the project graph) — the surface the source monitor renders through.
|
||||
#[test]
|
||||
fn renderer_render_frame_for_node_e2e() {
|
||||
common::force_link();
|
||||
let _g = state_lock();
|
||||
|
||||
unsafe { oakrender::ffi::manager::oakrender_manager_shutdown() };
|
||||
assert_eq!(
|
||||
unsafe { oakrender::ffi::manager::oakrender_manager_init() },
|
||||
0
|
||||
);
|
||||
|
||||
let base = unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() };
|
||||
|
||||
let project = unsafe { oakengine_project_create() };
|
||||
assert_eq!(unsafe { oakengine_project_new(project) }, 0);
|
||||
|
||||
// Import a footage file so the project graph owns a footage node.
|
||||
let media = temp_media_file();
|
||||
let media_c = std::ffi::CString::new(media.to_string_lossy().into_owned()).unwrap();
|
||||
let footage = unsafe { oakengine_project_import_footage(project, media_c.as_ptr()) };
|
||||
assert!(!footage.is_null());
|
||||
unsafe { oakengine_footage_free(footage) };
|
||||
assert_eq!(unsafe { oakengine_project_footage_count(project) }, 1);
|
||||
|
||||
// The footage node at index 0 is boxed; freed with node_free.
|
||||
let node = unsafe { oakengine_project_footage_at(project, 0) };
|
||||
assert!(
|
||||
!node.is_null(),
|
||||
"imported footage must be addressable by index"
|
||||
);
|
||||
|
||||
// NULL project / out-of-range indices → NULL.
|
||||
assert!(unsafe { oakengine_project_footage_at(std::ptr::null(), 0) }.is_null());
|
||||
assert!(unsafe { oakengine_project_footage_at(project, -1) }.is_null());
|
||||
assert!(unsafe { oakengine_project_footage_at(project, 5) }.is_null());
|
||||
|
||||
// NULL node → NULL renderer for every geometry combination.
|
||||
for (w, h, pf, num, den) in [
|
||||
(1920, 1080, 4, 30000, 1001),
|
||||
(0, 1080, 4, 30000, 1001),
|
||||
(1920, 0, 4, 30000, 1001),
|
||||
(1920, 1080, 4, 0, 1001),
|
||||
] {
|
||||
let r = unsafe {
|
||||
oakengine_renderer_create_for_node(
|
||||
std::ptr::null_mut(),
|
||||
w,
|
||||
h,
|
||||
pf,
|
||||
num,
|
||||
den,
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
assert!(r.is_null(), "NULL node (w={w} h={h} {num}/{den}) must give NULL");
|
||||
}
|
||||
|
||||
// 1920x1080 F32 renderer over the footage node.
|
||||
let r = unsafe {
|
||||
oakengine_renderer_create_for_node(
|
||||
node,
|
||||
1920,
|
||||
1080,
|
||||
4,
|
||||
30000,
|
||||
1001,
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
assert!(!r.is_null());
|
||||
|
||||
// A legal render returns a real frame with the renderer's geometry.
|
||||
let f = unsafe { oakengine_renderer_render_frame(r, 0) };
|
||||
assert!(
|
||||
!f.is_null(),
|
||||
"render_frame over a footage node must produce a frame"
|
||||
);
|
||||
assert_eq!(unsafe { oakengine_frame_width(f) }, 1920);
|
||||
assert_eq!(unsafe { oakengine_frame_height(f) }, 1080);
|
||||
assert_eq!(unsafe { oakengine_frame_format(f) }, 4); // F32 pipeline format
|
||||
assert_eq!(unsafe { oakengine_frame_linesize_bytes(f) }, 1920 * 4 * 4);
|
||||
assert!(!unsafe { oakengine_frame_data(f) }.is_null());
|
||||
assert_eq!(
|
||||
unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() },
|
||||
base + 1
|
||||
);
|
||||
unsafe { oakengine_frame_free(f) };
|
||||
assert_eq!(
|
||||
unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() },
|
||||
base
|
||||
);
|
||||
|
||||
// Illegal geometry / pixel format with a valid node → NULL.
|
||||
let r_bad_geom = unsafe {
|
||||
oakengine_renderer_create_for_node(
|
||||
node,
|
||||
0,
|
||||
1080,
|
||||
4,
|
||||
30000,
|
||||
1001,
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
assert!(r_bad_geom.is_null(), "zero width must give NULL");
|
||||
let r_bad_pf = unsafe {
|
||||
oakengine_renderer_create_for_node(
|
||||
node,
|
||||
64,
|
||||
48,
|
||||
99999,
|
||||
30000,
|
||||
1001,
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
assert!(r_bad_pf.is_null(), "garbage pixel format must give NULL");
|
||||
|
||||
unsafe { oakengine_renderer_free(r) };
|
||||
unsafe { oakengine_node_free(node) };
|
||||
unsafe { oakengine_project_free(project) };
|
||||
assert_eq!(
|
||||
unsafe { oakrender::ffi::cache::oakrender_debug_alive_count() },
|
||||
base
|
||||
);
|
||||
|
||||
unsafe { oakrender::ffi::manager::oakrender_manager_shutdown() };
|
||||
assert_eq!(
|
||||
unsafe { oakrender::ffi::manager::oakrender_manager_available() },
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/// Fresh media file under the system temp dir with a unique name.
|
||||
fn temp_media_file() -> std::path::PathBuf {
|
||||
let p = std::env::temp_dir().join(format!(
|
||||
"oak-it-render-media-{}.mp4",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::write(&p, b"not-real-media").expect("write temp file");
|
||||
p
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Frame accessors (7 exports)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -320,6 +320,12 @@ unsafe extern "C" {
|
||||
) -> c_int;
|
||||
/// `oakengine_project_footage_count`.
|
||||
pub fn oakengine_project_footage_count(self_: *const OakEngineProject) -> c_int;
|
||||
/// `oakengine_project_footage_at` — boxed footage node at `index` (free
|
||||
/// with `oakengine_node_free`); NULL for an invalid index.
|
||||
pub fn oakengine_project_footage_at(
|
||||
self_: *const OakEngineProject,
|
||||
index: c_int,
|
||||
) -> *mut OakEngineNode;
|
||||
/// `oakengine_project_footage_filename` (buf/size).
|
||||
pub fn oakengine_project_footage_filename(
|
||||
self_: *const OakEngineProject,
|
||||
@@ -616,6 +622,19 @@ unsafe extern "C" {
|
||||
frame_rate_den: c_int,
|
||||
output_colorspace: *const c_char,
|
||||
) -> *mut OakEngineRenderer;
|
||||
/// `oakengine_renderer_create_for_node` — like `oakengine_renderer_create`,
|
||||
/// but binds any node instead of a sequence: the surface for rendering a
|
||||
/// single footage node (the source monitor). Freed and rendered exactly
|
||||
/// like the sequence renderer.
|
||||
pub fn oakengine_renderer_create_for_node(
|
||||
node: *mut OakEngineNode,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
pixel_format: c_int,
|
||||
frame_rate_num: c_int,
|
||||
frame_rate_den: c_int,
|
||||
output_colorspace: *const c_char,
|
||||
) -> *mut OakEngineRenderer;
|
||||
/// `oakengine_renderer_free` — consuming free (NULL no-op).
|
||||
pub fn oakengine_renderer_free(self_: *mut OakEngineRenderer);
|
||||
/// `oakengine_renderer_render_frame` — synchronous render of the frame at
|
||||
@@ -644,6 +663,13 @@ unsafe extern "C" {
|
||||
/// `oakengine_frame_free` — consuming free (NULL no-op).
|
||||
pub fn oakengine_frame_free(self_: *mut OakEngineFrame);
|
||||
|
||||
// -- oakengine::audio --
|
||||
|
||||
/// `oakengine_audio_output_levels` — per-channel linear peaks of the
|
||||
/// buffered output into `peaks` (up to `capacity` entries); returns
|
||||
/// the channel count (0 = nothing buffered), negative on error.
|
||||
pub fn oakengine_audio_output_levels(peaks: *mut f32, capacity: c_int) -> c_int;
|
||||
|
||||
// -- oakrender module C ABI (carried by the dylib) --
|
||||
|
||||
/// `oakrender_manager_init` — bring up the module's process-global
|
||||
|
||||
+181
-21
@@ -41,13 +41,16 @@
|
||||
//!
|
||||
//! # What is still mock/stub
|
||||
//!
|
||||
//! * The source monitor's viewer frames are the shared synthetic SMPTE
|
||||
//! pattern ([`frames`]): the facade renderer binds a *sequence* handle
|
||||
//! only, so there is no surface to render a single footage node for the
|
||||
//! material viewer. The program monitor renders real frames through the
|
||||
//! facade CPU renderer ([`RealEngine::render_program_frame`]) at a proxy
|
||||
//! resolution; the full-resolution async render worker (the facade's
|
||||
//! worker module) is a separate process surface not bound yet.
|
||||
//! * The source monitor renders the selected footage node's frame through
|
||||
//! the facade CPU renderer
|
||||
//! ([`RealEngine::render_source_frame`],
|
||||
//! via the node-binding `oakengine_renderer_create_for_node`) at a proxy
|
||||
//! resolution — the same pattern as the program monitor
|
||||
//! ([`RealEngine::render_program_frame`]). Actual media *decode* is
|
||||
//! still a module gap (the oakrender eval's footage hook is deferred),
|
||||
//! so both viewers show the pipeline's generated frame, not the file's
|
||||
//! pixels; the full-resolution async render worker (the facade's worker
|
||||
//! module) is a separate process surface not bound yet.
|
||||
//! * Effect stack — the selected clip's effect chain is bound: the stack
|
||||
//! reads the chain through the facade (see
|
||||
//! [`EffectStackDataSource`](EffectStackDataSource) for `RealEngine`)
|
||||
@@ -653,10 +656,10 @@ pub struct RealEngine {
|
||||
/// Cache of the CPU frames handed to the viewers, keyed by monitor.
|
||||
/// Entries are the playhead frame that produced the image plus the scope
|
||||
/// samples analyzed in the same pass, so a paused viewer never
|
||||
/// regenerates its picture (or its scopes). The program monitor's entries
|
||||
/// are real rendered frames (see [`RealEngine::render_program_frame`]);
|
||||
/// the source monitor's are the synthetic pattern (the facade renderer
|
||||
/// binds a sequence only — footage-node rendering is a documented gap).
|
||||
/// regenerates its picture (or its scopes). Both monitors hold real
|
||||
/// rendered frames (see [`RealEngine::render_program_frame`] /
|
||||
/// [`RealEngine::render_source_frame`]); the synthetic pattern is only
|
||||
/// the failure fallback.
|
||||
cpu_frame_cache: Mutex<HashMap<Monitor, (i64, Arc<RenderImage>, ScopeData)>>,
|
||||
/// The program monitor's cached renderer, created lazily from the
|
||||
/// current sequence at a proxy resolution. The mutex both provides the
|
||||
@@ -664,6 +667,12 @@ pub struct RealEngine {
|
||||
/// the synchronous render calls. Reset to [`RendererSlot::Untried`]
|
||||
/// (before the sequence is freed) in [`RealEngine::drop_project`].
|
||||
renderer: Mutex<RendererSlot>,
|
||||
/// The source monitor's cached renderer, created lazily from the
|
||||
/// currently selected footage node at a proxy resolution (same slot
|
||||
/// semantics as `renderer`). Reset to [`RendererSlot::Untried`] when
|
||||
/// the selection changes or the project is dropped — the renderer binds
|
||||
/// the footage node, so a new selection must bind the new node.
|
||||
source_renderer: Mutex<RendererSlot>,
|
||||
/// Whether the project has unsaved changes (mirrors the facade flag).
|
||||
modified: bool,
|
||||
}
|
||||
@@ -692,6 +701,7 @@ impl RealEngine {
|
||||
meter_phase: 0,
|
||||
cpu_frame_cache: Mutex::new(HashMap::new()),
|
||||
renderer: Mutex::new(RendererSlot::Untried),
|
||||
source_renderer: Mutex::new(RendererSlot::Untried),
|
||||
modified: false,
|
||||
}
|
||||
}
|
||||
@@ -862,6 +872,140 @@ impl RealEngine {
|
||||
Some(RendererHandle(renderer))
|
||||
}
|
||||
|
||||
/// The material-bin footage index of the selected entry, if any. The bin
|
||||
/// children are numbered `100 + index` (see [`RealEngine::rebuild_bin`]);
|
||||
/// folder/project roots are not footage.
|
||||
fn selected_footage_index(&self) -> Option<c_int> {
|
||||
let id = self.selected_item?;
|
||||
if id >= 100 {
|
||||
Some((id - 100) as c_int)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The selected footage's facade node view (a boxed node handle the
|
||||
/// caller frees with `oakengine_node_free`), or `None` when no footage
|
||||
/// is selected or the index is out of range.
|
||||
fn selected_footage_node(&self) -> Option<*mut OakEngineNode> {
|
||||
let project = self.project_ptr()?;
|
||||
let index = self.selected_footage_index()?;
|
||||
let node = unsafe { oakengine_project_footage_at(project, index) };
|
||||
if node.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(node)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the per-footage renderer at the proxy resolution (see
|
||||
/// [`RealEngine::proxy_render_size`]). The renderer borrows the footage
|
||||
/// node handle; the caller owns the slot it is stored in.
|
||||
fn create_node_renderer(&self, node: *mut OakEngineNode) -> Option<RendererHandle> {
|
||||
let (width, height) = self.proxy_render_size()?;
|
||||
let rate = self.sequence_info.as_ref()?.format.rate;
|
||||
let renderer = unsafe {
|
||||
oakengine_renderer_create_for_node(
|
||||
node,
|
||||
width,
|
||||
height,
|
||||
PIXEL_FORMAT_F32,
|
||||
rate.num as c_int,
|
||||
rate.den as c_int,
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
if renderer.is_null() {
|
||||
println!(
|
||||
"[real engine] renderer_create_for_node failed; viewer keeps the synthetic frame"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Some(RendererHandle(renderer))
|
||||
}
|
||||
|
||||
/// Renders one source-monitor frame through the facade CPU renderer:
|
||||
/// creates the per-footage renderer lazily (cached in
|
||||
/// `self.source_renderer`, bound to the currently selected footage
|
||||
/// node), renders `frame`, analyzes the scope samples from the F32 RGBA
|
||||
/// result, and downconverts to BGRA8. Returns `None` (the caller falls
|
||||
/// back to the synthetic pattern) when no footage is selected, the
|
||||
/// render manager is unavailable, or the render itself fails.
|
||||
fn render_source_frame(&self, frame: Frame) -> Option<(RenderImage, ScopeData)> {
|
||||
let node = self.selected_footage_node()?;
|
||||
if !Self::ensure_render_manager() {
|
||||
// SAFETY: `node` is a box from `selected_footage_node`.
|
||||
unsafe { oakengine_node_free(node) };
|
||||
return None;
|
||||
}
|
||||
let mut slot = self.source_renderer.lock().unwrap();
|
||||
match &*slot {
|
||||
RendererSlot::Unavailable => {
|
||||
unsafe { oakengine_node_free(node) };
|
||||
return None;
|
||||
}
|
||||
RendererSlot::Untried => {
|
||||
let created = self.create_node_renderer(node);
|
||||
*slot = match created {
|
||||
Some(handle) => RendererSlot::Ready(handle),
|
||||
None => RendererSlot::Unavailable,
|
||||
};
|
||||
}
|
||||
RendererSlot::Ready(_) => {}
|
||||
}
|
||||
// SAFETY: `node` is a live box; freed on every path below.
|
||||
unsafe { oakengine_node_free(node) };
|
||||
let RendererSlot::Ready(handle) = &*slot else {
|
||||
return None;
|
||||
};
|
||||
let renderer = handle.ptr();
|
||||
let frame_ptr = unsafe { oakengine_renderer_render_frame(renderer, frame.0) };
|
||||
if frame_ptr.is_null() {
|
||||
let error = read_string(|buf, size| unsafe {
|
||||
oakengine_renderer_last_error(renderer, buf, size)
|
||||
});
|
||||
println!("[real engine] source render_frame failed: {error}");
|
||||
// Don't retry (and re-log) on every frame.
|
||||
*slot = RendererSlot::Unavailable;
|
||||
return None;
|
||||
}
|
||||
// Read the frame (F32 RGBA, rows padded to linesize), repack it
|
||||
// tightly, then downconvert.
|
||||
let (width, height, linesize, format) = unsafe {
|
||||
(
|
||||
oakengine_frame_width(frame_ptr),
|
||||
oakengine_frame_height(frame_ptr),
|
||||
oakengine_frame_linesize_bytes(frame_ptr),
|
||||
oakengine_frame_format(frame_ptr),
|
||||
)
|
||||
};
|
||||
let data = unsafe { oakengine_frame_data(frame_ptr) };
|
||||
let mut image = None;
|
||||
if width > 0 && height > 0 && format == PIXEL_FORMAT_F32 && !data.is_null() {
|
||||
let row_bytes = (width * 4 * 4) as usize;
|
||||
let linesize = (linesize as usize).max(row_bytes);
|
||||
let mut samples = vec![0.0f32; (width * height * 4) as usize];
|
||||
for y in 0..height as usize {
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
(data as *const u8).add(y * linesize),
|
||||
samples.as_mut_ptr().add(y * row_bytes / 4) as *mut u8,
|
||||
row_bytes,
|
||||
);
|
||||
}
|
||||
}
|
||||
let scope = analyze_f32_rgba(width as u32, height as u32, &samples);
|
||||
image = Some((
|
||||
f32_rgba_to_bgra_image(width as u32, height as u32, &samples),
|
||||
scope,
|
||||
));
|
||||
}
|
||||
unsafe {
|
||||
oakengine_frame_free(frame_ptr);
|
||||
}
|
||||
image
|
||||
}
|
||||
|
||||
/// Adopts a newly created/loaded facade project, freeing any previous
|
||||
/// one, and rebuilds every snapshot. `blank` projects get a default
|
||||
/// sequence; loaded ones use the first sequence.
|
||||
@@ -907,6 +1051,7 @@ impl RealEngine {
|
||||
/// borrowed from the project).
|
||||
fn drop_project(&mut self) {
|
||||
*self.renderer.lock().unwrap() = RendererSlot::Untried;
|
||||
*self.source_renderer.lock().unwrap() = RendererSlot::Untried;
|
||||
drop(self.sequence.take());
|
||||
drop(self.project.take());
|
||||
self.cpu_frame_cache.lock().unwrap().clear();
|
||||
@@ -1443,9 +1588,17 @@ impl ProjectDataSource for RealEngine {
|
||||
|
||||
impl AudioMeterDataSource for RealEngine {
|
||||
fn levels(&self) -> Vec<f32> {
|
||||
// Real audio levels are not exposed by the facade; report a silent
|
||||
// (but alive) meter.
|
||||
vec![0.0, 0.0]
|
||||
// Per-channel linear peaks of the engine's buffered audio output
|
||||
// (facade `oakengine_audio_output_levels`, clamped to the meter's
|
||||
// 0..1 range). Silent when nothing has been pushed to the output
|
||||
// (no playback audio path yet) or on any facade error.
|
||||
let mut peaks = [0.0f32; 8];
|
||||
// SAFETY: `peaks` is a live 8-entry buffer; capacity matches.
|
||||
let n = unsafe { oakengine_audio_output_levels(peaks.as_mut_ptr(), peaks.len() as c_int) };
|
||||
if n <= 0 {
|
||||
return vec![0.0, 0.0];
|
||||
}
|
||||
peaks[..n as usize].iter().map(|p| p.clamp(0.0, 1.0)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1480,15 +1633,14 @@ impl AppEngine for RealEngine {
|
||||
return image.clone();
|
||||
}
|
||||
}
|
||||
// The program monitor renders the current sequence through the
|
||||
// facade CPU renderer (falling back to the synthetic pattern when
|
||||
// rendering is unavailable). The source monitor keeps the synthetic
|
||||
// pattern: the facade renderer binds a *sequence* handle only, so
|
||||
// there is currently no surface to render a single footage node for
|
||||
// the material viewer — that is a documented facade gap.
|
||||
// Both monitors render through the facade CPU renderer (falling
|
||||
// back to the synthetic pattern when rendering is unavailable): the
|
||||
// program monitor renders the current sequence, the source monitor
|
||||
// renders the currently selected footage node at the source clock's
|
||||
// playhead frame.
|
||||
let rendered = match monitor {
|
||||
Monitor::Program => self.render_program_frame(frame),
|
||||
Monitor::Source => None,
|
||||
Monitor::Source => self.render_source_frame(frame),
|
||||
};
|
||||
let (image, scope) = match rendered {
|
||||
Some((image, scope)) => (Arc::new(image), scope),
|
||||
@@ -1562,7 +1714,15 @@ impl AppEngine for RealEngine {
|
||||
}
|
||||
|
||||
fn select_item(&mut self, id: u64, cx: &mut Context<Self>) {
|
||||
let changed = self.selected_item != Some(id);
|
||||
self.selected_item = Some(id);
|
||||
if changed {
|
||||
// The source monitor renders the selected footage node: a new
|
||||
// selection must rebind the renderer and drop the stale cached
|
||||
// frame (the cache key only tracks the playhead frame).
|
||||
*self.source_renderer.lock().unwrap() = RendererSlot::Untried;
|
||||
self.cpu_frame_cache.lock().unwrap().remove(&Monitor::Source);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user