Files
oak-editor/engine/include/oakengine/preview.h
T
Mike-Solar a59c33715f fix: node graph edge display, teardown crashes, and event/audio lifetime bugs
- capi: oakengine_node_output_connection_at/_at_ex returned the source
  node as the connection destination; the actual destination is
  conn.second.node(). Out-edge enumeration was useless, so the node
  view could only draw in-edges and randomly lost whichever edges
  needed the out-edge path (random per context build order).
  Regression test in oakengine_node_test
- project teardown: Project::clear() pre-notifies node removal while
  nodes are fully constructed (observers used to crash on
  half-destroyed nodes); childEvent suppresses the removal dance while
  clearing; Node::disconnect_all/disconnect_edge get a silent mode for
  teardown so no invalidation/events touch dying members
  (is_being_cleared); ClipBlock marker disconnect guarded against
  dead viewer/markers; ProjectCopier and PreviewAutoCacher drop
  project references on Project::destroyed instead of disconnecting
  dead objects at shutdown
- preview: add oakengine_preview_request_get_audio_sample_count; the
  viewer queried sample count by passing nullptr to get_audio_samples
  which rejects it, so all playback audio was silently dropped
- app: fix unterminated input-id memcpy in ResolveGroupInput
  (nodeparamviewitem, widgetbridge) that corrupted every parameter id
- app: unsubscribe raw C-API event subscriptions in destructors of
  NodeParamViewKeyframeControl, NodeParamViewConnectedLabel and
  ExportDialog; playhead events used to fire into dead widgets
  (crash when dragging the playhead)
- tests: preview request roundtrip (video frame + audio range) and
  free-while-active teardown coverage; env-gated OAK_DEBUG_EDGES /
  OAK_DEBUG_INVALID_INPUT diagnostics
- docs: investigation notes in docs/zh/
2026-08-02 14:29:49 +08:00

252 lines
9.7 KiB
C

/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAKENGINE_PREVIEW_H
#define OAKENGINE_PREVIEW_H
#include <stdint.h>
#include "export.h"
#include "footage.h"
#include "init.h"
#include "timeline.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @file preview.h
* @brief C ABI for preview state and readouts (loop mode, audio levels,
* waveform summary)
*
* This family is deliberately NOT realtime playback: the application's
* realtime transport is deeply coupled to its viewer widgets (viewer
* playback timer, audio queue, PortAudio manager). What a headless
* consumer needs is renderable through the renderer family; this family
* adds the remaining preview state and readouts.
*
* Loop mode note: the engine has no sequence-level loop state. LoopMode
* (engine/render/loopmode.h: off / loop / clamp) is a property of a CLIP
* (ClipBlock::k_loop_mode_input), which is what the application edits.
* The loop functions here therefore operate on clip handles, persisted
* through the node-graph and undoable like other parameter writes.
*
* Audio readouts are computed by rendering the exact range through
* RenderManager::render_audio() and reducing the samples in-process --
* the exact semantics are documented per function. Rendering audio may
* conform sources on first use; the wait is driven with an event loop
* like the export family (up to 120 s). Everything here works without
* GL. Errors follow the family model (oakengine_preview_last_error()).
*/
/** @brief Loop modes, mirroring olive::LoopMode. */
#define OAKENGINE_LOOP_MODE_OFF 0 /**< Play once (olive k_loop_mode_off). */
#define OAKENGINE_LOOP_MODE_LOOP 1 /**< Repeat the clip (olive k_loop_mode_loop). */
#define OAKENGINE_LOOP_MODE_CLAMP 2 /**< Hold first/last frame (olive k_loop_mode_clamp). */
/**
* @brief Opaque preview request handle (an active render ticket for
* single-frame or audio-range preview).
*/
typedef struct OakEnginePreviewRequest OakEnginePreviewRequest;
/**
* @brief POD for a single video frame from a preview request
* (borrowed data, valid until the request is freed).
*/
typedef struct oak_playback_frame {
int width;
int height;
int format; /**< olive::PixelFormat::Format value. */
const void *data; /**< Planar data pointer (first plane). */
int linesize; /**< Bytes per row of the first plane. */
int64_t timestamp_num; /**< Frame timestamp (seconds) as a rational. */
int64_t timestamp_den; /**< 0 when the frame carries no timestamp. */
} oak_playback_frame;
/**
* @brief Human-readable reason for the last failed preview call on this
* thread (buf/size convention).
*/
OAKENGINE_API int oakengine_preview_last_error(char *buf, int buf_size);
/**
* @brief The clip's loop mode (OAKENGINE_LOOP_MODE_*;
* ClipBlock::loop_mode()). Returns the mode or OAKENGINE_E_INVALID.
*/
OAKENGINE_API int oakengine_clip_get_loop_mode(const OakEngineClip *self);
/**
* @brief Set the clip's loop mode (undoable parameter write, same command
* path as the node family). OAKENGINE_E_INVALID for an unknown mode.
*/
OAKENGINE_API int oakengine_clip_set_loop_mode(OakEngineClip *self,
int mode);
/**
* @brief Per-channel audio level of a sequence at `time_ts`.
*
* Exact semantics: the sequence's audio is rendered over ONE frame
* starting at `time_ts` (a frame timestamp in the sequence's frame-rate
* timebase, like the rest of the family) and the linear RMS
* (root-mean-square) of every channel is written to `values`, in the
* [0, 1] range for normalized audio. Ranges with no audio content yield
* exact zeros (the engine returns no allocated samples there).
* `channel_count` is the capacity of `values`; up to that many of the
* sequence's channels are written, the return value is the number of
* channels written, and a negative OAKENGINE_E_* code signals an error
* (e.g. no RENDER-less engine state issue -- audio renders without GL,
* but the engine must be initialized with OAKENGINE_INIT_RENDER because
* rendering goes through RenderManager).
*/
OAKENGINE_API int oakengine_preview_get_audio_levels(
OakEngineSequence *seq, int64_t time_ts, double *values,
int channel_count);
/**
* @brief Waveform min/max summary of a footage's audio over a range.
*
* The footage's audio is rendered from `start_ts` to `end_ts` (frame
* timestamps in the timebase of the project's first sequence's frame
* rate, same convention as the keyframe family) and each of the `count`
* equal-sized buckets covering the range yields the minimum and maximum
* sample value of `channel` in `min_vals`/`max_vals` (linear, unclamped
* source samples; silent or content-free buckets are exact zeros). The
* footage handle must be a borrowed import handle (probe handles are
* rejected with OAKENGINE_E_INVALID); `channel` must be within the
* footage's channel count and `count` must be > 0.
*/
OAKENGINE_API int oakengine_preview_get_waveform_summary(
OakEngineFootage *footage, int channel, int64_t start_ts,
int64_t end_ts, double *min_vals, double *max_vals, int count);
/* ---- R4: waveform, audio levels, cacher, preview requests ------------------ */
/** @brief Maximum sample rate for waveform generation. > 0. */
OAKENGINE_API int oakengine_waveform_max_sample_rate(void);
/**
* @brief Analyze audio levels (linear RMS) from raw float sample data.
* `data` is an array of `channels` float pointers, each with `count` samples.
* Writes RMS values into `levels` (one per channel). Returns OAKENGINE_OK
* or OAKENGINE_E_INVALID on NULL/bad arguments.
*/
OAKENGINE_API int oakengine_audio_analyze_levels(const float *const *data,
int channels, int64_t count,
double *levels);
/**
* @brief Set the preview cacher's playhead position (num/den seconds).
* Returns OAKENGINE_E_STATE when the cacher is not available.
*/
OAKENGINE_API int oakengine_preview_cacher_set_playhead(int64_t num,
int64_t den);
/**
* @brief Pause or resume thumbnail generation in the cacher.
* Returns OAKENGINE_E_STATE when the cacher is not available.
*/
OAKENGINE_API int oakengine_preview_cacher_set_thumbnails_paused(int paused);
/**
* @brief Clear pending single-frame render requests from the cacher.
* Returns OAKENGINE_E_STATE when the cacher is not available.
*/
OAKENGINE_API int
oakengine_preview_cacher_clear_single_frame_renders(int only_finished);
/**
* @brief Force the cacher to cache a range (num/den seconds in/out).
* Returns OAKENGINE_E_INVALID on NULL node.
*/
OAKENGINE_API int oakengine_preview_cacher_force_cache_range(
OakEngineNode *node, int64_t in_num, int64_t in_den, int64_t out_num,
int64_t out_den);
/**
* @brief Request a single video frame at (num/den) seconds from `viewer`.
* Returns a request handle (caller owns it, must free) or NULL on failure.
*/
OAKENGINE_API OakEnginePreviewRequest *
oakengine_preview_request_single_frame(OakEngineNode *viewer, int64_t num,
int64_t den, int dry);
/**
* @brief Request an audio range (num/den seconds in/out) from `viewer`.
* Returns a request handle (caller owns it, must free) or NULL on failure.
*/
OAKENGINE_API OakEnginePreviewRequest *
oakengine_preview_request_audio_range(OakEngineNode *viewer, int64_t in_num,
int64_t in_den, int64_t out_num,
int64_t out_den);
/** @brief 1 if the request is done, 0 otherwise. 0 on NULL. */
OAKENGINE_API int oakengine_preview_request_is_done(
const OakEnginePreviewRequest *req);
/** @brief 1 if the request has a result, 0 otherwise. 0 on NULL. */
OAKENGINE_API int oakengine_preview_request_has_result(
const OakEnginePreviewRequest *req);
/** @brief Set a finished callback (called when the ticket completes).
* `callback` receives `user_data`. Returns OAKENGINE_E_INVALID on NULL
* request. */
OAKENGINE_API int oakengine_preview_request_set_finished_callback(
OakEnginePreviewRequest *req, void (*callback)(void *),
void *user_data);
/** @brief Copy the frame data into `out`. Returns OAKENGINE_OK or
* OAKENGINE_E_INVALID when the request has no video frame result. */
OAKENGINE_API int oakengine_preview_request_get_frame(
OakEnginePreviewRequest *req, oak_playback_frame *out);
/** @brief Number of audio channels in the result, or 0 if none. */
OAKENGINE_API int oakengine_preview_request_get_audio_channel_count(
const OakEnginePreviewRequest *req);
/** @brief Samples per channel in the result, or 0 if none. */
OAKENGINE_API int oakengine_preview_request_get_audio_sample_count(
const OakEnginePreviewRequest *req);
/** @brief Sample rate of the audio result, or 0 if none. */
OAKENGINE_API int oakengine_preview_request_get_audio_sample_rate(
const OakEnginePreviewRequest *req);
/**
* @brief Get audio sample data from the result.
* `channel` is the 0-based channel index. Writes up to `max_samples` float
* values into `samples`. Returns the number of samples written, or
* OAKENGINE_E_INVALID on bad arguments.
*/
OAKENGINE_API int oakengine_preview_request_get_audio_samples(
OakEnginePreviewRequest *req, int channel, const float *samples,
int max_samples);
/** @brief Free a preview request handle (NULL-safe). */
OAKENGINE_API void oakengine_preview_request_free(
OakEnginePreviewRequest *req);
#ifdef __cplusplus
}
#endif
#endif /* OAKENGINE_PREVIEW_H */