engine: add the preview family to the C ABI facade

- clip-level loop mode (off/loop/clamp), undoable
- per-channel audio levels as one-frame RMS at a timestamp
- waveform min/max buckets over a footage range, rendered on demand
- two real fixes uncovered by this family: conform completion signals
  were starved by msleep-only waits (audio renders always came back
  empty), and incomplete tickets from conform-pending renders are now
  retried until the conform is ready
This commit is contained in:
2026-07-20 09:28:47 +08:00
parent 882ce8268c
commit bb17c59c0f
6 changed files with 810 additions and 16 deletions
+40 -16
View File
@@ -25,6 +25,7 @@
#include <cstring>
#include <QByteArray>
#include <QCoreApplication>
#include <QElapsedTimer>
#include <QMutex>
#include <QString>
@@ -135,6 +136,10 @@ bool wait_for_ticket(OakEngineRendererState *state,
timer.start();
while (!finished.load() && !ticket->is_cancelled() &&
!timer.hasExpired(k_render_timeout_ms)) {
// Pump events, not just sleep: audio conforms signal their
// completion through this thread's event queue, and a bare sleep
// loop would starve them (the export family waits the same way).
QCoreApplication::processEvents(QEventLoop::AllEvents, 20);
QThread::msleep(5);
}
@@ -330,24 +335,43 @@ OakEngineAudioBuffer *oakengine_renderer_render_audio(
state->sequence, olive::TimeRange(in_time, in_time + length_time),
state->audio_params, state->mode);
olive::RenderTicketPtr ticket =
olive::RenderManager::instance()->render_audio(params);
{
QMutexLocker locker(&state->ticket_mutex);
state->in_flight = ticket;
// A ticket can finish "successfully" with an empty buffer when the
// audio conform was still generating (RenderProcessor marks it
// "incomplete" instead of blocking): resubmit until the conform is
// ready, up to the overall render timeout.
olive::SampleBuffer samples;
QElapsedTimer retry_timer;
retry_timer.start();
while (true) {
olive::RenderTicketPtr ticket =
olive::RenderManager::instance()->render_audio(params);
{
QMutexLocker locker(&state->ticket_mutex);
state->in_flight = ticket;
}
if (!wait_for_ticket(state, ticket) || !ticket->has_result()) {
set_error(state,
ticket->is_cancelled() ?
QStringLiteral("render cancelled") :
QStringLiteral(
"audio render produced nothing (timeout or failure)"));
return nullptr;
}
if (!ticket->property("incomplete").toBool()) {
samples = ticket->get().value<olive::SampleBuffer>();
break;
}
if (retry_timer.hasExpired(k_render_timeout_ms)) {
set_error(state, QStringLiteral(
"audio conform did not finish in time"));
return nullptr;
}
// Conform still generating; let it finish and try again.
QThread::msleep(50);
}
if (!wait_for_ticket(state, ticket) || !ticket->has_result()) {
set_error(state,
ticket->is_cancelled() ?
QStringLiteral("render cancelled") :
QStringLiteral(
"audio render produced nothing (timeout or failure)"));
return nullptr;
}
olive::SampleBuffer samples =
ticket->get().value<olive::SampleBuffer>();
if (!samples.is_allocated()) {
set_error(state, QStringLiteral("audio render result was empty"));
return nullptr;