diff --git a/include/xtc/xtc.h b/include/xtc/xtc.h index f329716..dcfcb96 100644 --- a/include/xtc/xtc.h +++ b/include/xtc/xtc.h @@ -11,9 +11,20 @@ extern "C" { typedef struct xtc_app xtc_app; /* Creates an application context. On success, *out_app is caller-owned. */ +/** + * @brief Allocates an application handle. + * + * @param out_app Receives the newly allocated app; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_app_create(xtc_app **out_app); /* Destroys an application context; NULL is accepted. */ +/** + * @brief Releases an application handle; accepts NULL. + * + * @param app Application handle. + */ void xtc_app_destroy(xtc_app *app); #ifdef __cplusplus diff --git a/include/xtc/xtc_audio.h b/include/xtc/xtc_audio.h index 6d65285..0337292 100644 --- a/include/xtc/xtc_audio.h +++ b/include/xtc/xtc_audio.h @@ -14,21 +14,78 @@ typedef struct xtc_recorder xtc_recorder; * mono, 1..768000 Hz. Absolute canonical references, <=511 bytes. * Adds a timeline source -> output route if no timeline source exists. * Transactional; track must exist. Duration rounds up to a musical tick. */ +/** + * @brief Imports a WAV reference as a clip and ensures a timeline output route exists. + * + * @param project Project model; borrowed for this call. + * @param track Stable track identifier. + * @param path NUL-terminated file or asset path. + * @param start_tick Start position in musical ticks. + * @param out_clip Receives the clip. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_import_audio(xtc_project *project, uint64_t track, const char *path, uint64_t start_tick, uint64_t *out_clip); /* Empty path makes a region silent; nonempty path must be absolute. Does not * open the file. Missing references survive save/load for later relinking. */ +/** + * @brief Sets an absolute audio asset reference, or clears it to make a region silent. + * + * @param project Project model; borrowed for this call. + * @param clip Stable clip identifier. + * @param path NUL-terminated file or asset path. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_set_clip_asset(xtc_project *project, uint64_t clip, const char *path); /* Immutable/preloaded plan. At most 64 distinct assets, 16M mono frames/asset, * 64M total frames, 256 MiB file and 4096 chunk input limits. Missing/invalid assets become silence with explicit * issues. OOM/capacity fails creation. No file access during render. */ +/** + * @brief Preloads audio assets and compiles an immutable arrangement snapshot for rendering. + * + * @param project Project model; borrowed for this call. + * @param rate Sample rate in hertz. + * @param out_plan Receives the newly allocated plan; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_audio_plan_create(const xtc_project *project, unsigned int rate, xtc_audio_plan **out_plan); +/** + * @brief Releases preloaded samples and arrangement state; accepts NULL. + * + * @param plan Preloaded arrangement plan. + */ void xtc_audio_plan_destroy(xtc_audio_plan *plan); +/** + * @brief Returns the number of asset issues and optionally the first affected path. + * + * @param plan Preloaded arrangement plan. + * @param out_first_path Receives the first path. + * @return Number of unique asset issues; zero for a NULL plan. + */ size_t xtc_audio_plan_issues(const xtc_audio_plan *plan, const char **out_first_path); /* Single consumer; output replaced with overlap-summed mono at unity gain. * Linear rate conversion, no time stretching. Output must have frames floats. */ /* Stereo outputs must be distinct; NULL right selects mono gain-only rendering. */ -xtc_result xtc_audio_plan_render_stereo(const xtc_audio_plan *, uint64_t, float *left, float *right, size_t); +/** + * @brief Replaces output with the overlap-summed timeline mix, applying track gain and pan. + * + * @param plan Preloaded arrangement plan. + * @param position Absolute sample position at the start of the block. + * @param left Destination left-channel buffer. + * @param right Destination right-channel buffer; NULL selects mono rendering. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_audio_plan_render_stereo(const xtc_audio_plan * plan, uint64_t position, float *left, float *right, size_t frames); +/** + * @brief Renders the timeline to a mono output buffer using track gain without pan. + * + * @param plan Preloaded arrangement plan. + * @param position Absolute sample position at the start of the block. + * @param output Destination buffer with room for the requested frames. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_audio_plan_render(const xtc_audio_plan *plan, uint64_t position, float *output, size_t frames); /* Control-thread creation opens a same-directory temporary float32 WAV. * Ring capacity 1..2097152 frames. One callback producer, one control consumer. @@ -37,11 +94,51 @@ xtc_result xtc_audio_plan_render(const xtc_audio_plan *plan, uint64_t position, * drains, finalizes and atomically renames even on overflow (partial recovery). * No source/recorder is freed until destroy, which also removes unfinished temp. * Counts and sticky error can be observed while producing. */ +/** + * @brief Allocates a bounded recording ring and opens a temporary mono float32 WAV file. + * + * @param path NUL-terminated file or asset path. + * @param rate Sample rate in hertz. + * @param capacity Recording ring capacity in frames, from 1 to 2097152. + * @param out_recorder Receives the newly allocated recorder; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_recorder_create(const char *path, unsigned int rate, size_t capacity, xtc_recorder **out_recorder); +/** + * @brief Enqueues an entire audio block without blocking; overflow sets a sticky error. + * + * @param recorder Recorder handle. + * @param input Borrowed input sample buffer. + * @param frames Number of audio frames. + */ void xtc_recorder_push(xtc_recorder *recorder, const float *input, size_t frames); +/** + * @brief Writes queued samples to the temporary WAV file on the control thread. + * + * @param recorder Recorder handle. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_recorder_drain(xtc_recorder *recorder); +/** + * @brief Drains and finalizes the WAV file, then renames it after producer quiescence. + * + * @param recorder Recorder handle. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_recorder_finish(xtc_recorder *recorder); +/** + * @brief Copies the number of accepted frames and returns the sticky recording status. + * + * @param recorder Recorder handle. + * @param out_frames Receives the frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_recorder_status(const xtc_recorder *recorder, uint64_t *out_frames); +/** + * @brief Releases recorder storage and removes any unfinished temporary file; accepts NULL. + * + * @param recorder Recorder handle. + */ void xtc_recorder_destroy(xtc_recorder *recorder); #ifdef __cplusplus diff --git a/include/xtc/xtc_backend.h b/include/xtc/xtc_backend.h index c68495f..ce8e715 100644 --- a/include/xtc/xtc_backend.h +++ b/include/xtc/xtc_backend.h @@ -44,46 +44,178 @@ typedef struct { * Returns IO if server/client setup fails. On failure *out_backend is NULL, * except if cleanup itself fails: a non-NULL handle must be closed/retried; * only close is permitted on that partially initialized handle. */ +/** + * @brief Opens a JACK client and registers its ports without starting the server or connecting routes. + * + * @param config Backend kind, client name, block capacity, and output channel configuration. + * @param out_backend Receives the newly allocated backend; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_backend); /* Stop/close establish callback quiescence before state can be destroyed. * On failure the handle remains owned by caller; retry close. NULL close OK. */ +/** + * @brief Quiesces callbacks and releases the backend; failed cleanup leaves the handle owned by the caller. + * + * @param backend Backend handle. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_close(xtc_backend *backend); +/** + * @brief Activates the backend and enables audio processing. + * + * @param backend Backend handle. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_start(xtc_backend *backend); +/** + * @brief Deactivates the backend and establishes callback quiescence. + * + * @param backend Backend handle. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_stop(xtc_backend *backend); /* Stopped only; changes transport clock, not oscillator phase. */ +/** + * @brief Changes the stopped backend's sample position and resets MIDI voices. + * + * @param backend Backend handle. + * @param sample Absolute sample position. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_seek(xtc_backend *backend, uint64_t sample); /* Copies graph. While running, capacity means pending slot busy. * While stopped, transactionally replaces all retained plans (counters/revisions * restart); failed compilation leaves the previous engine intact. */ +/** + * @brief Compiles and submits a graph while preserving the previous engine on failure. + * + * @param backend Backend handle. + * @param graph Graph model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph); /* Stopped only. Preloads immutable audio and submits graph before committing. * Asset issues are silent and reported in status/asset_issue. Other failures * retain the previous plans. Arrangement edits are adopted on the next prepare. */ +/** + * @brief Preloads project audio and MIDI and submits its graph while stopped. + * + * @param backend Backend handle. + * @param project Project model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *project); +/** + * @brief Returns the first asset issue path, or an empty string when none is available. + * + * @param backend Backend handle. + * @return Borrowed path string, or an empty string if no issue is available. + */ const char *xtc_backend_asset_issue(const xtc_backend *backend); /* Borrowed recorder, set/detach only while stopped. Stop before finish/destroy. */ /* Control-thread panic is delivered at a block boundary via lock-free counter. * Stops, seeks, xruns and event overflow also clear instrument voices. */ +/** + * @brief Requests an instrument panic at the next audio block boundary. + * + * @param backend Backend handle. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_panic(xtc_backend *backend); +/** + * @brief Returns the borrowed name of the backend's MIDI input port. + * + * @param backend Backend handle. + * @param out_name Receives the name. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_midi_port_name(const xtc_backend *backend, const char **out_name); +/** + * @brief Attaches or detaches a borrowed recorder while the backend is stopped. + * + * @param backend Backend handle. + * @param recorder Recorder handle. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_set_recorder(xtc_backend *backend, xtc_recorder *recorder); +/** + * @brief Reclaims retired realtime plans on the control thread. + * + * @param backend Backend handle. + */ void xtc_backend_collect(xtc_backend *backend); /* Snapshot fields are individually coherent, not a transactional snapshot. */ +/** + * @brief Copies individually coherent backend counters and transport state. + * + * @param backend Backend handle. + * @param out_status Receives the status. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *out_status); /* Borrowed names valid until close. Direction is the client's own port. */ /* Output channel 0 is legacy output, channel 1 is output_right in stereo. */ -xtc_result xtc_backend_output_name(const xtc_backend *, size_t channel, const char **out_name); +/** + * @brief Returns the borrowed name of an output channel port. + * + * @param backend Backend handle. + * @param channel Output channel index: 0 for primary, 1 for stereo right. + * @param out_name Receives the name. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_backend_output_name(const xtc_backend * backend, size_t channel, const char **out_name); +/** + * @brief Returns the borrowed name of the mono input or primary output port. + * + * @param backend Backend handle. + * @param direction Local port direction. + * @param out_name Receives the name. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction direction, const char **out_name); /* Enumerates available mono audio ports (devices and software routes). Visitor * receives a borrowed name valid only during call; return nonzero to stop. * Control-thread only. JACK NULL list means no matching ports. */ +/** + * @brief Inspects one available audio port during control-thread enumeration. + * + * @param name Borrowed port name, valid only during this callback. + * @param context Opaque pointer supplied to xtc_backend_visit_ports(). + * @return Nonzero to stop enumeration, zero to continue. + */ typedef int (*xtc_backend_port_visitor)(const char *name, void *context); +/** + * @brief Visits available audio ports matching the requested direction until the visitor stops. + * + * @param backend Backend handle. + * @param direction Local port direction. + * @param visitor Visitor called with each borrowed port name. + * @param context Opaque callback context. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction direction, xtc_backend_port_visitor visitor, void *context); /* Connect/disconnect this client's port with a remote opposite-direction port. * own_direction INPUT receives remote output; OUTPUT sends to remote input. * Explicit user choice only; no automatic physical routing. */ +/** + * @brief Connects this client's port to a remote port with the opposite direction. + * + * @param backend Backend handle. + * @param own_direction Local port direction. + * @param remote_name Borrowed remote port name. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_connect(xtc_backend *backend, xtc_port_direction own_direction, const char *remote_name); +/** + * @brief Disconnects this client's port from a remote port. + * + * @param backend Backend handle. + * @param own_direction Local port direction. + * @param remote_name Borrowed remote port name. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction own_direction, const char *remote_name); #ifdef __cplusplus diff --git a/include/xtc/xtc_engine.h b/include/xtc/xtc_engine.h index 2266889..2467aee 100644 --- a/include/xtc/xtc_engine.h +++ b/include/xtc/xtc_engine.h @@ -81,35 +81,172 @@ typedef struct { /* Read-only snapshots; indexes enumerate dense lists, distinct from stable IDs. * No borrowed pointers into graph storage are exposed. */ +/** + * @brief Copies the graph's node and connection counts. + * + * @param graph Graph model; borrowed for this call. + * @param out_nodes Receives the nodes. + * @param out_connections Receives the connections. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_get_counts(const xtc_graph *graph, size_t *out_nodes, size_t *out_connections); +/** + * @brief Copies node metadata selected by dense list index. + * + * @param graph Graph model; borrowed for this call. + * @param index Zero-based index in the dense list. + * @param out_node Receives the node. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_get_node(const xtc_graph *graph, size_t index, xtc_node_info *out_node); +/** + * @brief Copies connection metadata selected by dense list index. + * + * @param graph Graph model; borrowed for this call. + * @param index Zero-based index in the dense list. + * @param out_connection Receives the connection. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_get_connection(const xtc_graph *graph, size_t index, xtc_connection_info *out_connection); /* Set required=1 for explicit output nodes only (none means silence). * Default 0 retains terminal-node fallback for original offline callers. */ +/** + * @brief Sets whether rendering requires explicit output nodes rather than terminal-node fallback. + * + * @param graph Graph model; borrowed for this call. + * @param required One to require explicit output nodes, zero to allow terminal-node fallback. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_require_output_nodes(xtc_graph *graph, int required); /* Transactional command: on success caller owns a new graph; source unchanged. * Failure sets *out_graph=NULL, leaves source and out_added_id unchanged. * out_added_id is optional and written only for successful ADD_NODE. */ +/** + * @brief Applies an edit to a new graph copy, leaving the source unchanged. + * + * @param source Graph model; borrowed for this call. + * @param edit Graph edit command. + * @param out_graph Receives the newly allocated graph; caller owns it on success. + * @param out_added_id Receives the added id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_apply_edit(const xtc_graph *source, const xtc_graph_edit *edit, xtc_graph **out_graph, size_t *out_added_id); +/** + * @brief Allocates an empty graph with the requested node capacity. + * + * @param max_nodes Maximum number of graph nodes. + * @param out_graph Receives the newly allocated graph; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_create(size_t max_nodes, xtc_graph **out_graph); +/** + * @brief Releases graph storage; accepts NULL. + * + * @param graph Graph model; borrowed for this call. + */ void xtc_graph_destroy(xtc_graph *graph); +/** + * @brief Adds a gain node with a finite linear gain value. + * + * @param graph Graph model; borrowed for this call. + * @param gain Linear gain, 0 to 4 for track controls. + * @param out_node_id Receives the node id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_add_gain(xtc_graph *graph, float gain, size_t *out_node_id); +/** + * @brief Connects a source's output port 1 to a destination's input port 0. + * + * @param graph Graph model; borrowed for this call. + * @param source_node Stable source node identifier. + * @param destination_node Stable destination node identifier. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_connect(xtc_graph *graph, size_t source_node, size_t destination_node); /* Port 0 is input; port 1 is output. Oscillators, audio sources and instruments have only output 1; * output nodes have only input 0. All ports carry mono audio. */ +/** + * @brief Copies the direction, signal type, and channel count of a node port. + * + * @param graph Graph model; borrowed for this call. + * @param node_id Stable node identifier. + * @param port_id Node port identifier. + * @param out_port Receives the port. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_get_port(const xtc_graph *graph, size_t node_id, size_t port_id, xtc_port_info *out_port); +/** + * @brief Adds a validated audio connection while preserving an acyclic graph. + * + * @param graph Graph model; borrowed for this call. + * @param source_node Stable source node identifier. + * @param source_port Source output port identifier. + * @param destination_node Stable destination node identifier. + * @param destination_port Destination input port identifier. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_connect_ports(xtc_graph *graph, size_t source_node, size_t source_port, size_t destination_node, size_t destination_port); +/** + * @brief Adds a sine oscillator node with a positive frequency in hertz. + * + * @param graph Graph model; borrowed for this call. + * @param frequency Oscillator frequency in hertz. + * @param out_node_id Receives the node id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_add_oscillator(xtc_graph *graph, float frequency, size_t *out_node_id); +/** + * @brief Adds a pass-through node that records the last block's absolute peak. + * + * @param graph Graph model; borrowed for this call. + * @param out_node_id Receives the node id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_add_meter(xtc_graph *graph, size_t *out_node_id); +/** + * @brief Adds a source node for the separately rendered MIDI instrument buffer. + * + * @param graph Graph model; borrowed for this call. + * @param out_node_id Receives the node id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_add_instrument(xtc_graph *graph, size_t *out_node_id); +/** + * @brief Adds a source node for the separately rendered timeline audio buffer. + * + * @param graph Graph model; borrowed for this call. + * @param out_node_id Receives the node id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_add_audio(xtc_graph *graph, size_t *out_node_id); +/** + * @brief Adds an explicit output node included in the final mix. + * + * @param graph Graph model; borrowed for this call. + * @param out_node_id Receives the node id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_add_output(xtc_graph *graph, size_t *out_node_id); /* Compile copies graph state; caller owns the plan. Sample rate must be * finite and positive, oscillator frequencies <= Nyquist. max_frames: 1..65536. * Editing/destroying the graph never changes an existing plan. */ +/** + * @brief Allocates an independent execution plan with preallocated processing buffers. + * + * @param graph Graph model; borrowed for this call. + * @param sample_rate Sample rate in hertz. + * @param max_frames Maximum frames per processing block. + * @param out_plan Receives the newly allocated plan; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate, size_t max_frames, xtc_plan **out_plan); +/** + * @brief Releases a compiled plan and its processing state; accepts NULL. + * + * @param plan Compiled graph plan and its single-owner runtime state. + */ void xtc_plan_destroy(xtc_plan *plan); /* Single processing owner, no concurrent calls (including meter reads/reset). * No allocation, I/O, locking or graph validation. NULL input means silence. @@ -122,22 +259,96 @@ void xtc_plan_destroy(xtc_plan *plan); /* Stereo graph with paired source buffers and a mono external input duplicated * to both sides. Output buffers must be distinct and must not alias sources. * Stereo oscillator phase advances once per frame. NULL right selects mono. */ -xtc_result xtc_plan_process_stereo(xtc_plan *, const float *input, const float *audio_left, const float *audio_right, +/** + * @brief Processes a block with separate stereo timeline and instrument sources. + * + * @param plan Compiled graph plan and its single-owner runtime state. + * @param input Borrowed input sample buffer. + * @param audio_left Borrowed left timeline source buffer; NULL means silence. + * @param audio_right Borrowed right timeline source buffer; NULL means silence. + * @param midi_left Borrowed left instrument source buffer; NULL means silence. + * @param midi_right Borrowed right instrument source buffer; NULL means silence. + * @param left Destination left-channel buffer. + * @param right Destination right-channel buffer; NULL selects mono rendering. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_plan_process_stereo(xtc_plan * plan, const float *input, const float *audio_left, const float *audio_right, const float *midi_left, const float *midi_right, float *left, float *right, size_t frames); +/** + * @brief Processes a mono block with separate timeline and instrument sources. + * + * @param plan Compiled graph plan and its single-owner runtime state. + * @param input Borrowed input sample buffer. + * @param timeline Borrowed timeline source buffer; NULL means silence. + * @param instrument Borrowed instrument source buffer; NULL means silence. + * @param output Destination buffer with room for the requested frames. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_plan_process_sources(xtc_plan *plan, const float *input, const float *timeline, const float *instrument, float *output, size_t frames); /* timeline is a separate mono input for XTC_NODE_AUDIO, NULL means silence. */ +/** + * @brief Processes a mono block with timeline audio and no instrument source. + * + * @param plan Compiled graph plan and its single-owner runtime state. + * @param input Borrowed input sample buffer. + * @param timeline Borrowed timeline source buffer; NULL means silence. + * @param output Destination buffer with room for the requested frames. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_plan_process_audio(xtc_plan *plan, const float *input, const float *timeline, float *output, size_t frames); +/** + * @brief Processes a mono block with external input and no timeline or instrument sources. + * + * @param plan Compiled graph plan and its single-owner runtime state. + * @param input Borrowed input sample buffer. + * @param output Destination buffer with room for the requested frames. + * @param frame_count Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_plan_process(xtc_plan *plan, const float *input, float *output, size_t frame_count); /* Resets oscillator phases and meters, without allocation. */ +/** + * @brief Resets oscillator phases, meters, and native processor state without allocating. + * + * @param plan Compiled graph plan and its single-owner runtime state. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_plan_reset(xtc_plan *plan); /* Last block absolute peak, valid for meter nodes only. */ +/** + * @brief Copies the last block's absolute peak for a meter node. + * + * @param plan Compiled graph plan and its single-owner runtime state. + * @param node_id Stable node identifier. + * @param out_peak Receives the peak. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_plan_meter_peak(const xtc_plan *plan, size_t node_id, float *out_peak); +/** + * @brief Checks graph validity by attempting a topological ordering. + * + * @param graph Graph model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_validate(const xtc_graph *graph); /* Allocating compatibility helper: compiles at 48000 Hz and processes once. * Requires non-NULL input/output and finite input_gain. Not for callbacks. */ +/** + * @brief Compiles at 48000 Hz, applies input gain, renders once, and releases temporary state. + * + * @param graph Graph model; borrowed for this call. + * @param input Borrowed input sample buffer. + * @param output Destination buffer with room for the requested frames. + * @param frame_count Number of audio frames. + * @param input_gain Finite linear gain applied to external input before processing. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_process_offline(xtc_graph *graph, const float *input, float *output, size_t frame_count, float input_gain); diff --git a/include/xtc/xtc_midi.h b/include/xtc/xtc_midi.h index eac5eab..0306320 100644 --- a/include/xtc/xtc_midi.h +++ b/include/xtc/xtc_midi.h @@ -32,36 +32,149 @@ typedef struct xtc_midi_plan xtc_midi_plan; /* Control-thread model APIs, stable note IDs; deletion cascades from clips/tracks. * MIDI clips cannot reference audio. Creation transactionally adds an instrument * source -> output route if no instrument exists. Other clip edits stay shared. */ +/** + * @brief Adds a MIDI clip and ensures an instrument output route exists. + * + * @param project Project model; borrowed for this call. + * @param track Stable track identifier. + * @param start Start position in musical ticks. + * @param length Duration in musical ticks. + * @param out_id Receives the id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_add_midi_clip(xtc_project *project, uint64_t track, uint64_t start, uint64_t length, uint64_t *out_id); +/** + * @brief Copies the number of notes in the project. + * + * @param project Project model; borrowed for this call. + * @param out_count Receives the count. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_note_count(const xtc_project *project, size_t *out_count); +/** + * @brief Copies a note snapshot selected by dense list index. + * + * @param project Project model; borrowed for this call. + * @param index Zero-based index in the dense list. + * @param out_note Receives the note. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_get_note(const xtc_project *project, size_t index, xtc_note_info *out_note); +/** + * @brief Adds a validated MIDI note with a new stable identifier. + * + * @param project Project model; borrowed for this call. + * @param clip Stable clip identifier. + * @param start Start position in musical ticks. + * @param length Duration in musical ticks. + * @param pitch MIDI note number, 0 to 127. + * @param velocity MIDI note velocity, 1 to 127. + * @param channel MIDI channel, 0 to 15. + * @param out_id Receives the id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_add_note(xtc_project *project, uint64_t clip, uint64_t start, uint64_t length, unsigned int pitch, unsigned int velocity, unsigned int channel, uint64_t *out_id); +/** + * @brief Replaces a note's source range, pitch, velocity, and channel after validation. + * + * @param project Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @param start Start position in musical ticks. + * @param length Duration in musical ticks. + * @param pitch MIDI note number, 0 to 127. + * @param velocity MIDI note velocity, 1 to 127. + * @param channel MIDI channel, 0 to 15. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_edit_note(xtc_project *project, uint64_t id, uint64_t start, uint64_t length, unsigned int pitch, unsigned int velocity, unsigned int channel); +/** + * @brief Removes a note selected by stable identifier. + * + * @param project Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_remove_note(xtc_project *project, uint64_t id); /* Complete 3-byte note on/off and CC120/123 only. Velocity-zero on -> off. * Unsupported messages return UNSUPPORTED; malformed supported messages INVALID. * Output unchanged on error. No running-status/SysEx/pedal/program/bend support. */ +/** + * @brief Decodes a complete note or all-notes-off MIDI message into a block event. + * + * @param bytes Borrowed MIDI message bytes. + * @param size Number of message bytes. + * @param offset Sample offset within the current block. + * @param out_event Receives the event. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_midi_decode(const unsigned char *bytes, size_t size, unsigned int offset, xtc_midi_event *out_event); /* Control-thread compile; immutable ranges plus exclusively audio-owned runtime. * Rejects >32 simultaneous scheduled notes. Separate 32 live voices steal oldest. * Constant-tempo sine instrument, velocity *0.1, above-Nyquist notes silent. */ +/** + * @brief Compiles scheduled notes and mixer state into a bounded sine-instrument plan. + * + * @param project Project model; borrowed for this call. + * @param sample_rate Sample rate in hertz. + * @param out_plan Receives the newly allocated plan; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int sample_rate, xtc_midi_plan **out_plan); +/** + * @brief Releases scheduled notes and instrument state; accepts NULL. + * + * @param plan Instrument plan and its single-owner runtime state. + */ void xtc_midi_plan_destroy(xtc_midi_plan *plan); /* Audio owner or stopped only. Reset clears live notes and panic suppression. * Panic silences live notes and scheduled notes already started at position; * later scheduled notes can start. MIDI CC120/123 affects its own channel. */ +/** + * @brief Clears live voices, scheduling continuity, and panic suppression. + * + * @param plan Instrument plan and its single-owner runtime state. + */ void xtc_midi_plan_reset(xtc_midi_plan *plan); +/** + * @brief Silences live voices and suppresses scheduled notes already started at the given position. + * + * @param plan Instrument plan and its single-owner runtime state. + * @param position Absolute sample position at the start of the block. + */ void xtc_midi_plan_panic(xtc_midi_plan *plan, uint64_t position); /* No allocation, I/O, locks or GUI. Events sorted by offset, equal offsets retain * caller order, count<=256, frames<=65536. Invalid input leaves state/output alone. * Renders [position,position+frames); discontinuity clears live voices, chases * scheduled notes at their correct phase. Note-off is immediate (no envelope). */ /* Distinct stereo outputs; NULL right retains mono (gain only). */ -xtc_result xtc_midi_plan_render_stereo(xtc_midi_plan *, uint64_t, const xtc_midi_event *, size_t, float *left, - float *right, size_t); +/** + * @brief Renders scheduled and live MIDI with sample-offset events and track gain and pan. + * + * @param p Instrument plan and its single-owner runtime state. + * @param position Absolute sample position at the start of the block. + * @param events Borrowed MIDI events sorted by sample offset. + * @param count Number of MIDI events. + * @param left Destination left-channel buffer. + * @param right Destination right-channel buffer; NULL selects mono rendering. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_midi_plan_render_stereo(xtc_midi_plan * p, uint64_t position, const xtc_midi_event * events, size_t count, float *left, + float *right, size_t frames); +/** + * @brief Renders scheduled and live MIDI to mono without stereo pan attenuation. + * + * @param plan Instrument plan and its single-owner runtime state. + * @param position Absolute sample position at the start of the block. + * @param events Borrowed MIDI events sorted by sample offset. + * @param count Number of MIDI events. + * @param output Destination buffer with room for the requested frames. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_midi_plan_render(xtc_midi_plan *plan, uint64_t position, const xtc_midi_event *events, size_t count, float *output, size_t frames); diff --git a/include/xtc/xtc_mixer.h b/include/xtc/xtc_mixer.h index 8a81165..d3b27f8 100644 --- a/include/xtc/xtc_mixer.h +++ b/include/xtc/xtc_mixer.h @@ -25,11 +25,53 @@ typedef struct { * Linear pan: center unity on both outputs; hard pan attenuates opposite side. * Automation replaces the corresponding static value, linear between points, * holds first/last outside the lane. Duplicate track/parameter/tick replaces. */ -xtc_result xtc_project_set_mix(xtc_project *, uint64_t track, float gain, float pan, int mute, int solo); -xtc_result xtc_project_set_automation(xtc_project *, const xtc_automation_point *); -xtc_result xtc_project_remove_automation(xtc_project *, uint64_t track, xtc_automation_parameter, uint64_t tick); -xtc_result xtc_project_automation_count(const xtc_project *, size_t *); -xtc_result xtc_project_get_automation(const xtc_project *, size_t, xtc_automation_point *); +/** + * @brief Sets a track's gain, pan, mute, and solo controls after validation. + * + * @param p Project model; borrowed for this call. + * @param track Stable track identifier. + * @param gain Linear gain, 0 to 4 for track controls. + * @param pan Pan position from -1 (left) to 1 (right). + * @param mute Track mute flag, 0 or 1. + * @param solo Track solo flag, 0 or 1. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_project_set_mix(xtc_project * p, uint64_t track, float gain, float pan, int mute, int solo); +/** + * @brief Inserts an automation point, replacing any point at the same track, parameter, and tick. + * + * @param p Project model; borrowed for this call. + * @param a Automation point to read or update. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_project_set_automation(xtc_project * p, const xtc_automation_point * a); +/** + * @brief Removes the automation point at the specified track, parameter, and tick. + * + * @param p Project model; borrowed for this call. + * @param track Stable track identifier. + * @param param Automation lane selector. + * @param tick Musical position in ticks. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_project_remove_automation(xtc_project * p, uint64_t track, xtc_automation_parameter param, uint64_t tick); +/** + * @brief Copies the number of automation points in the project. + * + * @param p Project model; borrowed for this call. + * @param n Receives the item count. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_project_automation_count(const xtc_project * p, size_t * n); +/** + * @brief Copies an automation point selected by dense list index. + * + * @param p Project model; borrowed for this call. + * @param i Zero-based index in the dense list. + * @param a Automation point to read or update. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_project_get_automation(const xtc_project * p, size_t i, xtc_automation_point * a); /* Control-thread snapshot history; 32 edits plus baseline, no assets/DSP/GUI. * record clones before changing history; failure retains all history. checkout * clones adjacent state without moving cursor; accept only after installation. @@ -37,11 +79,45 @@ xtc_result xtc_project_get_automation(const xtc_project *, size_t, xtc_automatio * Reset by creating a new history for New/Open. */ typedef struct xtc_history xtc_history; -xtc_result xtc_history_create(const xtc_project *, xtc_history **); -void xtc_history_destroy(xtc_history *); -xtc_result xtc_history_record(xtc_history *, const xtc_project *); -xtc_result xtc_history_checkout(const xtc_history *, int redo, xtc_project **); -xtc_result xtc_history_accept(xtc_history *, int redo); +/** + * @brief Creates snapshot history with an independent copy of the initial project. + * + * @param p Project model; borrowed for this call. + * @param out Receives the newly allocated object; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_history_create(const xtc_project * p, xtc_history ** out); +/** + * @brief Releases all retained snapshots and history storage; accepts NULL. + * + * @param h Snapshot history handle. + */ +void xtc_history_destroy(xtc_history * h); +/** + * @brief Appends a project snapshot, discarding redo states and evicting the oldest state if full. + * + * @param h Snapshot history handle. + * @param p Project model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_history_record(xtc_history * h, const xtc_project * p); +/** + * @brief Clones the adjacent undo or redo state without moving the history cursor. + * + * @param h Snapshot history handle. + * @param redo Zero for undo, one for redo. + * @param out Receives the newly allocated object; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_history_checkout(const xtc_history * h, int redo, xtc_project ** out); +/** + * @brief Moves the history cursor after the checked-out project has been installed. + * + * @param h Snapshot history handle. + * @param redo Zero for undo, one for redo. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_history_accept(xtc_history * h, int redo); #ifdef __cplusplus } diff --git a/include/xtc/xtc_native.h b/include/xtc/xtc_native.h index d019e19..e91e03a 100644 --- a/include/xtc/xtc_native.h +++ b/include/xtc/xtc_native.h @@ -31,34 +31,110 @@ typedef struct { char identifier[XTC_NATIVE_ID_BYTES]; /* Nonempty printable ASCII, terminated. */ xtc_native_kind kind; /* Control thread. On error, any non-NULL *state is destroyed by host. */ + /** + * @brief Allocates and prepares plugin state on the control thread. + * + * @param sample_rate Processing rate in hertz. + * @param max_frames Maximum frames per processing block. + * @param parameter Initial processor parameter. + * @param state Receives plugin state; the host destroys non-NULL state even on failure. + * @return XTC_OK on success, or a preparation error. + */ xtc_result (*prepare)(double sample_rate, size_t max_frames, float parameter, void **state); /* Single audio owner (or stopped). No allocation/free, I/O, locks or GUI. */ + /** + * @brief Resets plugin state without allocation, I/O, locking, or GUI calls. + * + * @param state Plugin state owned exclusively by the audio consumer, or stopped. + */ void (*reset)(void *state); + /** + * @brief Processes one block without allocation, I/O, locking, or GUI calls. + * + * @param state Plugin state owned exclusively by the audio consumer. + * @param block Borrowed buffers and events; write every output frame, including silence. + * @return XTC_OK on success, or a processing error that makes the host silence the block. + */ xtc_result (*process)(void *state, const xtc_native_block *block); /* Control thread after quiescence; must accept NULL. */ + /** + * @brief Releases plugin state on the control thread after callback quiescence. + * + * @param state Plugin state to release; must accept NULL. + */ void (*destroy)(void *state); } xtc_native_descriptor; typedef struct xtc_native xtc_native; /* Descriptors are copied; callback code must remain loaded through destruction. * Trusted statically linked C code, not a sandbox or stable shared-library ABI. */ -xtc_result xtc_native_validate(const xtc_native_descriptor *); -xtc_result xtc_native_create(const xtc_native_descriptor *, double rate, size_t max_frames, float parameter, +/** + * @brief Checks the native descriptor's ABI, identifier, kind, and required callbacks. + * + * @param d Native processor descriptor. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_native_validate(const xtc_native_descriptor * d); +/** + * @brief Copies a native descriptor and prepares a processor instance on the control thread. + * + * @param d Native processor descriptor. + * @param rate Sample rate in hertz. + * @param max_frames Maximum frames per processing block. + * @param parameter Native parameter or automation lane selector, as applicable. + * @param out_processor Receives the newly allocated processor; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_native_create(const xtc_native_descriptor * d, double rate, size_t max_frames, float parameter, xtc_native **out_processor); -void xtc_native_destroy(xtc_native *); -void xtc_native_reset(xtc_native *); +/** + * @brief Destroys native state and releases the host wrapper; accepts NULL. + * + * @param p Native processor instance. + */ +void xtc_native_destroy(xtc_native * p); +/** + * @brief Resets native state and transport continuity; accepts NULL. + * + * @param p Native processor instance. + */ +void xtc_native_reset(xtc_native * p); /* Bounded, single-owner, realtime-safe host path (callbacks must obey contract). * Invalid arguments leave state/output unchanged. Zero frames is a no-op. * Discontinuity resets before processing. Callback failure silences this block * and resets the processor. Successful nonfinite output samples become zero. */ -xtc_result xtc_native_process(xtc_native *, const xtc_native_block *); +/** + * @brief Processes a bounded native block, resetting on discontinuity and silencing callback failures. + * + * @param p Native processor instance. + * @param b Borrowed processing buffers and block metadata. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_native_process(xtc_native * p, const xtc_native_block * b); /* Graph accepts effects/sources; creates independent L/R instances on compile. * For instruments, process events explicitly then feed the existing instrument * source buffer through xtc_plan_process_sources/stereo. Graph add returns * UNSUPPORTED for instrument descriptors (no event port in current topology). * Native graphs are session-only: project save returns UNSUPPORTED. */ -xtc_result xtc_graph_add_native(xtc_graph *, const xtc_native_descriptor *, float parameter, size_t *out_id); -xtc_result xtc_graph_get_native(const xtc_graph *, size_t node_id, xtc_native_descriptor *out_descriptor); +/** + * @brief Adds a validated native effect or source descriptor to the graph. + * + * @param graph Graph model; borrowed for this call. + * @param descriptor Native processor descriptor. + * @param parameter Native parameter or automation lane selector, as applicable. + * @param out_id Receives the id. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_graph_add_native(xtc_graph * graph, const xtc_native_descriptor * descriptor, float parameter, size_t *out_id); +/** + * @brief Copies the native descriptor of a node selected by stable identifier. + * + * @param graph Graph model; borrowed for this call. + * @param node_id Stable node identifier. + * @param out_descriptor Receives the descriptor. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_graph_get_native(const xtc_graph * graph, size_t node_id, xtc_native_descriptor *out_descriptor); #ifdef __cplusplus } diff --git a/include/xtc/xtc_project.h b/include/xtc/xtc_project.h index cd9f865..e57ad44 100644 --- a/include/xtc/xtc_project.h +++ b/include/xtc/xtc_project.h @@ -33,39 +33,191 @@ typedef struct { /* Single control-thread ownership. Names: nonempty UTF-8, <=63 bytes, no ASCII controls. * IDs are monotonic, never reused, UINT64_MAX reserved. Snapshots are copies. */ +/** + * @brief Allocates an empty project with an output-only graph policy and 120 BPM tempo. + * + * @param out_project Receives the newly allocated project; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_create(xtc_project **out_project); +/** + * @brief Releases a project and its graph; accepts NULL. + * + * @param project Project model; borrowed for this call. + */ void xtc_project_destroy(xtc_project *project); +/** + * @brief Allocates an independent copy of project metadata and its graph. + * + * @param project Project model; borrowed for this call. + * @param out_project Receives the newly allocated project; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_clone(const xtc_project *project, xtc_project **out_project); +/** + * @brief Copies the current track and clip counts. + * + * @param project Project model; borrowed for this call. + * @param out_tracks Receives the tracks. + * @param out_clips Receives the clips. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_get_counts(const xtc_project *project, size_t *out_tracks, size_t *out_clips); +/** + * @brief Copies a track snapshot selected by dense list index. + * + * @param project Project model; borrowed for this call. + * @param index Zero-based index in the dense list. + * @param out_track Receives the track. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_get_track(const xtc_project *project, size_t index, xtc_track_info *out_track); +/** + * @brief Copies a clip snapshot selected by dense list index. + * + * @param project Project model; borrowed for this call. + * @param index Zero-based index in the dense list. + * @param out_clip Receives the clip. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_get_clip(const xtc_project *project, size_t index, xtc_clip_info *out_clip); +/** + * @brief Copies a clip snapshot selected by stable identifier. + * + * @param project Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @param out_clip Receives the clip. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_find_clip(const xtc_project *project, uint64_t id, xtc_clip_info *out_clip); +/** + * @brief Appends a track with unity gain and a new stable identifier. + * + * @param project Project model; borrowed for this call. + * @param name NUL-terminated name. + * @param out_id Receives the id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_add_track(xtc_project *project, const char *name, uint64_t *out_id); /* Removing a track also removes its clips. */ +/** + * @brief Removes a track and its clips, notes, and automation. + * + * @param project Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_remove_track(xtc_project *project, uint64_t id); +/** + * @brief Appends a silent region on an existing track with a new stable identifier. + * + * @param project Project model; borrowed for this call. + * @param track_id Stable destination track identifier. + * @param name NUL-terminated name. + * @param start_tick Start position in musical ticks. + * @param length_tick Duration in musical ticks. + * @param out_id Receives the id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_add_clip(xtc_project *project, uint64_t track_id, const char *name, uint64_t start_tick, uint64_t length_tick, uint64_t *out_id); +/** + * @brief Removes a clip and its notes. + * + * @param project Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_remove_clip(xtc_project *project, uint64_t id); /* Atomic move/trim: source bounds remain unchanged, no destructive edits. */ +/** + * @brief Atomically moves or trims a clip within its existing source bounds. + * + * @param project Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @param track_id Stable destination track identifier. + * @param start_tick Start position in musical ticks. + * @param length_tick Duration in musical ticks. + * @param offset_tick Offset into the clip source in ticks. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_edit_clip(xtc_project *project, uint64_t id, uint64_t track_id, uint64_t start_tick, uint64_t length_tick, uint64_t offset_tick); /* Borrowed opaque graph valid until project mutation/destruction. set_graph * copies before replacing; max 64 nodes, preserving IDs, edges and output policy. */ +/** + * @brief Returns the project's borrowed graph, or NULL for a NULL project. + * + * @param project Project model; borrowed for this call. + * @return Borrowed graph valid until project mutation or destruction, or NULL for NULL. + */ const xtc_graph *xtc_project_graph(const xtc_project *project); +/** + * @brief Copies and validates a graph before replacing the project's graph. + * + * @param project Project model; borrowed for this call. + * @param graph Graph model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_set_graph(xtc_project *project, const xtc_graph *graph); /* Constant tempo in milli-BPM: 20000..300000, default 120000. */ +/** + * @brief Returns the constant tempo in milli-BPM, or zero for a NULL project. + * + * @param project Project model; borrowed for this call. + * @return Tempo in milli-BPM, or zero for NULL. + */ unsigned int xtc_project_tempo(const xtc_project *project); +/** + * @brief Sets the constant tempo within the range 20000 to 300000 milli-BPM. + * + * @param project Project model; borrowed for this call. + * @param milli_bpm Tempo in thousandths of a beat per minute. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_set_tempo(xtc_project *project, unsigned int milli_bpm); /* Floor conversion, no accumulated drift. Rate: 1..768000. Tick <= MAX_TICK. * Excess sample/tick positions return capacity; outputs unchanged on error. */ +/** + * @brief Converts a musical tick to an absolute sample position, rounding down. + * + * @param project Project model; borrowed for this call. + * @param tick Musical position in ticks. + * @param sample_rate Sample rate in hertz. + * @param out_sample Receives the sample. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_tick_to_sample(const xtc_project *project, uint64_t tick, unsigned int sample_rate, uint64_t *out_sample); +/** + * @brief Converts an absolute sample position to a musical tick, rounding down. + * + * @param project Project model; borrowed for this call. + * @param sample Absolute sample position. + * @param sample_rate Sample rate in hertz. + * @param out_tick Receives the tick. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_sample_to_tick(const xtc_project *project, uint64_t sample, unsigned int sample_rate, uint64_t *out_tick); /* Control-thread I/O. Versioned, bounded format; see docs/PROJECT_FORMAT.md. * Load is transactional. Save fsyncs a same-directory temporary and renames it; * failures before rename preserve the previous destination. No asset I/O. */ +/** + * @brief Saves a versioned project through a synced temporary file and atomic rename. + * + * @param project Project model; borrowed for this call. + * @param path NUL-terminated file or asset path. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_save(const xtc_project *project, const char *path); +/** + * @brief Loads and validates a project before transactionally replacing the current state. + * + * @param project Project model; borrowed for this call. + * @param path NUL-terminated file or asset path. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_load(xtc_project *project, const char *path); #ifdef __cplusplus diff --git a/include/xtc/xtc_realtime.h b/include/xtc/xtc_realtime.h index 0abb0fc..a7fd449 100644 --- a/include/xtc/xtc_realtime.h +++ b/include/xtc/xtc_realtime.h @@ -18,27 +18,103 @@ typedef struct { /* Control thread only. A single control producer and single audio consumer. * C99 with GCC/Clang lock-free __atomic builtins is required; unsupported * targets return XTC_ERR_UNSUPPORTED. No ownership is shared with callers. */ +/** + * @brief Allocates the single-producer, single-consumer realtime engine. + * + * @param sample_rate Sample rate in hertz. + * @param max_frames Maximum frames per processing block. + * @param out_engine Receives the newly allocated engine; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_realtime_create(double sample_rate, size_t max_frames, xtc_realtime **out_engine); /* Requires the audio consumer to have stopped and joined. */ +/** + * @brief Releases the engine after the audio consumer has stopped; accepts NULL. + * + * @param engine Realtime engine handle. + */ void xtc_realtime_destroy(xtc_realtime *engine); /* Compiles/copies a graph. One pending plan allowed; full mailbox returns * XTC_ERR_CAPACITY without changing the active or pending plan. */ +/** + * @brief Compiles and publishes a graph to the single pending-plan slot. + * + * @param engine Realtime engine handle. + * @param graph Graph model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_realtime_submit(xtc_realtime *engine, const xtc_graph *graph); /* Frees acknowledged retired state, never the audio thread's active plan. */ +/** + * @brief Frees acknowledged retired plans on the control thread. + * + * @param engine Realtime engine handle. + */ void xtc_realtime_collect(xtc_realtime *engine); +/** + * @brief Copies processing counters, plan revisions, and the last processing result. + * + * @param engine Realtime engine handle. + * @param out_status Receives the status. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_realtime_get_status(const xtc_realtime *engine, xtc_realtime_status *out_status); /* Audio consumer only. Adopts at most one plan per block, without allocation, * freeing, locks or I/O. No plan means silence. On failure writes silence. * frame_count must be <= configured max_frames and output must hold it. * Oversized buffers are silenced too; callers must bound their own frame count. * NULL input means silence. Zero frames is a no-op. */ -xtc_result xtc_realtime_process_stereo(xtc_realtime *, const float *input, const float *audio_left, +/** + * @brief Adopts a pending plan and processes stereo sources without allocation or locking. + * + * @param engine Realtime engine handle. + * @param input Borrowed input sample buffer. + * @param audio_left Borrowed left timeline source buffer; NULL means silence. + * @param audio_right Borrowed right timeline source buffer; NULL means silence. + * @param midi_left Borrowed left instrument source buffer; NULL means silence. + * @param midi_right Borrowed right instrument source buffer; NULL means silence. + * @param left Destination left-channel buffer. + * @param right Destination right-channel buffer; NULL selects mono rendering. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ +xtc_result xtc_realtime_process_stereo(xtc_realtime * engine, const float *input, const float *audio_left, const float *audio_right, const float *midi_left, const float *midi_right, float *left, float *right, size_t frames); +/** + * @brief Processes mono timeline and instrument sources through the realtime engine. + * + * @param engine Realtime engine handle. + * @param input Borrowed input sample buffer. + * @param timeline Borrowed timeline source buffer; NULL means silence. + * @param instrument Borrowed instrument source buffer; NULL means silence. + * @param output Destination buffer with room for the requested frames. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_realtime_process_sources(xtc_realtime *engine, const float *input, const float *timeline, const float *instrument, float *output, size_t frames); +/** + * @brief Processes a mono block with timeline audio and no instrument source. + * + * @param engine Realtime engine handle. + * @param input Borrowed input sample buffer. + * @param timeline Borrowed timeline source buffer; NULL means silence. + * @param output Destination buffer with room for the requested frames. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_realtime_process_audio(xtc_realtime *engine, const float *input, const float *timeline, float *output, size_t frames); +/** + * @brief Processes a mono block with external input only. + * + * @param engine Realtime engine handle. + * @param input Borrowed input sample buffer. + * @param output Destination buffer with room for the requested frames. + * @param frame_count Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_realtime_process(xtc_realtime *engine, const float *input, float *output, size_t frame_count); #ifdef __cplusplus diff --git a/include/xtc/xtc_types.h b/include/xtc/xtc_types.h index 9759e7f..ddb8b68 100644 --- a/include/xtc/xtc_types.h +++ b/include/xtc/xtc_types.h @@ -21,6 +21,12 @@ typedef enum { XTC_ERR_FORMAT } xtc_result; +/** + * @brief Returns a short human-readable name for a result code. + * + * @param result Result code to describe. + * @return Borrowed static string; never NULL. + */ const char *xtc_result_string(xtc_result result); #ifdef __cplusplus diff --git a/src/audio/arrangement.c b/src/audio/arrangement.c index fab7c13..6582d53 100644 --- a/src/audio/arrangement.c +++ b/src/audio/arrangement.c @@ -21,6 +21,23 @@ struct xtc_audio_plan { unsigned int rate; }; +/** + * @brief Imports an audio file as a timeline clip. + * + * Resolves path to an absolute reference, reads the WAV to determine its + * musical length, then transactionally clones the project, adds a clip, stores + * the asset reference, and adds an audio source -> output route when the graph + * has no audio source. The project is unchanged on failure. + * + * @param project Project to modify; must not be NULL. + * @param track Existing destination track ID. + * @param path Source WAV path; resolved with realpath() and must fit the asset path limit. + * @param start Clip start position in project ticks. + * @param out_clip Receives the new clip ID on success; unchanged on failure. + * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for NULL arguments, XTC_ERR_IO + * when the path cannot be resolved or read, XTC_ERR_CAPACITY when the absolute + * path is too long, or a WAV load or project edit error. + */ xtc_result xtc_project_import_audio(xtc_project *project, uint64_t track, const char *path, uint64_t start, uint64_t *out_clip) { @@ -79,6 +96,11 @@ xtc_result xtc_project_import_audio(xtc_project *project, uint64_t track, const return result; } +/** + * @brief Releases an audio plan and all samples preloaded by it. + * + * @param plan Plan to destroy; NULL is accepted and does nothing. + */ void xtc_audio_plan_destroy(xtc_audio_plan *plan) { size_t i; @@ -89,6 +111,21 @@ void xtc_audio_plan_destroy(xtc_audio_plan *plan) free(plan); } +/** + * @brief Preloads project audio into an immutable render plan. + * + * At most 64 distinct assets and 67,108,864 total mono frames are accepted. + * Missing or invalid assets are skipped and counted as issues rather than + * failing creation. The caller owns the returned plan and must release it with + * xtc_audio_plan_destroy(). + * + * @param project Project whose clips are compiled; must not be NULL. + * @param rate Output sample rate in Hz, from 1 to 768000. + * @param out Receives the new plan on success and is set to NULL on failure. + * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for NULL arguments or an + * invalid rate, XTC_ERR_OUT_OF_MEMORY if allocation fails, XTC_ERR_CAPACITY + * for asset or frame limits, or an error from mix preparation or tick conversion. + */ xtc_result xtc_audio_plan_create(const xtc_project *project, unsigned int rate, xtc_audio_plan **out) { xtc_audio_plan *plan; @@ -158,6 +195,14 @@ xtc_result xtc_audio_plan_create(const xtc_project *project, unsigned int rate, return XTC_OK; } +/** + * @brief Returns the number of assets that could not be loaded. + * + * @param plan Plan to query; NULL reports zero issues. + * @param path Receives a borrowed path to the first failed asset, or an empty + * string when there is none; may be NULL. + * @return Number of skipped assets; zero when plan is NULL. + */ size_t xtc_audio_plan_issues(const xtc_audio_plan *plan, const char **path) { if (path) @@ -165,6 +210,20 @@ size_t xtc_audio_plan_issues(const xtc_audio_plan *plan, const char **path) return plan ? plan->issues : 0; } +/** + * @brief Renders a block of the plan at unity gain with linear rate conversion. + * + * Both buffers are replaced, not mixed into. Rendering performs no allocation, + * file access, or locking. Non-finite output samples are replaced with zero. + * + * @param plan Immutable plan to render; must not be NULL. + * @param position First output sample position on the project timeline. + * @param output Left or mono destination with room for frames samples. + * @param right Optional right destination; must differ from output, or NULL for mono. + * @param frames Number of samples to render, at most 65536. + * @return XTC_OK on success, or XTC_ERR_INVALID_ARGUMENT for NULL output, aliased + * stereo buffers, too many frames, or a position overflow. + */ xtc_result xtc_audio_plan_render_stereo(const xtc_audio_plan *plan, uint64_t position, float *output, float *right, size_t frames) { @@ -206,6 +265,17 @@ xtc_result xtc_audio_plan_render_stereo(const xtc_audio_plan *plan, uint64_t pos return XTC_OK; } +/** + * @brief Renders a mono block of the plan. + * + * Equivalent to xtc_audio_plan_render_stereo() with no right channel. + * + * @param p Immutable plan to render; must not be NULL. + * @param position First output sample position on the project timeline. + * @param out Destination with room for frames samples. + * @param frames Number of samples to render, at most 65536. + * @return XTC_OK on success, or XTC_ERR_INVALID_ARGUMENT for invalid arguments. + */ xtc_result xtc_audio_plan_render(const xtc_audio_plan *p, uint64_t position, float *out, size_t frames) { return xtc_audio_plan_render_stereo(p, position, out, NULL, frames); diff --git a/src/audio/recorder.c b/src/audio/recorder.c index a8b6798..f64a038 100644 --- a/src/audio/recorder.c +++ b/src/audio/recorder.c @@ -15,6 +15,15 @@ struct xtc_recorder { int finished; }; +/** + * @brief Allocates a bounded recording ring and opens a temporary mono float32 WAV file. + * + * @param path NUL-terminated file or asset path. + * @param rate Sample rate in hertz. + * @param capacity Recording ring capacity in frames, from 1 to 2097152. + * @param out Receives the newly allocated object; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_recorder_create(const char *path, unsigned int rate, size_t capacity, xtc_recorder **out) { xtc_recorder *r; @@ -67,6 +76,13 @@ xtc_result xtc_recorder_create(const char *path, unsigned int rate, size_t capac return XTC_OK; } +/** + * @brief Enqueues an entire audio block without blocking; overflow sets a sticky error. + * + * @param r Recorder handle. + * @param input Borrowed input sample buffer. + * @param frames Number of audio frames. + */ void xtc_recorder_push(xtc_recorder *r, const float *input, size_t frames) { uint64_t write, read; @@ -85,6 +101,12 @@ void xtc_recorder_push(xtc_recorder *r, const float *input, size_t frames) __atomic_store_n(&r->write_position, write + frames, __ATOMIC_RELEASE); } +/** + * @brief Writes queued samples to the temporary WAV file on the control thread. + * + * @param r Recorder handle. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_recorder_drain(xtc_recorder *r) { uint64_t read, write; @@ -108,6 +130,13 @@ xtc_result xtc_recorder_drain(xtc_recorder *r) return (xtc_result)__atomic_load_n(&r->error, __ATOMIC_ACQUIRE); } +/** + * @brief Copies the number of accepted frames and returns the sticky recording status. + * + * @param r Recorder handle. + * @param frames Receives the accepted frame count. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_recorder_status(const xtc_recorder *r, uint64_t *frames) { if (!r || !frames) @@ -116,6 +145,12 @@ xtc_result xtc_recorder_status(const xtc_recorder *r, uint64_t *frames) return (xtc_result)__atomic_load_n(&r->error, __ATOMIC_ACQUIRE); } +/** + * @brief Drains and finalizes the WAV file, then renames it after producer quiescence. + * + * @param r Recorder handle. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_recorder_finish(xtc_recorder *r) { xtc_result result; @@ -137,6 +172,11 @@ xtc_result xtc_recorder_finish(xtc_recorder *r) return failed ? XTC_ERR_IO : result; } +/** + * @brief Releases recorder storage and removes any unfinished temporary file; accepts NULL. + * + * @param r Recorder handle. + */ void xtc_recorder_destroy(xtc_recorder *r) { if (!r) diff --git a/src/audio/wave.c b/src/audio/wave.c index 0fe88b2..d1c8086 100644 --- a/src/audio/wave.c +++ b/src/audio/wave.c @@ -4,16 +4,34 @@ #include #include +/** + * @brief Reads an unsigned 32-bit integer from four little-endian bytes. + * + * @param p Byte buffer containing the little-endian integer. + * @return Decoded 32-bit value. + */ static uint32_t u32(const unsigned char *p) { return (uint32_t)p[0] | (uint32_t)p[1] << 8U | (uint32_t)p[2] << 16U | (uint32_t)p[3] << 24U; } +/** + * @brief Reads an unsigned 16-bit integer from two little-endian bytes. + * + * @param p Byte buffer containing the little-endian integer. + * @return Decoded 16-bit value. + */ static unsigned int u16(const unsigned char *p) { return (unsigned int)p[0] | (unsigned int)p[1] << 8U; } +/** + * @brief Writes an unsigned 32-bit integer as four little-endian bytes. + * + * @param p Destination buffer with room for four bytes. + * @param v Unsigned integer to encode. + */ static void put32(unsigned char *p, uint32_t v) { size_t i; @@ -23,6 +41,14 @@ static void put32(unsigned char *p, uint32_t v) } } +/** + * @brief Writes a 44-byte mono float32 WAV header at the current file position. + * + * @param file Open file stream at the required read or write position. + * @param rate Sample rate in hertz. + * @param frames Number of audio frames. + * @return Nonzero if all header bytes were written, zero on a write failure. + */ int xtc_wave_header(FILE *file, unsigned int rate, uint32_t frames) { unsigned char h[44] = {0}; @@ -41,6 +67,14 @@ int xtc_wave_header(FILE *file, unsigned int rate, uint32_t frames) return fwrite(h, 1, sizeof(h), file) == sizeof(h); } +/** + * @brief Writes mono float32 samples in little-endian order, replacing nonfinite values with zero. + * + * @param file Open file stream at the required read or write position. + * @param input Borrowed input sample buffer. + * @param frames Number of audio frames. + * @return Nonzero if all samples were written, zero on a write failure. + */ int xtc_wave_write(FILE *file, const float *input, size_t frames) { unsigned char bytes[4096]; @@ -60,6 +94,13 @@ int xtc_wave_write(FILE *file, const float *input, size_t frames) return 1; } +/** + * @brief Loads a bounded PCM or float WAV file into caller-owned mono sample storage. + * + * @param path NUL-terminated file or asset path. + * @param out Receives the operation result. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_wave_load(const char *path, xtc_wave *out) { FILE *f; diff --git a/src/audio/wave_internal.h b/src/audio/wave_internal.h index 366b82d..ffd8adf 100644 --- a/src/audio/wave_internal.h +++ b/src/audio/wave_internal.h @@ -10,7 +10,30 @@ typedef struct { unsigned int rate; } xtc_wave; +/** + * @brief Loads a bounded PCM or float WAV file into caller-owned mono sample storage. + * + * @param path NUL-terminated file or asset path. + * @param out_wave Receives the wave. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_wave_load(const char *path, xtc_wave *out_wave); +/** + * @brief Writes a 44-byte mono float32 WAV header at the current file position. + * + * @param file Open file stream at the required read or write position. + * @param rate Sample rate in hertz. + * @param frames Number of audio frames. + * @return Nonzero if all header bytes were written, zero on a write failure. + */ int xtc_wave_header(FILE *file, unsigned int rate, uint32_t frames); +/** + * @brief Writes mono float32 samples in little-endian order, replacing nonfinite values with zero. + * + * @param file Open file stream at the required read or write position. + * @param input Borrowed input sample buffer. + * @param frames Number of audio frames. + * @return Nonzero if all samples were written, zero on a write failure. + */ int xtc_wave_write(FILE *file, const float *input, size_t frames); #endif diff --git a/src/backend/jack_backend.c b/src/backend/jack_backend.c index d1f1c6f..12d58aa 100644 --- a/src/backend/jack_backend.c +++ b/src/backend/jack_backend.c @@ -26,6 +26,14 @@ struct xtc_backend { int ready, running; /* Control thread only. */ }; +/** + * @brief Decodes queued JACK MIDI and renders the instrument for the current audio block. + * + * @param backend Backend handle. + * @param position Absolute sample position at the start of the block. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ static xtc_result render_midi(xtc_backend *backend, uint64_t position, jack_nframes_t frames) { xtc_midi_event events[XTC_MIDI_MAX_EVENTS]; @@ -70,6 +78,13 @@ static xtc_result render_midi(xtc_backend *backend, uint64_t position, jack_nfra (size_t)frames); } +/** + * @brief Runs the JACK audio callback, rendering sources, processing the graph, and publishing status. + * + * @param frames Number of audio frames. + * @param context Opaque callback context. + * @return Zero after publishing the callback result. + */ static int process(jack_nframes_t frames, void *context) { xtc_backend *backend = context; @@ -145,12 +160,25 @@ static int process(jack_nframes_t frames, void *context) return 0; /* Keep connected and silent on recoverable processing errors. */ } +/** + * @brief Marks server loss and clears running state when JACK shuts down. + * + * @param context Opaque callback context. + */ static void shutdown_callback(void *context) { xtc_backend *backend = context; __atomic_store_n(&backend->lost, 1U, __ATOMIC_RELEASE); } +/** + * @brief Requests a MIDI panic when a connection to the MIDI input changes. + * + * @param source JACK source port identifier. + * @param destination JACK destination port identifier. + * @param connected Nonzero when the ports became connected. + * @param context Opaque callback context. + */ static void port_connection(jack_port_id_t source, jack_port_id_t destination, int connected, void *context) { xtc_backend *backend = context; @@ -159,6 +187,13 @@ static void port_connection(jack_port_id_t source, jack_port_id_t destination, i (void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE); } +/** + * @brief Records a JACK sample-rate change and disables processing until recovery. + * + * @param rate Sample rate in hertz. + * @param context Opaque callback context. + * @return Zero to acknowledge the rate notification. + */ static int rate_callback(jack_nframes_t rate, void *context) { xtc_backend *backend = context; @@ -168,6 +203,13 @@ static int rate_callback(jack_nframes_t rate, void *context) return 0; } +/** + * @brief Records the JACK buffer size and reports whether it exceeds the configured limit. + * + * @param frames Number of audio frames. + * @param context Opaque callback context. + * @return Zero if the buffer fits, nonzero if it exceeds the configured capacity. + */ static int buffer_callback(jack_nframes_t frames, void *context) { xtc_backend *backend = context; @@ -175,6 +217,12 @@ static int buffer_callback(jack_nframes_t frames, void *context) return 0; } +/** + * @brief Increments the xrun counter and requests a MIDI panic. + * + * @param context Opaque callback context. + * @return Zero to acknowledge the xrun notification. + */ static int xrun_callback(void *context) { xtc_backend *backend = context; @@ -183,6 +231,13 @@ static int xrun_callback(void *context) return 0; } +/** + * @brief Opens a JACK client and registers its ports without starting the server or connecting routes. + * + * @param config Backend kind, client name, block capacity, and output channel configuration. + * @param out_backend Receives the newly allocated backend; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_backend) { xtc_backend *backend; @@ -266,6 +321,12 @@ fail: return result; } +/** + * @brief Quiesces callbacks and releases the backend; failed cleanup leaves the handle owned by the caller. + * + * @param backend Backend handle. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_close(xtc_backend *backend) { if (backend == NULL) @@ -282,6 +343,12 @@ xtc_result xtc_backend_close(xtc_backend *backend) return XTC_OK; } +/** + * @brief Activates the backend and enables audio processing. + * + * @param backend Backend handle. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_start(xtc_backend *backend) { if (backend == NULL) @@ -300,6 +367,12 @@ xtc_result xtc_backend_start(xtc_backend *backend) return XTC_OK; } +/** + * @brief Deactivates the backend and establishes callback quiescence. + * + * @param backend Backend handle. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_stop(xtc_backend *backend) { if (backend == NULL) @@ -318,6 +391,13 @@ xtc_result xtc_backend_stop(xtc_backend *backend) return XTC_OK; } +/** + * @brief Changes the stopped backend's sample position and resets MIDI voices. + * + * @param backend Backend handle. + * @param sample Absolute sample position. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_seek(xtc_backend *backend, uint64_t sample) { if (backend == NULL || sample > UINT64_MAX - 65536U) @@ -330,6 +410,13 @@ xtc_result xtc_backend_seek(xtc_backend *backend, uint64_t sample) return XTC_OK; } +/** + * @brief Compiles and submits a graph while preserving the previous engine on failure. + * + * @param backend Backend handle. + * @param graph Graph model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph) { if (backend == NULL) @@ -358,6 +445,13 @@ xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph) return xtc_realtime_submit(backend->engine, graph); } +/** + * @brief Preloads project audio and MIDI and submits its graph while stopped. + * + * @param backend Backend handle. + * @param project Project model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *project) { xtc_audio_plan *candidate = NULL; @@ -384,6 +478,12 @@ xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project * return XTC_OK; } +/** + * @brief Returns the first asset issue path, or an empty string when none is available. + * + * @param backend Backend handle. + * @return Borrowed path string, or an empty string if no issue is available. + */ const char *xtc_backend_asset_issue(const xtc_backend *backend) { const char *path = ""; @@ -392,6 +492,13 @@ const char *xtc_backend_asset_issue(const xtc_backend *backend) return path; } +/** + * @brief Attaches or detaches a borrowed recorder while the backend is stopped. + * + * @param backend Backend handle. + * @param recorder Recorder handle. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_set_recorder(xtc_backend *backend, xtc_recorder *recorder) { if (!backend) @@ -405,6 +512,12 @@ xtc_result xtc_backend_set_recorder(xtc_backend *backend, xtc_recorder *recorder return XTC_OK; } +/** + * @brief Requests an instrument panic at the next audio block boundary. + * + * @param backend Backend handle. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_panic(xtc_backend *backend) { if (!backend) @@ -417,6 +530,13 @@ xtc_result xtc_backend_panic(xtc_backend *backend) return XTC_OK; } +/** + * @brief Returns the borrowed name of the backend's MIDI input port. + * + * @param backend Backend handle. + * @param out Receives the newly allocated object; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_midi_port_name(const xtc_backend *backend, const char **out) { if (!backend || !out) @@ -427,12 +547,24 @@ xtc_result xtc_backend_midi_port_name(const xtc_backend *backend, const char **o return XTC_OK; } +/** + * @brief Reclaims retired realtime plans on the control thread. + * + * @param backend Backend handle. + */ void xtc_backend_collect(xtc_backend *backend) { if (backend != NULL) xtc_realtime_collect(backend->engine); } +/** + * @brief Copies individually coherent backend counters and transport state. + * + * @param backend Backend handle. + * @param out_status Receives the status. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *out_status) { xtc_realtime_status engine_status; @@ -461,6 +593,14 @@ xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status return XTC_OK; } +/** + * @brief Returns the borrowed name of the mono input or primary output port. + * + * @param backend Backend handle. + * @param direction Local port direction. + * @param out_name Receives the name. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction direction, const char **out_name) { if (backend == NULL || out_name == NULL || (direction != XTC_PORT_INPUT && direction != XTC_PORT_OUTPUT)) @@ -471,6 +611,14 @@ xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction return *out_name == NULL ? XTC_ERR_IO : XTC_OK; } +/** + * @brief Returns the borrowed name of an output channel port. + * + * @param backend Backend handle. + * @param channel Output channel index: 0 for primary, 1 for stereo right. + * @param out Receives the newly allocated object; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_output_name(const xtc_backend *backend, size_t channel, const char **out) { if (!backend || !out || channel > 1) @@ -483,6 +631,15 @@ xtc_result xtc_backend_output_name(const xtc_backend *backend, size_t channel, c return *out ? XTC_OK : XTC_ERR_IO; } +/** + * @brief Visits available audio ports matching the requested direction until the visitor stops. + * + * @param backend Backend handle. + * @param direction Local port direction. + * @param visitor Visitor called with each borrowed port name. + * @param context Opaque callback context. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction direction, xtc_backend_port_visitor visitor, void *context) { @@ -503,6 +660,15 @@ xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction dire return XTC_OK; } +/** + * @brief Connects or disconnects a local audio port and a named remote port. + * + * @param backend Backend handle. + * @param direction Local port direction. + * @param remote Borrowed remote port name. + * @param disconnect Nonzero to disconnect, zero to connect. + * @return XTC_OK on success, or an error code describing the failure. + */ static xtc_result route(xtc_backend *backend, xtc_port_direction direction, const char *remote, int disconnect) { const char *own = NULL; @@ -522,17 +688,41 @@ static xtc_result route(xtc_backend *backend, xtc_port_direction direction, cons return code == 0 || (!disconnect && code == EEXIST) ? XTC_OK : XTC_ERR_IO; } +/** + * @brief Connects this client's port to a remote port with the opposite direction. + * + * @param backend Backend handle. + * @param direction Local port direction. + * @param remote Borrowed remote port name. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_connect(xtc_backend *backend, xtc_port_direction direction, const char *remote) { return route(backend, direction, remote, 0); } +/** + * @brief Disconnects this client's port from a remote port. + * + * @param backend Backend handle. + * @param direction Local port direction. + * @param remote Borrowed remote port name. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction direction, const char *remote) { return route(backend, direction, remote, 1); } #else +/** + * @brief Reports that output name is unavailable without JACK support. + * + * @param b Unused in this build. + * @param c Unused in this build. + * @param out Unused in this build. + * @return XTC_ERR_UNSUPPORTED. + */ xtc_result xtc_backend_output_name(const xtc_backend *b, size_t c, const char **out) { (void)b; @@ -541,12 +731,25 @@ xtc_result xtc_backend_output_name(const xtc_backend *b, size_t c, const char ** return XTC_ERR_UNSUPPORTED; } +/** + * @brief Reports that panic is unavailable without JACK support. + * + * @param backend Unused in this build. + * @return XTC_ERR_UNSUPPORTED. + */ xtc_result xtc_backend_panic(xtc_backend *backend) { (void)backend; return XTC_ERR_UNSUPPORTED; } +/** + * @brief Reports that midi port name is unavailable without JACK support. + * + * @param backend Unused in this build. + * @param out Unused in this build. + * @return XTC_ERR_UNSUPPORTED. + */ xtc_result xtc_backend_midi_port_name(const xtc_backend *backend, const char **out) { (void)backend; @@ -554,6 +757,13 @@ xtc_result xtc_backend_midi_port_name(const xtc_backend *backend, const char **o return XTC_ERR_UNSUPPORTED; } +/** + * @brief Reports that prepare project is unavailable without JACK support. + * + * @param backend Unused in this build. + * @param project Unused in this build. + * @return XTC_ERR_UNSUPPORTED. + */ xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *project) { (void)backend; @@ -561,12 +771,25 @@ xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project * return XTC_ERR_UNSUPPORTED; } +/** + * @brief Returns an empty issue string in builds without JACK support. + * + * @param backend Unused in this build. + * @return Borrowed empty static string. + */ const char *xtc_backend_asset_issue(const xtc_backend *backend) { (void)backend; return ""; } +/** + * @brief Reports that set recorder is unavailable without JACK support. + * + * @param backend Unused in this build. + * @param recorder Unused in this build. + * @return XTC_ERR_UNSUPPORTED. + */ xtc_result xtc_backend_set_recorder(xtc_backend *backend, xtc_recorder *recorder) { (void)backend; @@ -574,6 +797,13 @@ xtc_result xtc_backend_set_recorder(xtc_backend *backend, xtc_recorder *recorder return XTC_ERR_UNSUPPORTED; } +/** + * @brief Reports that seek is unavailable without JACK support. + * + * @param backend Unused in this build. + * @param sample Unused in this build. + * @return XTC_ERR_UNSUPPORTED. + */ xtc_result xtc_backend_seek(xtc_backend *backend, uint64_t sample) { (void)backend; @@ -582,6 +812,13 @@ xtc_result xtc_backend_seek(xtc_backend *backend, uint64_t sample) } /* Keep core/offline builds available on machines without JACK headers. */ +/** + * @brief Validates open arguments and reports that JACK support is unavailable. + * + * @param config Configuration to check; must not be NULL. + * @param out_backend Receives NULL; must not be NULL. + * @return XTC_ERR_INVALID_ARGUMENT for missing pointers, otherwise XTC_ERR_UNSUPPORTED; clears a supplied output handle. + */ xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_backend) { if (out_backend == NULL) @@ -592,23 +829,48 @@ xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_ return XTC_ERR_UNSUPPORTED; } +/** + * @brief Accepts a NULL backend in builds without JACK support. + * + * @param backend Handle to close; NULL is accepted. + * @return XTC_OK for NULL, otherwise XTC_ERR_UNSUPPORTED. + */ xtc_result xtc_backend_close(xtc_backend *backend) { return backend == NULL ? XTC_OK : XTC_ERR_UNSUPPORTED; } +/** + * @brief Reports that start is unavailable without JACK support. + * + * @param backend Unused in this build. + * @return XTC_ERR_UNSUPPORTED. + */ xtc_result xtc_backend_start(xtc_backend *backend) { (void)backend; return XTC_ERR_UNSUPPORTED; } +/** + * @brief Reports that stop is unavailable without JACK support. + * + * @param backend Unused in this build. + * @return XTC_ERR_UNSUPPORTED. + */ xtc_result xtc_backend_stop(xtc_backend *backend) { (void)backend; return XTC_ERR_UNSUPPORTED; } +/** + * @brief Reports that submit is unavailable without JACK support. + * + * @param backend Unused in this build. + * @param graph Unused in this build. + * @return XTC_ERR_UNSUPPORTED. + */ xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph) { (void)backend; @@ -616,11 +878,23 @@ xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph) return XTC_ERR_UNSUPPORTED; } +/** + * @brief Performs no collection in builds without JACK support. + * + * @param backend Unused in this build. + */ void xtc_backend_collect(xtc_backend *backend) { (void)backend; } +/** + * @brief Reports that get status is unavailable without JACK support. + * + * @param backend Unused in this build. + * @param out_status Unused in this build. + * @return XTC_ERR_UNSUPPORTED. + */ xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *out_status) { (void)backend; @@ -628,6 +902,14 @@ xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status return XTC_ERR_UNSUPPORTED; } +/** + * @brief Reports that port name is unavailable without JACK support. + * + * @param backend Unused in this build. + * @param direction Unused in this build. + * @param out_name Unused in this build. + * @return XTC_ERR_UNSUPPORTED. + */ xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction direction, const char **out_name) { (void)backend; @@ -636,6 +918,15 @@ xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction return XTC_ERR_UNSUPPORTED; } +/** + * @brief Reports that visit ports is unavailable without JACK support. + * + * @param backend Unused in this build. + * @param direction Unused in this build. + * @param visitor Unused in this build. + * @param context Unused in this build. + * @return XTC_ERR_UNSUPPORTED. + */ xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction direction, xtc_backend_port_visitor visitor, void *context) { @@ -646,6 +937,14 @@ xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction dire return XTC_ERR_UNSUPPORTED; } +/** + * @brief Reports that connect is unavailable without JACK support. + * + * @param backend Unused in this build. + * @param direction Unused in this build. + * @param remote Unused in this build. + * @return XTC_ERR_UNSUPPORTED. + */ xtc_result xtc_backend_connect(xtc_backend *backend, xtc_port_direction direction, const char *remote) { (void)backend; @@ -654,6 +953,14 @@ xtc_result xtc_backend_connect(xtc_backend *backend, xtc_port_direction directio return XTC_ERR_UNSUPPORTED; } +/** + * @brief Reports that disconnect is unavailable without JACK support. + * + * @param backend Unused in this build. + * @param direction Unused in this build. + * @param remote Unused in this build. + * @return XTC_ERR_UNSUPPORTED. + */ xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction direction, const char *remote) { (void)backend; diff --git a/src/core/xtc.c b/src/core/xtc.c index 5a4fa2f..344d0cc 100644 --- a/src/core/xtc.c +++ b/src/core/xtc.c @@ -6,6 +6,12 @@ struct xtc_app { unsigned int reserved; }; +/** + * @brief Returns a short human-readable name for a result code. + * + * @param result Result code to describe. + * @return Borrowed static string; never NULL. + */ const char *xtc_result_string(xtc_result result) { switch (result) { @@ -34,6 +40,12 @@ const char *xtc_result_string(xtc_result result) } } +/** + * @brief Allocates an application handle. + * + * @param out_app Receives the newly allocated app; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_app_create(xtc_app **out_app) { xtc_app *app; @@ -52,6 +64,11 @@ xtc_result xtc_app_create(xtc_app **out_app) return XTC_OK; } +/** + * @brief Releases an application handle; accepts NULL. + * + * @param app Application handle. + */ void xtc_app_destroy(xtc_app *app) { free(app); diff --git a/src/dsp/gain.c b/src/dsp/gain.c index c4b92b3..af3c449 100644 --- a/src/dsp/gain.c +++ b/src/dsp/gain.c @@ -1,6 +1,16 @@ #include -/* Kept as a separately testable DSP primitive. */ +/** + * @brief Applies a constant gain to a block of mono samples. + * + * This is kept as a small, separately testable DSP primitive. The input and + * output buffers may alias. A NULL buffer makes the call a no-op. + * + * @param input Source samples; NULL is accepted. + * @param output Destination samples; NULL is accepted. + * @param frame_count Number of samples to process. + * @param gain Linear gain factor applied to every sample. + */ void xtc_dsp_gain_process(const float *input, float *output, size_t frame_count, float gain) { size_t frame; diff --git a/src/engine/offline_engine.c b/src/engine/offline_engine.c index 1dee018..1bb5e3b 100644 --- a/src/engine/offline_engine.c +++ b/src/engine/offline_engine.c @@ -18,6 +18,11 @@ struct xtc_plan { xtc_native **native; }; +/** + * @brief Releases a compiled plan and all resources it owns. + * + * @param plan Plan to destroy; NULL is accepted. + */ void xtc_plan_destroy(xtc_plan *plan) { if (plan == NULL) @@ -39,6 +44,15 @@ void xtc_plan_destroy(xtc_plan *plan) free(plan); } +/** + * @brief Allocates an independent execution plan with preallocated processing buffers. + * + * @param graph Graph model; borrowed for this call. + * @param sample_rate Sample rate in hertz. + * @param max_frames Maximum frames per processing block. + * @param out_plan Receives the newly allocated plan; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate, size_t max_frames, xtc_plan **out_plan) { xtc_plan *plan; @@ -118,6 +132,12 @@ xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate, size_t return XTC_OK; } +/** + * @brief Resets oscillator phases, meters, and native processor state without allocating. + * + * @param plan Compiled graph plan and its single-owner runtime state. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_plan_reset(xtc_plan *plan) { if (plan == NULL) @@ -133,6 +153,14 @@ xtc_result xtc_plan_reset(xtc_plan *plan) return XTC_OK; } +/** + * @brief Copies the last block's absolute peak for a meter node. + * + * @param plan Compiled graph plan and its single-owner runtime state. + * @param node_id Stable node identifier. + * @param out_peak Receives the peak. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_plan_meter_peak(const xtc_plan *plan, size_t node_id, float *out_peak) { if (plan == NULL || out_peak == NULL) @@ -150,6 +178,20 @@ xtc_result xtc_plan_meter_peak(const xtc_plan *plan, size_t node_id, float *out_ return XTC_OK; } +/** + * @brief Processes a block with separate stereo timeline and instrument sources. + * + * @param plan Compiled graph plan and its single-owner runtime state. + * @param input Borrowed input sample buffer. + * @param timeline Borrowed timeline source buffer; NULL means silence. + * @param timeline_right Borrowed right timeline source buffer; NULL means silence. + * @param instrument Borrowed instrument source buffer; NULL means silence. + * @param instrument_right Borrowed right instrument source buffer; NULL means silence. + * @param output Destination buffer with room for the requested frames. + * @param right Destination right-channel buffer; NULL selects mono rendering. + * @param frame_count Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, const float *timeline, const float *timeline_right, const float *instrument, const float *instrument_right, float *output, float *right, size_t frame_count) @@ -231,6 +273,16 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, const flo return XTC_OK; } +/** + * @brief Compiles at 48000 Hz, applies input gain, renders once, and releases temporary state. + * + * @param graph Graph model; borrowed for this call. + * @param input Borrowed input sample buffer. + * @param output Destination buffer with room for the requested frames. + * @param frame_count Number of audio frames. + * @param input_gain Finite linear gain applied to external input before processing. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_process_offline(xtc_graph *graph, const float *input, float *output, size_t frame_count, float input_gain) { @@ -247,17 +299,47 @@ xtc_result xtc_graph_process_offline(xtc_graph *graph, const float *input, float return result; } +/** + * @brief Processes a mono block with external input and no timeline or instrument sources. + * + * @param plan Compiled graph plan and its single-owner runtime state. + * @param input Borrowed input sample buffer. + * @param output Destination buffer with room for the requested frames. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_plan_process(xtc_plan *plan, const float *input, float *output, size_t frames) { return xtc_plan_process_audio(plan, input, NULL, output, frames); } +/** + * @brief Processes a mono block with timeline audio and no instrument source. + * + * @param plan Compiled graph plan and its single-owner runtime state. + * @param input Borrowed input sample buffer. + * @param timeline Borrowed timeline source buffer; NULL means silence. + * @param output Destination buffer with room for the requested frames. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_plan_process_audio(xtc_plan *plan, const float *input, const float *timeline, float *output, size_t frames) { return xtc_plan_process_sources(plan, input, timeline, NULL, output, frames); } +/** + * @brief Processes a mono block with separate timeline and instrument sources. + * + * @param p Compiled graph plan and its single-owner runtime state. + * @param in Borrowed mono input buffer; NULL means silence. + * @param audio Borrowed timeline source buffer; NULL means silence. + * @param midi Borrowed instrument source buffer; NULL means silence. + * @param out Destination sample buffer with room for the requested frames. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_plan_process_sources(xtc_plan *p, const float *in, const float *audio, const float *midi, float *out, size_t frames) { diff --git a/src/engine/realtime.c b/src/engine/realtime.c index 1fbbc75..6a9dd24 100644 --- a/src/engine/realtime.c +++ b/src/engine/realtime.c @@ -14,6 +14,21 @@ struct xtc_realtime { unsigned int submitted_revision, pending_revision, active_revision; }; +/** + * @brief Creates a realtime engine for single-producer, single-consumer use. + * + * The engine starts without an active plan, so processing produces silence + * until a graph is submitted. The caller owns the new engine and must release + * it with xtc_realtime_destroy() after stopping the audio consumer. + * + * @param sample_rate Sample rate in Hz; must be finite and positive. + * @param max_frames Maximum frames per process call, from 1 to 65536. + * @param out_engine Receives the new engine on success and NULL on failure; + * must not be NULL. + * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for invalid limits, + * XTC_ERR_UNSUPPORTED if required atomics are not lock-free, or + * XTC_ERR_OUT_OF_MEMORY if allocation fails. + */ xtc_result xtc_realtime_create(double sample_rate, size_t max_frames, xtc_realtime **out_engine) { xtc_realtime *engine; @@ -33,6 +48,11 @@ xtc_result xtc_realtime_create(double sample_rate, size_t max_frames, xtc_realti return XTC_OK; } +/** + * @brief Frees acknowledged retired plans on the control thread. + * + * @param engine Realtime engine handle. + */ void xtc_realtime_collect(xtc_realtime *engine) { xtc_plan *retired; @@ -42,6 +62,11 @@ void xtc_realtime_collect(xtc_realtime *engine) xtc_plan_destroy(retired); } +/** + * @brief Releases the engine after the audio consumer has stopped; accepts NULL. + * + * @param engine Realtime engine handle. + */ void xtc_realtime_destroy(xtc_realtime *engine) { if (engine == NULL) @@ -53,6 +78,13 @@ void xtc_realtime_destroy(xtc_realtime *engine) free(engine); } +/** + * @brief Compiles and publishes a graph to the single pending-plan slot. + * + * @param engine Realtime engine handle. + * @param graph Graph model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_realtime_submit(xtc_realtime *engine, const xtc_graph *graph) { xtc_plan *plan = NULL; @@ -74,6 +106,13 @@ xtc_result xtc_realtime_submit(xtc_realtime *engine, const xtc_graph *graph) return XTC_OK; } +/** + * @brief Copies processing counters, plan revisions, and the last processing result. + * + * @param engine Realtime engine handle. + * @param out_status Receives the status. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_realtime_get_status(const xtc_realtime *engine, xtc_realtime_status *out_status) { if (engine == NULL || out_status == NULL) @@ -85,6 +124,20 @@ xtc_result xtc_realtime_get_status(const xtc_realtime *engine, xtc_realtime_stat return XTC_OK; } +/** + * @brief Adopts a pending plan and processes stereo sources without allocation or locking. + * + * @param engine Realtime engine handle. + * @param input Borrowed input sample buffer. + * @param timeline Borrowed timeline source buffer; NULL means silence. + * @param timeline_right Borrowed right timeline source buffer; NULL means silence. + * @param instrument Borrowed instrument source buffer; NULL means silence. + * @param instrument_right Borrowed right instrument source buffer; NULL means silence. + * @param output Destination buffer with room for the requested frames. + * @param right Destination right-channel buffer; NULL selects mono rendering. + * @param frame_count Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_realtime_process_stereo(xtc_realtime *engine, const float *input, const float *timeline, const float *timeline_right, const float *instrument, const float *instrument_right, float *output, float *right, size_t frame_count) @@ -132,17 +185,47 @@ xtc_result xtc_realtime_process_stereo(xtc_realtime *engine, const float *input, return result; } +/** + * @brief Processes a mono block with external input only. + * + * @param engine Realtime engine handle. + * @param input Borrowed input sample buffer. + * @param output Destination buffer with room for the requested frames. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_realtime_process(xtc_realtime *engine, const float *input, float *output, size_t frames) { return xtc_realtime_process_audio(engine, input, NULL, output, frames); } +/** + * @brief Processes a mono block with timeline audio and no instrument source. + * + * @param engine Realtime engine handle. + * @param input Borrowed input sample buffer. + * @param timeline Borrowed timeline source buffer; NULL means silence. + * @param output Destination buffer with room for the requested frames. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_realtime_process_audio(xtc_realtime *engine, const float *input, const float *timeline, float *output, size_t frames) { return xtc_realtime_process_sources(engine, input, timeline, NULL, output, frames); } +/** + * @brief Processes mono timeline and instrument sources through the realtime engine. + * + * @param p Realtime engine handle. + * @param in Borrowed mono input buffer; NULL means silence. + * @param audio Borrowed timeline source buffer; NULL means silence. + * @param midi Borrowed instrument source buffer; NULL means silence. + * @param out Destination sample buffer with room for the requested frames. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_realtime_process_sources(xtc_realtime *p, const float *in, const float *audio, const float *midi, float *out, size_t frames) { diff --git a/src/graph/graph.c b/src/graph/graph.c index c1f71bc..b8fb64d 100644 --- a/src/graph/graph.c +++ b/src/graph/graph.c @@ -3,6 +3,13 @@ #include #include +/** + * @brief Allocates an empty graph with the requested node capacity. + * + * @param max_nodes Maximum number of graph nodes. + * @param out_graph Receives the newly allocated graph; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_create(size_t max_nodes, xtc_graph **out_graph) { xtc_graph *graph; @@ -25,6 +32,11 @@ xtc_result xtc_graph_create(size_t max_nodes, xtc_graph **out_graph) return XTC_OK; } +/** + * @brief Releases graph storage; accepts NULL. + * + * @param graph Graph model; borrowed for this call. + */ void xtc_graph_destroy(xtc_graph *graph) { if (graph != NULL) { @@ -34,6 +46,13 @@ void xtc_graph_destroy(xtc_graph *graph) } } +/** + * @brief Finds a graph's dense node index by stable identifier. + * + * @param graph Graph model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @return Dense node index, or SIZE_MAX if absent. + */ static size_t node_index(const xtc_graph *graph, size_t id) { size_t i; @@ -43,6 +62,15 @@ static size_t node_index(const xtc_graph *graph, size_t id) return SIZE_MAX; } +/** + * @brief Appends a node after checking type, value, capacity, and identifier availability. + * + * @param graph Graph model; borrowed for this call. + * @param type Node type to create. + * @param value Value to encode, write, or apply. + * @param out_node_id Receives the node id. + * @return XTC_OK on success, or an error code describing the failure. + */ static xtc_result add_node(xtc_graph *graph, xtc_node_type type, float value, size_t *out_node_id) { if (graph == NULL || out_node_id == NULL || !isfinite(value)) @@ -56,11 +84,27 @@ static xtc_result add_node(xtc_graph *graph, xtc_node_type type, float value, si return XTC_OK; } +/** + * @brief Adds a gain node with a finite linear gain value. + * + * @param graph Graph model; borrowed for this call. + * @param gain Linear gain, 0 to 4 for track controls. + * @param out_node_id Receives the node id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_add_gain(xtc_graph *graph, float gain, size_t *out_node_id) { return add_node(graph, XTC_NODE_GAIN, gain, out_node_id); } +/** + * @brief Adds a sine oscillator node with a positive frequency in hertz. + * + * @param graph Graph model; borrowed for this call. + * @param frequency Oscillator frequency in hertz. + * @param out_node_id Receives the node id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_add_oscillator(xtc_graph *graph, float frequency, size_t *out_node_id) { if (frequency < 0.0f) @@ -68,26 +112,63 @@ xtc_result xtc_graph_add_oscillator(xtc_graph *graph, float frequency, size_t *o return add_node(graph, XTC_NODE_OSCILLATOR, frequency, out_node_id); } +/** + * @brief Adds a pass-through node that records the last block's absolute peak. + * + * @param graph Graph model; borrowed for this call. + * @param out_node_id Receives the node id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_add_meter(xtc_graph *graph, size_t *out_node_id) { return add_node(graph, XTC_NODE_METER, 0.0f, out_node_id); } +/** + * @brief Adds a source node for the separately rendered MIDI instrument buffer. + * + * @param graph Graph model; borrowed for this call. + * @param out_node_id Receives the node id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_add_instrument(xtc_graph *graph, size_t *out_node_id) { return add_node(graph, XTC_NODE_INSTRUMENT, 0.0f, out_node_id); } +/** + * @brief Adds a source node for the separately rendered timeline audio buffer. + * + * @param graph Graph model; borrowed for this call. + * @param out_node_id Receives the node id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_add_audio(xtc_graph *graph, size_t *out_node_id) { return add_node(graph, XTC_NODE_AUDIO, 0.0f, out_node_id); } +/** + * @brief Adds an explicit output node included in the final mix. + * + * @param graph Graph model; borrowed for this call. + * @param out_node_id Receives the node id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_add_output(xtc_graph *graph, size_t *out_node_id) { return add_node(graph, XTC_NODE_OUTPUT, 0.0f, out_node_id); } +/** + * @brief Copies the direction, signal type, and channel count of a node port. + * + * @param graph Graph model; borrowed for this call. + * @param node_id Stable node identifier. + * @param port_id Node port identifier. + * @param out_port Receives the port. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_get_port(const xtc_graph *graph, size_t node_id, size_t port_id, xtc_port_info *out_port) { xtc_node_type type; @@ -108,6 +189,13 @@ xtc_result xtc_graph_get_port(const xtc_graph *graph, size_t node_id, size_t por return XTC_OK; } +/** + * @brief Validates the graph and writes dense node indexes in topological order. + * + * @param graph Graph model; borrowed for this call. + * @param order Receives dense node indexes in topological order; must hold the graph node count. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_order(const xtc_graph *graph, size_t *order) { size_t indegree[XTC_MAX_NODES] = {0U}; @@ -132,12 +220,28 @@ xtc_result xtc_graph_order(const xtc_graph *graph, size_t *order) return tail == graph->node_count ? XTC_OK : XTC_ERR_GRAPH_CYCLE; } +/** + * @brief Checks graph validity by attempting a topological ordering. + * + * @param graph Graph model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_validate(const xtc_graph *graph) { size_t order[XTC_MAX_NODES]; return xtc_graph_order(graph, order); } +/** + * @brief Adds a validated audio connection while preserving an acyclic graph. + * + * @param graph Graph model; borrowed for this call. + * @param source_node Stable source node identifier. + * @param source_port Source output port identifier. + * @param destination_node Stable destination node identifier. + * @param destination_port Destination input port identifier. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_connect_ports(xtc_graph *graph, size_t source_node, size_t source_port, size_t destination_node, size_t destination_port) { @@ -167,11 +271,27 @@ xtc_result xtc_graph_connect_ports(xtc_graph *graph, size_t source_node, size_t return result; } +/** + * @brief Connects a source's output port 1 to a destination's input port 0. + * + * @param graph Graph model; borrowed for this call. + * @param source_node Stable source node identifier. + * @param destination_node Stable destination node identifier. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_connect(xtc_graph *graph, size_t source_node, size_t destination_node) { return xtc_graph_connect_ports(graph, source_node, 1U, destination_node, 0U); } +/** + * @brief Copies the graph's node and connection counts. + * + * @param graph Graph model; borrowed for this call. + * @param out_nodes Receives the nodes. + * @param out_connections Receives the connections. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_get_counts(const xtc_graph *graph, size_t *out_nodes, size_t *out_connections) { if (graph == NULL || out_nodes == NULL || out_connections == NULL) @@ -181,6 +301,14 @@ xtc_result xtc_graph_get_counts(const xtc_graph *graph, size_t *out_nodes, size_ return XTC_OK; } +/** + * @brief Copies node metadata selected by dense list index. + * + * @param graph Graph model; borrowed for this call. + * @param index Zero-based index in the dense list. + * @param out_node Receives the node. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_get_node(const xtc_graph *graph, size_t index, xtc_node_info *out_node) { if (graph == NULL || out_node == NULL) @@ -193,6 +321,14 @@ xtc_result xtc_graph_get_node(const xtc_graph *graph, size_t index, xtc_node_inf return XTC_OK; } +/** + * @brief Copies connection metadata selected by dense list index. + * + * @param graph Graph model; borrowed for this call. + * @param index Zero-based index in the dense list. + * @param out_connection Receives the connection. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_get_connection(const xtc_graph *graph, size_t index, xtc_connection_info *out_connection) { if (graph == NULL || out_connection == NULL) @@ -206,6 +342,13 @@ xtc_result xtc_graph_get_connection(const xtc_graph *graph, size_t index, xtc_co return XTC_OK; } +/** + * @brief Sets whether rendering requires explicit output nodes rather than terminal-node fallback. + * + * @param graph Graph model; borrowed for this call. + * @param required One to require explicit output nodes, zero to allow terminal-node fallback. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_require_output_nodes(xtc_graph *graph, int required) { if (graph == NULL || (required != 0 && required != 1)) @@ -214,6 +357,13 @@ xtc_result xtc_graph_require_output_nodes(xtc_graph *graph, int required) return XTC_OK; } +/** + * @brief Removes a node and all connections touching it. + * + * @param graph Graph model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @return XTC_OK on success, or an error code describing the failure. + */ static xtc_result remove_node(xtc_graph *graph, size_t id) { size_t index = node_index(graph, id), i, count = 0U; @@ -235,6 +385,13 @@ static xtc_result remove_node(xtc_graph *graph, size_t id) return XTC_OK; } +/** + * @brief Removes the connection matching all source and destination port fields. + * + * @param graph Graph model; borrowed for this call. + * @param connection Connection identifying both node and port pairs. + * @return XTC_OK on success, or an error code describing the failure. + */ static xtc_result disconnect_ports(xtc_graph *graph, const xtc_connection_info *connection) { size_t i; @@ -260,6 +417,15 @@ static xtc_result disconnect_ports(xtc_graph *graph, const xtc_connection_info * return XTC_ERR_NOT_FOUND; } +/** + * @brief Applies an edit to a new graph copy, leaving the source unchanged. + * + * @param source Graph model; borrowed for this call. + * @param edit Graph edit command. + * @param out_graph Receives the newly allocated graph; caller owns it on success. + * @param out_added_id Receives the added id. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_apply_edit(const xtc_graph *source, const xtc_graph_edit *edit, xtc_graph **out_graph, size_t *out_added_id) { @@ -331,6 +497,15 @@ xtc_result xtc_graph_apply_edit(const xtc_graph *source, const xtc_graph_edit *e return XTC_OK; } +/** + * @brief Adds a validated native effect or source descriptor to the graph. + * + * @param graph Graph model; borrowed for this call. + * @param descriptor Native processor descriptor. + * @param value Value to encode, write, or apply. + * @param out Receives the operation result. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_add_native(xtc_graph *graph, const xtc_native_descriptor *descriptor, float value, size_t *out) { xtc_result result = xtc_native_validate(descriptor); @@ -344,6 +519,14 @@ xtc_result xtc_graph_add_native(xtc_graph *graph, const xtc_native_descriptor *d return result; } +/** + * @brief Copies the native descriptor of a node selected by stable identifier. + * + * @param graph Graph model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @param out Receives the operation result. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_get_native(const xtc_graph *graph, size_t id, xtc_native_descriptor *out) { size_t i; diff --git a/src/graph/graph_internal.h b/src/graph/graph_internal.h index 23ae3b8..510e19b 100644 --- a/src/graph/graph_internal.h +++ b/src/graph/graph_internal.h @@ -26,5 +26,12 @@ struct xtc_graph { xtc_edge *edges; }; +/** + * @brief Validates the graph and writes dense node indexes in topological order. + * + * @param graph Graph model; borrowed for this call. + * @param order Receives dense node indexes in topological order; must hold the graph node count. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_graph_order(const xtc_graph *graph, size_t *order); #endif diff --git a/src/gui/graph_widget.cpp b/src/gui/graph_widget.cpp index 7c93ee1..dfa71ce 100644 --- a/src/gui/graph_widget.cpp +++ b/src/gui/graph_widget.cpp @@ -9,6 +9,12 @@ namespace { constexpr int node_width = 150, node_height = 74; +/** + * @brief Returns the display label for a node type. + * + * @param type Node type to describe. + * @return Borrowed static string; never NULL, falling back to "Unknown". + */ const char *node_name(xtc_node_type type) { switch (type) { @@ -31,6 +37,21 @@ const char *node_name(xtc_node_type type) } } // namespace +/** + * @brief Constructs the widget and initializes its cached layout from the graph. + * + * The graph, handler, context, and feedback box are stored without taking + * ownership. Layout population is delegated to set_graph(). + * + * @param left Left coordinate in FLTK screen space. + * @param top Top coordinate in FLTK screen space. + * @param width Widget width in pixels. + * @param height Widget height in pixels. + * @param graph Borrowed graph snapshot; must outlive the widget's use of it. + * @param handler Edit callback invoked for every graph mutation. + * @param context Opaque pointer passed back to @p handler; may be NULL. + * @param feedback_box Borrowed label used to report edit results. + */ GraphWidget::GraphWidget(int left, int top, int width, int height, const xtc_graph *graph, EditHandler handler, void *context, Fl_Box *feedback_box) : Fl_Widget(left, top, width, height), graph_(graph), handler_(handler), context_(context), feedback_(feedback_box) @@ -38,6 +59,16 @@ GraphWidget::GraphWidget(int left, int top, int width, int height, const xtc_gra set_graph(graph); } +/** + * @brief Replaces the graph snapshot and rebuilds cached node positions. + * + * Existing positions are preserved by node ID. New nodes get grid slots that do + * not overlap previously known nodes. The selection is dropped when the selected + * node or wire is no longer present. The widget does not take ownership of the + * graph. + * + * @param graph Borrowed graph that must outlive the widget's use of it. + */ void GraphWidget::set_graph(const xtc_graph *graph) { graph_ = graph; @@ -86,6 +117,14 @@ void GraphWidget::set_graph(const xtc_graph *graph) redraw(); } +/** + * @brief Looks up the cached position of a node. + * + * @param id Stable node identifier. + * @param left Receives the left position in widget-local coordinates. + * @param top Receives the top position in widget-local coordinates. + * @return True when the node has a cached position, false otherwise. + */ bool GraphWidget::node_position(size_t id, int &left, int &top) const { for (size_t i = 0U; i < count_; ++i) @@ -97,6 +136,16 @@ bool GraphWidget::node_position(size_t id, int &left, int &top) const return false; } +/** + * @brief Moves a node within the widget, clamped to the visible bounds. + * + * The node keeps a 12-pixel margin from the widget edges. + * + * @param id Stable node identifier. + * @param left Requested left position in widget-local coordinates. + * @param top Requested top position in widget-local coordinates. + * @return True when the node has a cached position, false otherwise. + */ bool GraphWidget::move_node(size_t id, int left, int top) { for (size_t i = 0U; i < count_; ++i) @@ -109,6 +158,12 @@ bool GraphWidget::move_node(size_t id, int left, int top) return false; } +/** + * @brief Selects a node by ID and clears any wire selection. + * + * @param id Stable node identifier. + * @return True when the node has a cached position, false otherwise. + */ bool GraphWidget::select_node(size_t id) { int left, top; @@ -120,6 +175,13 @@ bool GraphWidget::select_node(size_t id) return true; } +/** + * @brief Selects a wire between two nodes and clears the node selection. + * + * @param source Source node identifier. + * @param destination Destination node identifier. + * @return True when a matching connection exists, false otherwise. + */ bool GraphWidget::select_connection(size_t source, size_t destination) { size_t nodes, edges; @@ -139,6 +201,12 @@ bool GraphWidget::select_connection(size_t source, size_t destination) return false; } +/** + * @brief Shows the outcome of an edit in the feedback label. + * + * @param result Result returned by the edit handler. + * @param success Text shown when @p result is XTC_OK; must be valid then and is ignored otherwise. + */ void GraphWidget::feedback(xtc_result result, const char *success) { char text[256]; @@ -153,6 +221,14 @@ void GraphWidget::feedback(xtc_result result, const char *success) feedback_->copy_label(text); } +/** + * @brief Requests creation of a node through the edit handler. + * + * Selects the new node when the edit succeeds. + * + * @param type Kind of node to add. + * @return Result reported by the edit handler. + */ xtc_result GraphWidget::add_node(xtc_node_type type) { xtc_graph_edit edit{}; @@ -167,6 +243,12 @@ xtc_result GraphWidget::add_node(xtc_node_type type) return result; } +/** + * @brief Requests a connection between two ports through the edit handler. + * + * @param connection Connection describing the source and destination node/port pairs. + * @return Result reported by the edit handler. + */ xtc_result GraphWidget::connect_ports(const xtc_connection_info &connection) { xtc_graph_edit edit{}; @@ -177,6 +259,12 @@ xtc_result GraphWidget::connect_ports(const xtc_connection_info &connection) return result; } +/** + * @brief Requests removal of the selected wire or node through the edit handler. + * + * @return Result reported by the edit handler, or XTC_ERR_INVALID_STATE when + * nothing is selected. + */ xtc_result GraphWidget::delete_selected() { xtc_graph_edit edit{}; @@ -195,6 +283,15 @@ xtc_result GraphWidget::delete_selected() return result; } +/** + * @brief Computes the widget-local center of a node port. + * + * @param id Stable node identifier. + * @param port Port index; 0 selects the input and 1 the output. + * @param left Receives the port center x coordinate. + * @param top Receives the port center y coordinate. + * @return True when the node and port exist, false otherwise. + */ bool GraphWidget::port_position(size_t id, size_t port, int &left, int &top) const { xtc_port_info info{}; @@ -205,6 +302,18 @@ bool GraphWidget::port_position(size_t id, size_t port, int &left, int &top) con return true; } +/** + * @brief Finds the port nearest to a widget-local point. + * + * Searches nodes in reverse draw order and accepts points within 9 pixels of a + * port center. + * + * @param left Widget-local x coordinate to test. + * @param top Widget-local y coordinate to test. + * @param node Receives the matching node ID when true is returned. + * @param port Receives the matching port index (0 input, 1 output) when true is returned. + * @return True when a port is within range, false otherwise. + */ bool GraphWidget::hit_port(int left, int top, size_t &node, size_t &port) const { for (size_t i = count_; i > 0U; --i) @@ -220,6 +329,9 @@ bool GraphWidget::hit_port(int left, int top, size_t &node, size_t &port) const return false; } +/** + * @brief Draws the background, wires, nodes, ports, and any pending wire. + */ void GraphWidget::draw() { fl_push_clip(x(), y(), w(), h()); @@ -286,6 +398,13 @@ void GraphWidget::draw() fl_pop_clip(); } +/** + * @brief Handles mouse and keyboard events for selection, dragging, and wiring. + * + * @param event FLTK event code. + * @return 1 when the event was consumed, 0 for unsupported left-button input, + * or the result of Fl_Widget::handle() for unhandled events. + */ int GraphWidget::handle(int event) { const int left = Fl::event_x() - x(), top = Fl::event_y() - y(); diff --git a/src/gui/graph_widget.hpp b/src/gui/graph_widget.hpp index e40383e..c33441e 100644 --- a/src/gui/graph_widget.hpp +++ b/src/gui/graph_widget.hpp @@ -9,10 +9,36 @@ class Fl_Box; /* GUI positions/selection only. Domain snapshots and edits stay in the C API. */ class GraphWidget : public Fl_Widget { public: + /** + * @brief Applies a graph edit using an opaque context and optional added-node output. + * + * The handler receives the context, edit command, and output identifier in that order. + * @return XTC_OK on success, or an edit error. + */ using EditHandler = xtc_result (*)(void *, const xtc_graph_edit &, size_t *); + /** + * @brief Constructs a graph widget bound to a graph and an edit callback. + * + * The widget stores the graph, handler, context, and feedback box without + * taking ownership of any of them. + * + * @param x Left coordinate in FLTK screen space. + * @param y Top coordinate in FLTK screen space. + * @param width Widget width in pixels. + * @param height Widget height in pixels. + * @param graph Borrowed graph snapshot; must outlive the widget's use of it. + * @param handler Edit callback invoked for every graph mutation. + * @param context Opaque pointer passed back to @p handler; may be NULL. + * @param feedback Borrowed label used to report edit results; must outlive the widget. + */ GraphWidget(int x, int y, int width, int height, const xtc_graph *graph, EditHandler handler, void *context, Fl_Box *feedback); + /** + * @brief Clears cached node positions, selection, and interaction state. + * + * Does not change the borrowed graph pointer or the graph's own layout. + */ void reset_layout() { count_ = 0; @@ -20,23 +46,105 @@ class GraphWidget : public Fl_Widget { edge_selected_ = dragging_ = wiring_ = false; } + /** + * @brief Replaces the graph snapshot and rebuilds cached node positions. + * + * Existing positions are preserved by node ID; new nodes are placed on a grid + * that avoids overlapping previously known nodes. Selections that no longer + * exist are cleared. The widget does not take ownership of @p graph. + * + * @param graph Borrowed graph that must outlive the widget's use of it. + */ void set_graph(const xtc_graph *graph); + /** + * @brief Returns the graph snapshot currently displayed. + * + * The returned pointer is borrowed and valid for GUI thread use only. + * + * @return Borrowed graph pointer; may be NULL when none was supplied. + */ const xtc_graph *graph() const { return graph_; } // Borrowed, GUI thread only. + /** + * @brief Requests creation of a node through the edit handler. + * + * Selects the new node when the edit succeeds. + * + * @param type Kind of node to add. + * @return Result reported by the edit handler. + */ xtc_result add_node(xtc_node_type type); + + /** + * @brief Requests a connection between two ports through the edit handler. + * + * @param connection Connection describing the source and destination node/port pairs. + * @return Result reported by the edit handler. + */ xtc_result connect_ports(const xtc_connection_info &connection); + + /** + * @brief Requests removal of the selected wire or node through the edit handler. + * + * @return Result reported by the edit handler, or XTC_ERR_INVALID_STATE when + * nothing is selected. + */ xtc_result delete_selected(); + + /** + * @brief Selects a node by ID and clears any wire selection. + * + * @param id Stable node identifier. + * @return True when the node has a cached position, false otherwise. + */ bool select_node(size_t id); + + /** + * @brief Selects a wire between two nodes and clears the node selection. + * + * @param source Source node identifier. + * @param destination Destination node identifier. + * @return True when a matching connection exists, false otherwise. + */ bool select_connection(size_t source, size_t destination); + + /** + * @brief Moves a node within the widget, clamped to the visible bounds. + * + * @param id Stable node identifier. + * @param left Requested left position in widget-local coordinates. + * @param top Requested top position in widget-local coordinates. + * @return True when the node has a cached position, false otherwise. + */ bool move_node(size_t id, int left, int top); + + /** + * @brief Looks up the cached position of a node. + * + * @param id Stable node identifier. + * @param left Receives the left position in widget-local coordinates. + * @param top Receives the top position in widget-local coordinates. + * @return True when the node has a cached position, false otherwise. + */ bool node_position(size_t id, int &left, int &top) const; + + /** + * @brief Handles mouse and keyboard events for selection, dragging, and wiring. + * + * @param event FLTK event code. + * @return 1 when the event was consumed, 0 for unsupported left-button input, + * or the result of Fl_Widget::handle() for unhandled events. + */ int handle(int event) override; protected: + /** + * @brief Draws the background, wires, nodes, ports, and any pending wire. + */ void draw() override; private: @@ -57,8 +165,37 @@ class GraphWidget : public Fl_Widget { bool dragging_ = false, wiring_ = false; size_t wire_node_ = SIZE_MAX, wire_port_ = 0U; int drag_x_ = 0, drag_y_ = 0, mouse_x_ = 0, mouse_y_ = 0; + /** + * @brief Shows the outcome of an edit in the feedback label. + * + * @param result Result returned by the edit handler. + * @param success Text shown when @p result is XTC_OK; must be valid then and is ignored otherwise. + */ void feedback(xtc_result result, const char *success); + + /** + * @brief Finds the port nearest to a widget-local point. + * + * Searches nodes in reverse draw order and accepts points within 9 pixels of a + * port center. + * + * @param left Widget-local x coordinate to test. + * @param top Widget-local y coordinate to test. + * @param node Receives the matching node ID when true is returned. + * @param port Receives the matching port index (0 input, 1 output) when true is returned. + * @return True when a port is within range, false otherwise. + */ bool hit_port(int left, int top, size_t &node, size_t &port) const; + + /** + * @brief Computes the widget-local center of a node port. + * + * @param id Stable node identifier. + * @param port Port index; 0 selects the input and 1 the output. + * @param left Receives the port center x coordinate. + * @param top Receives the port center y coordinate. + * @return True when the node and port exist, false otherwise. + */ bool port_position(size_t id, size_t port, int &left, int &top) const; }; #endif diff --git a/src/gui/main.cpp b/src/gui/main.cpp index 7a987ad..f5c3442 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -6,6 +6,13 @@ #include "xtc/xtc_version.h" #include +/** + * @brief Runs the FLTK application and reports command-line or startup failures. + * + * @param argc Number of command-line arguments including the program name. + * @param argv Command-line argument vector. + * @return EXIT_SUCCESS after the event loop exits, or EXIT_FAILURE for invalid usage or startup failure. + */ int main(int argc, char **argv) { if (argc == 2 && !std::strcmp(argv[1], "--version")) { diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index ea52eed..c2fa767 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -19,6 +19,15 @@ #include namespace { +/** + * @brief Updates a box label only when the text differs. + * + * Avoids redundant relayout and redraw when status text is refreshed + * repeatedly. The box takes a copy of the text. + * + * @param box Box whose label is updated; must not be NULL. + * @param text New label text; must not be NULL. + */ void set_label(Fl_Box *box, const char *text) { if (box->label() == nullptr || std::strcmp(box->label(), text) != 0) @@ -26,6 +35,16 @@ void set_label(Fl_Box *box, const char *text) } } // namespace +/** + * @brief Constructs the window, its widgets, and the initial project. + * + * Creates an empty project and undo history, opens the audio backend, and + * schedules the periodic status timer. Throws std::runtime_error when the + * initial project or history cannot be created. + * + * @param prompt_unsaved Whether open, new, and close actions ask before + * discarding unsaved changes. + */ MainWindow::MainWindow(bool prompt_unsaved) : Fl_Double_Window(1000, 680, XTC_PROJECT_NAME " — modular audio workstation"), prompt_unsaved_(prompt_unsaved) { @@ -229,6 +248,12 @@ MainWindow::MainWindow(bool prompt_unsaved) Fl::add_timeout(0.1, timer_callback, this); } +/** + * @brief Removes the status timer and releases backend and recording state. + * + * A backend that fails to close is intentionally retained with its recorder + * until process exit so active callbacks never observe freed state. + */ MainWindow::~MainWindow() { Fl::remove_timeout(timer_callback, this); @@ -244,6 +269,13 @@ MainWindow::~MainWindow() } } +/** + * @brief Shows an operation failure and a context-sensitive hint. + * + * @param operation Human-readable operation name; names containing + * "Project" select a file and permission hint. + * @param result Failing result code whose name is included in the message. + */ void MainWindow::report_error(const char *operation, xtc_result result) { char text[384]; @@ -255,6 +287,20 @@ void MainWindow::report_error(const char *operation, xtc_result result) set_label(error_, text); } +/** + * @brief Edit handler passed to the modular graph widget. + * + * Applies the edit to a candidate graph, clones the project around it, and + * submits the new graph to the engine when connected. The live project is + * replaced only after every step succeeds. + * + * @param context MainWindow * passed as the graph widget owner context. + * @param edit Graph edit to apply. + * @param out_added_id Optional output receiving the new node ID when the edit + * adds a node; untouched otherwise or when nullptr. + * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, or an + * error from the graph, project, or backend operation. + */ xtc_result MainWindow::edit_graph(void *context, const xtc_graph_edit &edit, size_t *out_added_id) { auto *window = static_cast(context); @@ -286,6 +332,13 @@ xtc_result MainWindow::edit_graph(void *context, const xtc_graph_edit &edit, siz return XTC_OK; } +/** + * @brief Opens the JACK backend and submits the current graph. + * + * Uses the default backend configuration and leaves the engine stopped. + * Failure is reported and leaves the backend disconnected; a handle that + * opened but failed to accept the graph is retained for reconnect. + */ void MainWindow::open_backend() { xtc_backend_config config = {XTC_BACKEND_JACK, XTC_PROJECT_NAME, 65536U, 2U}; @@ -305,6 +358,13 @@ void MainWindow::open_backend() refresh_status(); } +/** + * @brief Starts the audio engine with the current project. + * + * Does nothing when already running. Reports an error when disconnected. + * Otherwise prepares the project and starts the engine. Missing or + * unreadable audio is reported as a warning after a successful start. + */ void MainWindow::start_engine() { if (!ready_) { @@ -331,6 +391,11 @@ void MainWindow::start_engine() refresh_status(); } +/** + * @brief Stops the audio engine, finalizing any active recording. + * + * Does nothing when disconnected. Failures are reported in the window. + */ void MainWindow::stop_engine() { if (!ready_) @@ -346,6 +411,12 @@ void MainWindow::stop_engine() refresh_status(); } +/** + * @brief Closes and reopens the JACK backend, leaving the engine stopped. + * + * Refuses to run while recording. If closing fails the existing handle is + * retained for a later retry and the failure is reported. + */ void MainWindow::reconnect_engine() { if (recorder_) { @@ -363,6 +434,14 @@ void MainWindow::reconnect_engine() open_backend(); } +/** + * @brief Polls the backend and updates labels, meters, and button states. + * + * When disconnected, shows offline text and disables transport controls. + * Otherwise updates the sequencer position, mixer peaks, status line, and + * error indicators from the latest engine status, then collects backend + * work. + */ void MainWindow::refresh_status() { if (mixer_) @@ -432,6 +511,15 @@ void MainWindow::refresh_status() stop_->deactivate(); } +/** + * @brief Attempts to close the window after discarding unsaved changes. + * + * Refuses while recording. On success closes the backend, removes the status + * timer, hides the piano-roll window, and hides this window. + * + * @return True when the window closed; false when recording is active, the + * user cancels discarding, or the backend cannot close. + */ bool MainWindow::close_window() { if (recorder_) { @@ -456,6 +544,14 @@ bool MainWindow::close_window() return true; } +/** + * @brief Handles FLTK events for the window. + * + * Space without Ctrl/Alt/Meta toggles the audio engine and is consumed. + * + * @param event FLTK event code, such as FL_SHORTCUT. + * @return 1 when Space was consumed; otherwise the Fl_Double_Window result. + */ int MainWindow::handle(int event) { if (event == FL_SHORTCUT && Fl::event_key() == ' ' && (Fl::event_state() & (FL_CTRL | FL_ALT | FL_META)) == 0) { @@ -468,26 +564,53 @@ int MainWindow::handle(int event) return Fl_Double_Window::handle(event); } +/** + * @brief FLTK callback for the Play menu item and button. + * + * @param context MainWindow * whose engine is started. + */ void MainWindow::play_callback(Fl_Widget *, void *context) { static_cast(context)->start_engine(); } +/** + * @brief FLTK callback for the Stop menu item and button. + * + * @param context MainWindow * whose engine is stopped. + */ void MainWindow::stop_callback(Fl_Widget *, void *context) { static_cast(context)->stop_engine(); } +/** + * @brief FLTK callback for the Reconnect menu item and button. + * + * @param context MainWindow * whose backend is reopened. + */ void MainWindow::reconnect_callback(Fl_Widget *, void *context) { static_cast(context)->reconnect_engine(); } +/** + * @brief FLTK callback for the window close box, File/Quit, and close events. + * + * @param context MainWindow * whose close is requested; a refused close + * simply leaves the window open. + */ void MainWindow::close_callback(Fl_Widget *, void *context) { (void)static_cast(context)->close_window(); } +/** + * @brief FLTK menu callback that selects a tab page in the main window. + * + * @param widget Triggering menu item; window() must return the MainWindow. + * @param view Fl_Widget * tab page to make current, not the MainWindow. + */ void MainWindow::view_callback(Fl_Widget *widget, void *view) { auto *window = static_cast(widget->window()); @@ -495,12 +618,25 @@ void MainWindow::view_callback(Fl_Widget *widget, void *view) window->tabs_->redraw(); } +/** + * @brief FLTK help callback that shows the application name and version. + * + */ void MainWindow::about_callback(Fl_Widget *, void *) { fl_message("%s %s\nModular audio workstation\nPhase 9: native C processors and Linux release preparation.", XTC_PROJECT_NAME, XTC_VERSION_STRING); } +/** + * @brief FLTK timeout callback that refreshes status and supervises recording. + * + * While recording, drains the recorder and stops the engine if draining + * fails or the backend reports a lost server, sample-rate change, or + * processing error. Reschedules itself every 0.1 seconds until closing. + * + * @param context MainWindow * registered with Fl::add_timeout(). + */ void MainWindow::timer_callback(void *context) { auto *window = static_cast(context); @@ -516,6 +652,12 @@ void MainWindow::timer_callback(void *context) Fl::repeat_timeout(0.1, timer_callback, context); } +/** + * @brief Records the current project in history and marks the window dirty. + * + * Dependent widgets are refreshed with the current project and graph, and + * the title gains the unsaved marker. A history failure is reported. + */ void MainWindow::mark_dirty() { xtc_result history_result = xtc_history_record(history_.get(), project_.get()); @@ -529,12 +671,29 @@ void MainWindow::mark_dirty() copy_label("xtc * — unsaved changes"); } +/** + * @brief Asks whether unsaved changes may be discarded. + * + * @return True when the project is clean, prompting is disabled, or the user + * chose Discard; false when the user cancels. + */ bool MainWindow::discard_changes() { return !dirty_ || !prompt_unsaved_ || fl_choice("Discard unsaved project changes?", "Cancel", "Discard", nullptr) == 1; } +/** + * @brief Hook used by the sequencer to seek the audio engine. + * + * Does nothing when the backend is disconnected. Otherwise reads the engine + * sample rate, converts the tick position to samples, and seeks the backend. + * + * @param context MainWindow * passed as the sequencer owner context. + * @param tick Target timeline position in project ticks. + * @return XTC_OK when disconnected or the seek succeeded, or the status, + * conversion, or backend seek error. + */ xtc_result MainWindow::seek(void *context, uint64_t tick) { auto *window = static_cast(context); @@ -548,6 +707,15 @@ xtc_result MainWindow::seek(void *context, uint64_t tick) return r == XTC_OK ? xtc_backend_seek(window->backend_, sample) : r; } +/** + * @brief Saves the current project to a path and updates the window title. + * + * On success the path is remembered and the unsaved marker is cleared. + * + * @param path Destination project path; nullptr is invalid. + * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for nullptr, + * XTC_ERR_INVALID_STATE while recording, or the save error. + */ xtc_result MainWindow::save_project(const char *path) { if (recorder_) @@ -564,6 +732,21 @@ xtc_result MainWindow::save_project(const char *path) return r; } +/** + * @brief Installs a candidate project as the active project. + * + * Rejected while recording. Optionally builds fresh undo history, stops and + * prepares the connected backend, or validates offline audio and MIDI plans + * when disconnected. On success the window takes ownership of candidate, + * rebinds widgets, resets the graph layout, clears the dirty flag, and shows + * any missing-asset warning. On failure ownership stays with the caller. + * + * @param candidate Project to install; must be non-null. + * @param reset_history True to discard existing undo/redo history and start + * from candidate. + * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, or an + * error from history creation, the backend, or offline planning. + */ xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_history) { if (recorder_) @@ -625,6 +808,15 @@ xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_histor return XTC_OK; } +/** + * @brief Loads a project from a path and makes it the current project. + * + * The file is loaded into a candidate project and installed only on success. + * + * @param path Source project path; nullptr is invalid. + * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for nullptr, or an + * error from loading or installing the project. + */ xtc_result MainWindow::open_project(const char *path) { if (path == nullptr) @@ -645,6 +837,17 @@ xtc_result MainWindow::open_project(const char *path) return r; } +/** + * @brief FLTK menu callback for the File/New, Open, Save, and Save As items. + * + * Reads the action from the selected menu label, confirms discarding unsaved + * changes for New and Open, and uses a native file chooser unless Save can + * reuse the remembered path. Refuses file operations while recording and + * reports failures in the window. + * + * @param widget Triggering Fl_Menu_Bar whose selected label names the action. + * @param context MainWindow * owning the project and file path. + */ void MainWindow::file_callback(Fl_Widget *widget, void *context) { auto *window = static_cast(context); @@ -694,6 +897,16 @@ void MainWindow::file_callback(Fl_Widget *widget, void *context) window->report_error("Project operation failed", r); } +/** + * @brief Hook called by arrangement widgets before mutating the project. + * + * Refuses edits while recording. When connected, stops the engine so the + * mutation cannot race the audio thread, then refreshes the status display. + * + * @param context MainWindow * passed as the widget owner context. + * @return XTC_OK when disconnected or the stop succeeded, + * XTC_ERR_INVALID_STATE while recording, or the backend stop error. + */ xtc_result MainWindow::before_arrangement_edit(void *context) { auto *window = static_cast(context); @@ -706,6 +919,17 @@ xtc_result MainWindow::before_arrangement_edit(void *context) return r; } +/** + * @brief Imports a WAV file as an audio clip in a cloned project. + * + * Stops the engine first, creates an "Audio" track when none is selected, + * and places the clip at the sequencer cursor. The clone replaces the live + * project only after every step succeeds. + * + * @param path Path to the WAV file to import. + * @return XTC_OK on success, or an error from the engine stop, clone, import, + * or backend preparation. + */ xtc_result MainWindow::import_audio(const char *path) { xtc_result r = before_arrangement_edit(this); @@ -732,6 +956,19 @@ xtc_result MainWindow::import_audio(const char *path) return XTC_OK; } +/** + * @brief Starts recording JACK input to a WAV file. + * + * Requires a connected backend and no active recording. The engine is + * stopped, the current position and selected track are remembered, and a + * stereo recorder at the engine sample rate is attached before starting. + * Failure leaves the window without a recorder. + * + * @param path Destination WAV path chosen by the user. + * @return XTC_OK when recording started, XTC_ERR_INVALID_STATE when + * disconnected or already recording, or another backend or recorder + * error. + */ xtc_result MainWindow::record_audio(const char *path) { if (!ready_ || recorder_) @@ -763,6 +1000,14 @@ xtc_result MainWindow::record_audio(const char *path) return r; } +/** + * @brief Finalizes the active recording and adds the take to the project. + * + * Must run only after the engine has stopped, so no audio callback can + * reference the recorder ring. The recorder is detached, finished, and + * destroyed. A failed finalize or empty take is reported and not added. If + * the subsequent import fails, the saved WAV path is reported for recovery. + */ void MainWindow::finish_recording() { // Called only after successful backend_stop: no producer can reference ring. @@ -807,6 +1052,18 @@ void MainWindow::finish_recording() mark_dirty(); } +/** + * @brief FLTK callback for the audio import, record, and relink commands. + * + * The triggering widget supplies the menu label; the Record button uses the + * "Record WAV" default. A native file chooser selects the file, after which + * the selected clip is relinked, recording starts, or a WAV is imported. + * Failures are reported in the window. + * + * @param widget Triggering Fl_Menu_Bar or Fl_Button; menu labels select the + * action. + * @param context MainWindow * owning the project and sequencer. + */ void MainWindow::audio_file_callback(Fl_Widget *widget, void *context) { auto *window = static_cast(context); @@ -843,6 +1100,14 @@ void MainWindow::audio_file_callback(Fl_Widget *widget, void *context) window->report_error("Project audio operation failed", r); } +/** + * @brief Shows the piano roll for the selected MIDI clip. + * + * The piano-roll window is created on first use and reused afterwards. + * + * @return XTC_OK on success, or XTC_ERR_INVALID_STATE when the selected clip + * does not exist or is not MIDI. + */ xtc_result MainWindow::open_piano_roll() { xtc_clip_info clip{}; @@ -873,6 +1138,14 @@ xtc_result MainWindow::open_piano_roll() return XTC_OK; } +/** + * @brief FLTK callback for the Piano roll menu item and button. + * + * Refuses to open the editor while recording and asks the user to select a + * MIDI clip when open_piano_roll() fails. + * + * @param context MainWindow * owning the piano roll. + */ void MainWindow::piano_callback(Fl_Widget *, void *context) { auto *window = static_cast(context); @@ -885,6 +1158,14 @@ void MainWindow::piano_callback(Fl_Widget *, void *context) set_label(window->error_, "Select a MIDI clip, then open Piano roll."); } +/** + * @brief FLTK callback for the Panic menu item and button. + * + * When the backend is connected, sends a panic request that silences held + * instrument notes and reports failures. + * + * @param context MainWindow * owning the backend. + */ void MainWindow::panic_callback(Fl_Widget *, void *context) { auto *window = static_cast(context); @@ -895,6 +1176,17 @@ void MainWindow::panic_callback(Fl_Widget *, void *context) window->report_error("Panic failed", r); } +/** + * @brief Applies one undo or redo step from the project history. + * + * The selected snapshot is installed without resetting history and then + * accepted, leaving the window marked dirty. + * + * @param redo True to redo, false to undo. + * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, + * XTC_ERR_NOT_FOUND when no step is available, or another error from + * checkout, install, or accept. + */ xtc_result MainWindow::undo(bool redo) { if (recorder_) @@ -913,6 +1205,15 @@ xtc_result MainWindow::undo(bool redo) return r; } +/** + * @brief FLTK menu callback for the Undo and Redo menu items. + * + * The selected menu label starting with "Redo" chooses redo; all other + * labels undo. Failures other than XTC_ERR_NOT_FOUND are shown in the window. + * + * @param widget Fl_Menu_Bar whose selected item triggered the callback. + * @param context MainWindow * owning the menu and history. + */ void MainWindow::history_callback(Fl_Widget *widget, void *context) { auto *window = static_cast(context); diff --git a/src/gui/main_window.hpp b/src/gui/main_window.hpp index 1f7fbcd..276ff89 100644 --- a/src/gui/main_window.hpp +++ b/src/gui/main_window.hpp @@ -18,55 +18,262 @@ class Fl_Tabs; class MainWindow : public Fl_Double_Window { public: + /** + * @brief Constructs the window, its widgets, and the initial project. + * + * Creates an empty project and undo history, opens the audio backend, and + * schedules the periodic status timer. Throws std::runtime_error when the + * initial project or history cannot be created. + * + * @param prompt_unsaved Whether open, new, and close actions ask before + * discarding unsaved changes. + */ explicit MainWindow(bool prompt_unsaved = true); + /** + * @brief Removes the status timer and releases backend and recording state. + * + * A backend that fails to close is intentionally retained with its recorder + * until process exit so active callbacks never observe freed state. + */ ~MainWindow() override; + /** + * @brief Deleted copy constructor; a window uniquely owns its project and backend. + */ MainWindow(const MainWindow &) = delete; + /** + * @brief Deleted copy assignment; a window uniquely owns its project and backend. + */ MainWindow &operator=(const MainWindow &) = delete; using Fl_Double_Window::show; + /** + * @brief Handles FLTK events for the window. + * + * Space without Ctrl/Alt/Meta toggles the audio engine and is consumed. + * + * @param event FLTK event code, such as FL_SHORTCUT. + * @return 1 when Space was consumed; otherwise the Fl_Double_Window result. + */ int handle(int event) override; + /** + * @brief Applies one undo or redo step from the project history. + * + * The selected snapshot is installed without resetting history and then + * accepted, leaving the window marked dirty. + * + * @param redo True to redo, false to undo. + * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, + * XTC_ERR_NOT_FOUND when no step is available, or another error from + * checkout, install, or accept. + */ xtc_result undo(bool redo = false); + /** + * @brief Returns the mixer tab widget owned by the window. + * + * @return Borrowed mixer widget, or nullptr before construction completes. + */ MixerWidget *mixer() const { return mixer_; } + /** + * @brief Shows the piano roll for the selected MIDI clip. + * + * The piano-roll window is created on first use and reused afterwards. + * + * @return XTC_OK on success, or XTC_ERR_INVALID_STATE when the selected clip + * does not exist or is not MIDI. + */ xtc_result open_piano_roll(); + /** + * @brief Returns the piano-roll editor widget. + * + * @return Borrowed widget, or nullptr until open_piano_roll() first succeeds. + */ PianoRollWidget *piano_roll() const { return piano_; } + /** + * @brief Imports a WAV file as an audio clip in a cloned project. + * + * Stops the engine first, creates an "Audio" track when none is selected, + * and places the clip at the sequencer cursor. The clone replaces the live + * project only after every step succeeds. + * + * @param path Path to the WAV file to import. + * @return XTC_OK on success, or an error from the engine stop, clone, import, + * or backend preparation. + */ xtc_result import_audio(const char *path); + /** + * @brief Starts recording JACK input to a WAV file. + * + * Requires a connected backend and no active recording. The engine is + * stopped, the current position and selected track are remembered, and a + * stereo recorder at the engine sample rate is attached before starting. + * Failure leaves the window without a recorder. + * + * @param path Destination WAV path chosen by the user. + * @return XTC_OK when recording started, XTC_ERR_INVALID_STATE when + * disconnected or already recording, or another backend or recorder + * error. + */ xtc_result record_audio(const char *path); + /** + * @brief Saves the current project to a path and updates the window title. + * + * On success the path is remembered and the unsaved marker is cleared. + * + * @param path Destination project path; nullptr is invalid. + * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for nullptr, + * XTC_ERR_INVALID_STATE while recording, or the save error. + */ xtc_result save_project(const char *path); + /** + * @brief Loads a project from a path and makes it the current project. + * + * The file is loaded into a candidate project and installed only on success. + * + * @param path Source project path; nullptr is invalid. + * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for nullptr, or an + * error from loading or installing the project. + */ xtc_result open_project(const char *path); private: MixerWidget *mixer_ = nullptr; std::unique_ptr history_{nullptr, xtc_history_destroy}; - static void history_callback(Fl_Widget *, void *); + /** + * @brief FLTK menu callback for the Undo and Redo menu items. + * + * The selected menu label starting with "Redo" chooses redo; all other + * labels undo. Failures other than XTC_ERR_NOT_FOUND are shown in the window. + * + * @param widget Fl_Menu_Bar whose selected item triggered the callback. + * @param context MainWindow * owning the menu and history. + */ + static void history_callback(Fl_Widget * widget, void * context); std::unique_ptr project_{nullptr, xtc_project_destroy}; std::unique_ptr piano_window_; PianoRollWidget *piano_ = nullptr; - static void piano_callback(Fl_Widget *, void *context); - static void panic_callback(Fl_Widget *, void *context); + /** + * @brief FLTK callback for the Piano roll menu item and button. + * + * Refuses to open the editor while recording and asks the user to select a + * MIDI clip when open_piano_roll() fails. + * + * @param widget Unused triggering widget. + * @param context MainWindow * owning the piano roll. + */ + static void piano_callback(Fl_Widget * widget, void *context); + /** + * @brief FLTK callback for the Panic menu item and button. + * + * When the backend is connected, sends a panic request that silences held + * instrument notes and reports failures. + * + * @param widget Unused triggering widget. + * @param context MainWindow * owning the backend. + */ + static void panic_callback(Fl_Widget * widget, void *context); SequencerWidget *sequencer_ = nullptr; xtc_recorder *recorder_ = nullptr; std::string recording_path_; uint64_t recording_tick_ = 0, recording_track_ = UINT64_MAX; + /** + * @brief Finalizes the active recording and adds the take to the project. + * + * Must run only after the engine has stopped, so no audio callback can + * reference the recorder ring. The recorder is detached, finished, and + * destroyed. A failed finalize or empty take is reported and not added. If + * the subsequent import fails, the saved WAV path is reported for recovery. + */ void finish_recording(); + /** + * @brief Hook called by arrangement widgets before mutating the project. + * + * Refuses edits while recording. When connected, stops the engine so the + * mutation cannot race the audio thread, then refreshes the status display. + * + * @param context MainWindow * passed as the widget owner context. + * @return XTC_OK when disconnected or the stop succeeded, + * XTC_ERR_INVALID_STATE while recording, or the backend stop error. + */ static xtc_result before_arrangement_edit(void *context); - static void audio_file_callback(Fl_Widget *, void *context); + /** + * @brief FLTK callback for the audio import, record, and relink commands. + * + * The triggering widget supplies the menu label; the Record button uses the + * "Record WAV" default. A native file chooser selects the file, after which + * the selected clip is relinked, recording starts, or a WAV is imported. + * Failures are reported in the window. + * + * @param widget Triggering Fl_Menu_Bar or Fl_Button; menu labels select the + * action. + * @param context MainWindow * owning the project and sequencer. + */ + static void audio_file_callback(Fl_Widget * widget, void *context); bool dirty_ = false, prompt_unsaved_ = true; std::string path_; + /** + * @brief Asks whether unsaved changes may be discarded. + * + * @return True when the project is clean, prompting is disabled, or the user + * chose Discard; false when the user cancels. + */ bool discard_changes(); + /** + * @brief Records the current project in history and marks the window dirty. + * + * Dependent widgets are refreshed with the current project and graph, and + * the title gains the unsaved marker. A history failure is reported. + */ void mark_dirty(); + /** + * @brief Installs a candidate project as the active project. + * + * Rejected while recording. Optionally builds fresh undo history, stops and + * prepares the connected backend, or validates offline audio and MIDI plans + * when disconnected. On success the window takes ownership of candidate, + * rebinds widgets, resets the graph layout, clears the dirty flag, and shows + * any missing-asset warning. On failure ownership stays with the caller. + * + * @param candidate Project to install; must be non-null. + * @param reset_history True to discard existing undo/redo history and start + * from candidate. + * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, or an + * error from history creation, the backend, or offline planning. + */ xtc_result install_project(xtc_project *candidate, bool reset_history = true); + /** + * @brief Hook used by the sequencer to seek the audio engine. + * + * Does nothing when the backend is disconnected. Otherwise reads the engine + * sample rate, converts the tick position to samples, and seeks the backend. + * + * @param context MainWindow * passed as the sequencer owner context. + * @param tick Target timeline position in project ticks. + * @return XTC_OK when disconnected or the seek succeeded, or the status, + * conversion, or backend seek error. + */ static xtc_result seek(void *context, uint64_t tick); - static void file_callback(Fl_Widget *, void *context); + /** + * @brief FLTK menu callback for the File/New, Open, Save, and Save As items. + * + * Reads the action from the selected menu label, confirms discarding unsaved + * changes for New and Open, and uses a native file chooser unless Save can + * reuse the remembered path. Refuses file operations while recording and + * reports failures in the window. + * + * @param widget Triggering Fl_Menu_Bar whose selected label names the action. + * @param context MainWindow * owning the project and file path. + */ + static void file_callback(Fl_Widget * widget, void *context); GraphWidget *graph_widget_ = nullptr; Fl_Box *graph_state_ = nullptr; xtc_backend *backend_ = nullptr; @@ -81,20 +288,129 @@ class MainWindow : public Fl_Double_Window { Fl_Box *error_ = nullptr; Fl_Tabs *tabs_ = nullptr; + /** + * @brief Edit handler passed to the modular graph widget. + * + * Applies the edit to a candidate graph, clones the project around it, and + * submits the new graph to the engine when connected. The live project is + * replaced only after every step succeeds. + * + * @param context MainWindow * passed as the graph widget owner context. + * @param edit Graph edit to apply. + * @param out_added_id Optional output receiving the new node ID when the edit + * adds a node; untouched otherwise or when nullptr. + * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, or an + * error from the graph, project, or backend operation. + */ static xtc_result edit_graph(void *context, const xtc_graph_edit &edit, size_t *out_added_id); + /** + * @brief Opens the JACK backend and submits the current graph. + * + * Uses the default backend configuration and leaves the engine stopped. + * Failure is reported and leaves the backend disconnected; a handle that + * opened but failed to accept the graph is retained for reconnect. + */ void open_backend(); + /** + * @brief Starts the audio engine with the current project. + * + * Does nothing when already running. Reports an error when disconnected. + * Otherwise prepares the project and starts the engine. Missing or + * unreadable audio is reported as a warning after a successful start. + */ void start_engine(); + /** + * @brief Stops the audio engine, finalizing any active recording. + * + * Does nothing when disconnected. Failures are reported in the window. + */ void stop_engine(); + /** + * @brief Closes and reopens the JACK backend, leaving the engine stopped. + * + * Refuses to run while recording. If closing fails the existing handle is + * retained for a later retry and the failure is reported. + */ void reconnect_engine(); + /** + * @brief Polls the backend and updates labels, meters, and button states. + * + * When disconnected, shows offline text and disables transport controls. + * Otherwise updates the sequencer position, mixer peaks, status line, and + * error indicators from the latest engine status, then collects backend + * work. + */ void refresh_status(); + /** + * @brief Attempts to close the window after discarding unsaved changes. + * + * Refuses while recording. On success closes the backend, removes the status + * timer, hides the piano-roll window, and hides this window. + * + * @return True when the window closed; false when recording is active, the + * user cancels discarding, or the backend cannot close. + */ bool close_window(); + /** + * @brief Shows an operation failure and a context-sensitive hint. + * + * @param operation Human-readable operation name; names containing + * "Project" select a file and permission hint. + * @param result Failing result code whose name is included in the message. + */ void report_error(const char *operation, xtc_result result); - static void play_callback(Fl_Widget *, void *context); - static void stop_callback(Fl_Widget *, void *context); - static void reconnect_callback(Fl_Widget *, void *context); - static void close_callback(Fl_Widget *, void *context); + /** + * @brief FLTK callback for the Play menu item and button. + * + * @param widget Unused triggering widget. + * @param context MainWindow * whose engine is started. + */ + static void play_callback(Fl_Widget * widget, void *context); + /** + * @brief FLTK callback for the Stop menu item and button. + * + * @param widget Unused triggering widget. + * @param context MainWindow * whose engine is stopped. + */ + static void stop_callback(Fl_Widget * widget, void *context); + /** + * @brief FLTK callback for the Reconnect menu item and button. + * + * @param widget Unused triggering widget. + * @param context MainWindow * whose backend is reopened. + */ + static void reconnect_callback(Fl_Widget * widget, void *context); + /** + * @brief FLTK callback for the window close box, File/Quit, and close events. + * + * @param widget Unused triggering widget or window. + * @param context MainWindow * whose close is requested; a refused close + * simply leaves the window open. + */ + static void close_callback(Fl_Widget * widget, void *context); + /** + * @brief FLTK menu callback that selects a tab page in the main window. + * + * @param widget Triggering menu item; window() must return the MainWindow. + * @param view Fl_Widget * tab page to make current, not the MainWindow. + */ static void view_callback(Fl_Widget *widget, void *view); - static void about_callback(Fl_Widget *, void *); + /** + * @brief FLTK help callback that shows the application name and version. + * + * @param widget Unused triggering widget. + * @param context Unused callback context. + */ + static void about_callback(Fl_Widget * widget, void * context); + /** + * @brief FLTK timeout callback that refreshes status and supervises recording. + * + * While recording, drains the recorder and stops the engine if draining + * fails or the backend reports a lost server, sample-rate change, or + * processing error. Reschedules itself every 0.1 seconds until closing. + * + * @param context MainWindow * registered with Fl::add_timeout(). + */ static void timer_callback(void *context); }; diff --git a/src/gui/mixer_widget.cpp b/src/gui/mixer_widget.cpp index aaa1c15..15ab850 100644 --- a/src/gui/mixer_widget.cpp +++ b/src/gui/mixer_widget.cpp @@ -22,10 +22,25 @@ class AutomationLane : public Fl_Widget { MixerWidget &mixer_; public: + /** + * @brief Creates an automation lane bound to its owning mixer. + * + * @param x Left edge in parent coordinates. + * @param y Top edge in parent coordinates. + * @param w Lane width in pixels; tick math requires a value greater than 1. + * @param h Lane height in pixels. + * @param m Mixer that owns the lane; stored by reference and must outlive it. + */ AutomationLane(int x, int y, int w, int h, MixerWidget &m) : Fl_Widget(x, y, w, h), mixer_(m) { } + /** + * @brief Draws the lane grid, automation curve, points, and empty hint. + * + * Points for the selected track and parameter are sorted by tick, rendered + * as an interpolated curve, and marked within the visible tick range. + */ void draw() override { fl_push_clip(x(), y(), w(), h()); @@ -100,6 +115,12 @@ class AutomationLane : public Fl_Widget { fl_pop_clip(); } + /** + * @brief Handles FLTK input events and delegates unhandled events to the base widget. + * + * @param event FLTK event code. + * @return Nonzero if the event was handled, zero otherwise. + */ int handle(int event) override { if (event != FL_PUSH) @@ -136,6 +157,20 @@ class AutomationLane : public Fl_Widget { } }; +/** + * @brief Creates the mixer panel and its child controls. + * + * Builds the track chooser, gain/pan sliders, mute/solo buttons, output + * meters, automation controls, and lane, then displays the given project. + * + * @param x Left edge in parent coordinates. + * @param y Top edge in parent coordinates. + * @param w Widget width in pixels. + * @param h Widget height in pixels. + * @param p Project to display; borrowed and must outlive its use. + * @param before Callback invoked before each edit; may be nullptr. + * @param context Opaque pointer forwarded to before. + */ MixerWidget::MixerWidget(int x, int y, int w, int h, xtc_project *p, BeforeEdit before, void *context) : Fl_Group(x, y, w, h), project_(p), before_(before), context_(context) { @@ -182,17 +217,35 @@ MixerWidget::MixerWidget(int x, int y, int w, int h, xtc_project *p, BeforeEdit set_project(p); } +/** + * @brief Returns the ID of the track selected in the track chooser. + * + * @return Selected track ID, or UINT64_MAX when the chooser selection does + * not resolve to a project track. + */ uint64_t MixerWidget::selected_track() const { xtc_track_info t{}; return xtc_project_get_track(project_, static_cast(tracks_->value()), &t) == XTC_OK ? t.id : UINT64_MAX; } +/** + * @brief Returns the automation parameter selected in the chooser. + * + * @return XTC_AUTOMATION_PAN when the chooser selects "Pan", otherwise + * XTC_AUTOMATION_GAIN. + */ xtc_automation_parameter MixerWidget::parameter() const { return parameter_->value() == 1 ? XTC_AUTOMATION_PAN : XTC_AUTOMATION_GAIN; } +/** + * @brief Returns the first tick shown by the automation lane. + * + * @return Clamped first tick in project ticks, or 0 when the input value is + * not finite. + */ uint64_t MixerWidget::lane_start() const { if (!std::isfinite(start_->value())) @@ -201,6 +254,14 @@ uint64_t MixerWidget::lane_start() const std::clamp(start_->value(), 0.0, static_cast(XTC_PROJECT_MAX_TICK - lane_ticks))); } +/** + * @brief Displays a different project and refreshes the controls. + * + * The current track selection is preserved only when the same project + * pointer is passed again. The project is borrowed and must outlive use. + * + * @param p Project to display; must remain valid while shown. + */ void MixerWidget::set_project(xtc_project *p) { uint64_t selected = project_ == p ? selected_track() : UINT64_MAX; @@ -227,6 +288,13 @@ void MixerWidget::set_project(xtc_project *p) redraw(); } +/** + * @brief Synchronizes the child controls with the selected track. + * + * Controls are deactivated when no valid track is selected. Gain falls + * back to 1, and the remaining values come from the track info or zero when + * unresolved. + */ void MixerWidget::selection() { xtc_track_info t{}; @@ -246,6 +314,15 @@ void MixerWidget::selection() lane_->redraw(); } +/** + * @brief Updates the left and right output meters. + * + * Meter values are clamped to 1.0, while the over-range color is chosen + * from the unclamped level. + * + * @param left Left-channel peak level. + * @param right Right-channel peak level. + */ void MixerWidget::peaks(float left, float right) { left_->value(std::min(left, 1.0f)); @@ -254,6 +331,16 @@ void MixerWidget::peaks(float left, float right) right_->selection_color(right > 1 ? FL_RED : FL_GREEN); } +/** + * @brief Reports an edit result and refreshes the controls. + * + * A successful result invokes the widget callback so the owner can mark + * the project dirty; a failure shows a modal alert. The controls are + * resynchronized in both cases. + * + * @param r Result of the attempted edit. + * @return The same result passed in. + */ xtc_result MixerWidget::changed(xtc_result r) { if (r == XTC_OK) @@ -264,6 +351,19 @@ xtc_result MixerWidget::changed(xtc_result r) return r; } +/** + * @brief Applies gain, pan, mute, and solo to the selected track. + * + * The constructor's before-edit callback runs first; the edit is skipped + * if it returns an error. On success the widget callback is invoked and + * the controls are refreshed. + * + * @param gain Linear gain in the range 0..4. + * @param pan Pan position in the range -1 (left)..1 (right). + * @param mute Mute state: 0 or 1. + * @param solo Solo state: 0 or 1. + * @return XTC_OK on success, or the before-edit or project mutation error. + */ xtc_result MixerWidget::mix(float gain, float pan, int mute, int solo) { xtc_result r = before_ ? before_(context_) : XTC_OK; @@ -272,6 +372,18 @@ xtc_result MixerWidget::mix(float gain, float pan, int mute, int solo) return changed(r); } +/** + * @brief Adds, replaces, or removes an automation point. + * + * The constructor's before-edit callback runs first; the edit is skipped + * if it returns an error. On success the widget callback is invoked and + * the controls are refreshed. + * + * @param tick Automation tick to modify; must be within the project range. + * @param value New value; ignored when remove is true. + * @param remove When true, removes the point instead of setting it. + * @return XTC_OK on success, or the before-edit or project mutation error. + */ xtc_result MixerWidget::point(uint64_t tick, float value, bool remove) { xtc_result r = before_ ? before_(context_) : XTC_OK; @@ -282,6 +394,14 @@ xtc_result MixerWidget::point(uint64_t tick, float value, bool remove) return changed(r); } +/** + * @brief FLTK callback for the gain, pan, mute, and solo controls. + * + * Reads all four controls and applies them together to the selected track + * through mix(). + * + * @param c Opaque MixerWidget pointer supplied as callback context. + */ void MixerWidget::control(Fl_Widget *, void *c) { auto *m = static_cast(c); @@ -289,11 +409,25 @@ void MixerWidget::control(Fl_Widget *, void *c) m->solo_->value()); } +/** + * @brief FLTK callback for the track, parameter, and first-tick controls. + * + * @param c Opaque MixerWidget pointer supplied as callback context. + */ void MixerWidget::select(Fl_Widget *, void *c) { static_cast(c)->selection(); } +/** + * @brief Handles keyboard shortcuts and forwards other events. + * + * M and S toggle mute and solo, Left/Right adjust gain (Shift adjusts pan), + * and Up/Down change the selected track. Modified key presses fall through. + * + * @param e FLTK event code. + * @return 1 when the event was consumed, otherwise the Fl_Group result. + */ int MixerWidget::handle(int e) { if (e == FL_KEYDOWN && !(Fl::event_state() & (FL_CTRL | FL_ALT | FL_META))) { diff --git a/src/gui/mixer_widget.hpp b/src/gui/mixer_widget.hpp index e54c986..84b81d6 100644 --- a/src/gui/mixer_widget.hpp +++ b/src/gui/mixer_widget.hpp @@ -12,22 +12,124 @@ class AutomationLane; class MixerWidget : public Fl_Group { public: + /** + * @brief Prepares a model edit using the widget's opaque callback context. + * + * @return XTC_OK to allow the edit, or an error to cancel it. + */ using BeforeEdit = xtc_result (*)(void *); - MixerWidget(int, int, int, int, xtc_project *, BeforeEdit, void *); - void set_project(xtc_project *); + + /** + * @brief Creates the mixer panel and its child controls. + * + * Builds the track chooser, gain/pan sliders, mute/solo buttons, output + * meters, automation controls, and lane, then displays the given project. + * + * @param x Left edge in parent coordinates. + * @param y Top edge in parent coordinates. + * @param w Widget width in pixels. + * @param h Widget height in pixels. + * @param p Project to display; borrowed and must outlive its use. + * @param before Callback invoked before each edit; may be nullptr. + * @param context Opaque pointer forwarded to before. + */ + MixerWidget(int x, int y, int w, int h, xtc_project * p, BeforeEdit before, void * context); + + /** + * @brief Displays a different project and refreshes the controls. + * + * The current track selection is preserved only when the same project + * pointer is passed again. The project is borrowed and must outlive use. + * + * @param p Project to display; must remain valid while shown. + */ + void set_project(xtc_project * p); + + /** + * @brief Updates the left and right output meters. + * + * Meter values are clamped to 1.0, while the over-range color is chosen + * from the unclamped level. + * + * @param left Left-channel peak level. + * @param right Right-channel peak level. + */ void peaks(float left, float right); + + /** + * @brief Returns the ID of the track selected in the track chooser. + * + * @return Selected track ID, or UINT64_MAX when the chooser selection does + * not resolve to a project track. + */ uint64_t selected_track() const; + /** + * @brief Returns the project currently displayed by the widget. + * + * @return Borrowed project pointer, possibly nullptr; the widget does not + * own it. + */ xtc_project *project() const { return project_; } + /** + * @brief Returns the automation parameter selected in the chooser. + * + * @return XTC_AUTOMATION_PAN when the chooser selects "Pan", otherwise + * XTC_AUTOMATION_GAIN. + */ xtc_automation_parameter parameter() const; + + /** + * @brief Returns the first tick shown by the automation lane. + * + * @return Clamped first tick in project ticks, or 0 when the input value is + * not finite. + */ uint64_t lane_start() const; + + /** + * @brief Adds, replaces, or removes an automation point. + * + * The constructor's before-edit callback runs first; the edit is skipped + * if it returns an error. On success the widget callback is invoked and + * the controls are refreshed. + * + * @param tick Automation tick to modify; must be within the project range. + * @param value New value; ignored when remove is true. + * @param remove When true, removes the point instead of setting it. + * @return XTC_OK on success, or the before-edit or project mutation error. + */ xtc_result point(uint64_t tick, float value, bool remove = false); + + /** + * @brief Applies gain, pan, mute, and solo to the selected track. + * + * The constructor's before-edit callback runs first; the edit is skipped + * if it returns an error. On success the widget callback is invoked and + * the controls are refreshed. + * + * @param gain Linear gain in the range 0..4. + * @param pan Pan position in the range -1 (left)..1 (right). + * @param mute Mute state: 0 or 1. + * @param solo Solo state: 0 or 1. + * @return XTC_OK on success, or the before-edit or project mutation error. + */ xtc_result mix(float gain, float pan, int mute, int solo); - int handle(int) override; + + /** + * @brief Handles keyboard shortcuts and forwards other events. + * + * M and S toggle mute and solo, Left/Right adjust gain (Shift adjusts pan), + * and Up/Down change the selected track. Modified key presses fall through. + * + * @param event FLTK event code. + * @return 1 when the event was consumed, otherwise the Fl_Group result. + */ + int handle(int event) override; private: xtc_project *project_; @@ -39,9 +141,44 @@ class MixerWidget : public Fl_Group { Fl_Progress *left_, *right_; Fl_Value_Input *start_; AutomationLane *lane_; - xtc_result changed(xtc_result); + /** + * @brief Reports an edit result and refreshes the controls. + * + * A successful result invokes the widget callback so the owner can mark + * the project dirty; a failure shows a modal alert. The controls are + * resynchronized in both cases. + * + * @param r Result of the attempted edit. + * @return The same result passed in. + */ + xtc_result changed(xtc_result r); + + /** + * @brief Synchronizes the child controls with the selected track. + * + * Controls are deactivated when no valid track is selected. Gain falls + * back to 1, and the remaining values come from the track info or zero when + * unresolved. + */ void selection(); - static void control(Fl_Widget *, void *); - static void select(Fl_Widget *, void *); + + /** + * @brief FLTK callback for the gain, pan, mute, and solo controls. + * + * Reads all four controls and applies them together to the selected track + * through mix(). + * + * @param widget Widget that triggered the callback; unused. + * @param c Opaque MixerWidget pointer supplied as callback context. + */ + static void control(Fl_Widget * widget, void * c); + + /** + * @brief FLTK callback for the track, parameter, and first-tick controls. + * + * @param widget Widget that triggered the callback; unused. + * @param c Opaque MixerWidget pointer supplied as callback context. + */ + static void select(Fl_Widget * widget, void * c); }; #endif diff --git a/src/gui/piano_roll.cpp b/src/gui/piano_roll.cpp index 8685e0f..6e48399 100644 --- a/src/gui/piano_roll.cpp +++ b/src/gui/piano_roll.cpp @@ -9,6 +9,19 @@ namespace { constexpr int key_width = 64, row_height = 16, scale = 96, header = 28; } +/** + * @brief Creates a piano-roll editor for a MIDI clip. + * + * The pre-edit guard, when non-NULL, is called before every project mutation + * and can veto the edit by returning an error. + * + * @param left Left edge of the widget in the parent's coordinate system. + * @param top Top edge of the widget in the parent's coordinate system. + * @param project Borrowed project that must outlive the widget. + * @param clip ID of the MIDI clip to edit. + * @param before Optional pre-edit guard; NULL means no guard is used. + * @param context Opaque pointer passed to @p before on every edit. + */ PianoRollWidget::PianoRollWidget(int left, int top, xtc_project *project, uint64_t clip, BeforeEdit before, void *context) : Fl_Widget(left, top, 900, header + 128 * row_height), project_(project), clip_(clip), before_(before), @@ -17,6 +30,15 @@ PianoRollWidget::PianoRollWidget(int left, int top, xtc_project *project, uint64 set_project(project, clip); } +/** + * @brief Points the editor at a different project or clip. + * + * Clears any in-progress drag when the project or clip changes and resizes the + * widget to fit the clip's source length. The current selection is not changed. + * + * @param project Borrowed project that must outlive the widget. + * @param clip ID of the MIDI clip to display. + */ void PianoRollWidget::set_project(xtc_project *project, uint64_t clip) { if (project != project_ || clip != clip_) @@ -31,11 +53,30 @@ void PianoRollWidget::set_project(xtc_project *project, uint64_t clip) redraw(); } +/** + * @brief Converts a horizontal pixel position to a snapped tick. + * + * The position is measured in absolute window coordinates and snapped down to + * the nearest sixteenth note (240 ticks). + * + * @param px Horizontal position in window coordinates. + * @return Tick offset within the clip source, or 0 for positions on or left of + * the keyboard. + */ uint64_t PianoRollWidget::tick(int px) const { return static_cast(std::max(0, px - x() - key_width)) * 960U / scale / 240U * 240U; } +/** + * @brief Reports an edit result and notifies listeners on success. + * + * On success the widget callback runs and the editor is redrawn; on failure an + * alert shows the error and the project is left unchanged. + * + * @param result Result of the attempted project mutation. + * @return The same result code. + */ xtc_result PianoRollWidget::change(xtc_result result) { if (result == XTC_OK) { @@ -46,6 +87,18 @@ xtc_result PianoRollWidget::change(xtc_result result) return result; } +/** + * @brief Adds a note to the current clip and selects the new note. + * + * Invokes the pre-edit guard, if any, before mutating the project. + * + * @param start Note start in clip source ticks. + * @param length Note length in ticks. + * @param pitch MIDI pitch from 0 to 127. + * @param velocity MIDI velocity from 1 to 127. + * @param channel MIDI channel from 0 to 15. + * @return XTC_OK on success, the guard's error, or the project error. + */ xtc_result PianoRollWidget::add_note(uint64_t start, uint64_t length, unsigned int pitch, unsigned int velocity, unsigned int channel) { @@ -58,6 +111,14 @@ xtc_result PianoRollWidget::add_note(uint64_t start, uint64_t length, unsigned i return change(r); } +/** + * @brief Removes the selected note from the current clip. + * + * Invokes the pre-edit guard, if any, before mutating the project. The + * selection is cleared only when the removal succeeds. + * + * @return XTC_OK on success, the guard's error, or the project error. + */ xtc_result PianoRollWidget::delete_selected() { xtc_result r = before_ ? before_(context_) : XTC_OK; @@ -68,6 +129,11 @@ xtc_result PianoRollWidget::delete_selected() return change(r); } +/** + * @brief Draws the keyboard, beat grid, and notes of the current clip. + * + * Paints only the background when the clip is missing or is not a MIDI clip. + */ void PianoRollWidget::draw() { fl_push_clip(x(), y(), w(), h()); @@ -124,6 +190,17 @@ void PianoRollWidget::draw() fl_pop_clip(); } +/** + * @brief Handles mouse, keyboard, and focus events for the editor. + * + * Left-click on empty space adds a note, clicking a note selects it, and + * dragging moves or resizes it. Right-click and Delete remove the selected + * note; Escape cancels an in-progress drag. + * + * @param event FLTK event code. + * @return 1 when the event was handled, otherwise the result of the base + * class handler. + */ int PianoRollWidget::handle(int event) { if (event == FL_PUSH) { diff --git a/src/gui/piano_roll.hpp b/src/gui/piano_roll.hpp index f2f9e4a..acf0bdf 100644 --- a/src/gui/piano_roll.hpp +++ b/src/gui/piano_roll.hpp @@ -5,21 +5,90 @@ class PianoRollWidget : public Fl_Widget { public: + /** + * @brief Prepares a model edit using the widget's opaque callback context. + * + * @return XTC_OK to allow the edit, or an error to cancel it. + */ using BeforeEdit = xtc_result (*)(void *); + /** + * @brief Creates a piano-roll editor for a MIDI clip. + * + * The pre-edit guard, when non-NULL, is called before every project + * mutation and can veto the edit by returning an error. + * + * @param x Left edge of the widget in the parent's coordinate system. + * @param y Top edge of the widget in the parent's coordinate system. + * @param project Borrowed project that must outlive the widget. + * @param clip ID of the MIDI clip to edit. + * @param before Optional pre-edit guard; NULL means no guard is used. + * @param context Opaque pointer passed to @p before on every edit. + */ PianoRollWidget(int x, int y, xtc_project *project, uint64_t clip, BeforeEdit before, void *context); + /** + * @brief Points the editor at a different project or clip. + * + * Clears any in-progress drag when the project or clip changes and resizes + * the widget to fit the clip's source length. The current selection is not + * changed. + * + * @param project Borrowed project that must outlive the widget. + * @param clip ID of the MIDI clip to display. + */ void set_project(xtc_project *project, uint64_t clip); + /** + * @brief Returns the ID of the clip currently being edited. + * + * @return Clip identifier supplied to the constructor or set_project(). + */ uint64_t clip() const { return clip_; } + /** + * @brief Adds a note to the current clip and selects the new note. + * + * Invokes the pre-edit guard, if any, before mutating the project. + * + * @param start Note start in clip source ticks. + * @param length Note length in ticks. + * @param pitch MIDI pitch from 0 to 127. + * @param velocity MIDI velocity from 1 to 127. + * @param channel MIDI channel from 0 to 15. + * @return XTC_OK on success, the guard's error, or the project error. + */ xtc_result add_note(uint64_t start, uint64_t length, unsigned int pitch, unsigned int velocity = 100, unsigned int channel = 0); + /** + * @brief Removes the selected note from the current clip. + * + * Invokes the pre-edit guard, if any, before mutating the project. The + * selection is cleared only when the removal succeeds. + * + * @return XTC_OK on success, the guard's error, or the project error. + */ xtc_result delete_selected(); + /** + * @brief Handles mouse, keyboard, and focus events for the editor. + * + * Left-click on empty space adds a note, clicking a note selects it, and + * dragging moves or resizes it. Right-click and Delete remove the selected + * note; Escape cancels an in-progress drag. + * + * @param event FLTK event code. + * @return 1 when the event was handled, otherwise the result of the base + * class handler. + */ int handle(int event) override; protected: + /** + * @brief Draws the keyboard, beat grid, and notes of the current clip. + * + * Paints only the background when the clip is missing or is not a MIDI clip. + */ void draw() override; private: @@ -29,7 +98,26 @@ class PianoRollWidget : public Fl_Widget { void *context_; xtc_note_info original_{}; int mode_ = 0, drag_x_ = 0, drag_y_ = 0; + /** + * @brief Reports an edit result and notifies listeners on success. + * + * On success the widget callback runs and the editor is redrawn; on failure + * an alert shows the error and the project is left unchanged. + * + * @param result Result of the attempted project mutation. + * @return The same result code. + */ xtc_result change(xtc_result result); + /** + * @brief Converts a horizontal pixel position to a snapped tick. + * + * The position is measured in absolute window coordinates and snapped down + * to the nearest sixteenth note (240 ticks). + * + * @param px Horizontal position in window coordinates. + * @return Tick offset within the clip source, or 0 for positions on or left + * of the keyboard. + */ uint64_t tick(int px) const; }; #endif diff --git a/src/gui/sequencer_widget.cpp b/src/gui/sequencer_widget.cpp index 2dfbca1..40cc18a 100644 --- a/src/gui/sequencer_widget.cpp +++ b/src/gui/sequencer_widget.cpp @@ -7,12 +7,27 @@ #include #include +/** + * @brief Constructs the timeline widget with a borrowed project and seek callback. + * + * @param left Left coordinate in pixels. + * @param top Top coordinate in pixels. + * @param project Borrowed project; must outlive the widget's use of it. + * @param seek Callback used to request a transport seek. + * @param context Opaque callback context. + */ SequencerWidget::SequencerWidget(int left, int top, xtc_project *project, Seek seek, void *context) : Fl_Widget(left, top, 6300, 3700), project_(project), seek_(seek), context_(context) { extent(); } +/** + * @brief Replaces the borrowed project and refreshes the widget's displayed state. + * + * @param project Borrowed project; must outlive the widget's use of it. + * @param reset Whether to clear selection, cursor, and drag state. + */ void SequencerWidget::set_project(xtc_project *project, bool reset) { project_ = project; @@ -25,12 +40,21 @@ void SequencerWidget::set_project(xtc_project *project, bool reset) redraw(); } +/** + * @brief Converts a screen x coordinate to a bounded timeline tick snapped to a sixteenth note. + * + * @param px Screen x coordinate in pixels. + * @return Snapped tick within the supported project range. + */ uint64_t SequencerWidget::tick(int px) const { return std::min(XTC_PROJECT_MAX_TICK, static_cast(std::max(0, px - x() - 120)) * 960U / static_cast(scale_) / 240U * 240U); } +/** + * @brief Resizes the timeline's scrollable area to fit its tracks and clip ends. + */ void SequencerWidget::extent() { size_t tracks, clips; @@ -47,6 +71,11 @@ void SequencerWidget::extent() parent()->redraw(); } +/** + * @brief Reports an edit result, refreshes the widget, and notifies listeners on success. + * + * @param result Result of the attempted operation. + */ void SequencerWidget::changed(xtc_result result) { if (result != XTC_OK) @@ -58,6 +87,10 @@ void SequencerWidget::changed(xtc_result result) redraw(); } +/** + * @brief Creates and selects a track through the arrangement edit hook. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result SequencerWidget::add_track() { if (before_edit) { @@ -77,6 +110,10 @@ xtc_result SequencerWidget::add_track() return r; } +/** + * @brief Creates and selects a one-bar silent region at the timeline cursor. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result SequencerWidget::add_region() { if (before_edit) { @@ -101,6 +138,10 @@ xtc_result SequencerWidget::add_region() return r; } +/** + * @brief Creates and selects a one-bar MIDI clip at the timeline cursor. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result SequencerWidget::add_midi_clip() { xtc_result r = before_edit ? before_edit(context_) : XTC_OK; @@ -120,6 +161,10 @@ xtc_result SequencerWidget::add_midi_clip() return r; } +/** + * @brief Removes the selected clip, or the selected track when no clip is selected. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result SequencerWidget::delete_selection() { if (before_edit) { @@ -137,6 +182,11 @@ xtc_result SequencerWidget::delete_selection() return r; } +/** + * @brief Adjusts timeline zoom within its supported bounds and updates the scroll extent. + * + * @param direction Zoom direction; positive zooms in, nonpositive zooms out. + */ void SequencerWidget::zoom(int direction) { scale_ = std::clamp(direction > 0 ? scale_ * 2 : scale_ / 2, 24, 384); @@ -144,6 +194,9 @@ void SequencerWidget::zoom(int direction) redraw(); } +/** + * @brief Draws the widget's current model, selection, and interaction feedback. + */ void SequencerWidget::draw() { fl_push_clip(x(), y(), w(), h()); @@ -203,6 +256,12 @@ void SequencerWidget::draw() fl_pop_clip(); } +/** + * @brief Handles FLTK input events and delegates unhandled events to the base widget. + * + * @param event FLTK event code. + * @return Nonzero if the event was handled, zero otherwise. + */ int SequencerWidget::handle(int event) { if (event == FL_PUSH && Fl::event_button() == FL_LEFT_MOUSE) { diff --git a/src/gui/sequencer_widget.hpp b/src/gui/sequencer_widget.hpp index ab1d2a8..40dd412 100644 --- a/src/gui/sequencer_widget.hpp +++ b/src/gui/sequencer_widget.hpp @@ -5,48 +5,124 @@ class SequencerWidget : public Fl_Widget { public: + /** + * @brief Prepares a model edit using the widget's opaque callback context. + * + * @return XTC_OK to allow the edit, or an error to cancel it. + */ using BeforeEdit = xtc_result (*)(void *); BeforeEdit before_edit = nullptr; + /** + * @brief Returns the selected track identifier, or UINT64_MAX when none is selected. + * @return Stable track identifier, or UINT64_MAX if no track is selected. + */ uint64_t selected_track() const { return track_; } + /** + * @brief Returns the selected clip identifier, or UINT64_MAX when none is selected. + * @return Stable clip identifier, or UINT64_MAX if no clip is selected. + */ uint64_t selected_clip() const { return clip_; } + /** + * @brief Requests a transport seek using an opaque context and musical tick. + * + * @return XTC_OK on success, or a transport error. + */ using Seek = xtc_result (*)(void *, uint64_t); + /** + * @brief Constructs the timeline widget with a borrowed project and seek callback. + * + * @param x Left coordinate in pixels. + * @param y Top coordinate in pixels. + * @param project Borrowed project; must outlive the widget's use of it. + * @param seek Callback used to request a transport seek. + * @param context Opaque callback context. + */ SequencerWidget(int x, int y, xtc_project *project, Seek seek, void *context); + /** + * @brief Replaces the borrowed project and refreshes the widget's displayed state. + * + * @param project Borrowed project; must outlive the widget's use of it. + * @param reset Whether to clear selection, cursor, and drag state. + */ void set_project(xtc_project *project, bool reset = true); + /** + * @brief Returns the timeline cursor position in musical ticks. + * @return Cursor position in musical ticks. + */ uint64_t position() const { return cursor_; } + /** + * @brief Moves the timeline cursor to the supplied musical tick and requests a redraw. + * + * @param tick Musical position in ticks. + */ void position(uint64_t tick) { cursor_ = tick; redraw(); } + /** + * @brief Creates and selects a track through the arrangement edit hook. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result add_track(); + /** + * @brief Creates and selects a one-bar silent region at the timeline cursor. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result add_region(); + /** + * @brief Creates and selects a one-bar MIDI clip at the timeline cursor. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result add_midi_clip(); + /** + * @brief Removes the selected clip, or the selected track when no clip is selected. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result delete_selection(); + /** + * @brief Adjusts timeline zoom within its supported bounds and updates the scroll extent. + * + * @param direction Zoom direction; positive zooms in, nonpositive zooms out. + */ void zoom(int direction); + /** + * @brief Returns the borrowed project currently displayed by the widget. + * @return Borrowed project pointer. + */ xtc_project *project() const { return project_; } + /** + * @brief Handles FLTK input events and delegates unhandled events to the base widget. + * + * @param event FLTK event code. + * @return Nonzero if the event was handled, zero otherwise. + */ int handle(int event) override; protected: + /** + * @brief Draws the widget's current model, selection, and interaction feedback. + */ void draw() override; private: @@ -56,8 +132,22 @@ class SequencerWidget : public Fl_Widget { uint64_t track_ = UINT64_MAX, clip_ = UINT64_MAX, cursor_ = 0; int scale_ = 48, drag_x_ = 0, mode_ = 0; xtc_clip_info original_{}; + /** + * @brief Converts a screen x coordinate to a bounded timeline tick snapped to a sixteenth note. + * + * @param px Screen x coordinate in pixels. + * @return Snapped tick within the supported project range. + */ uint64_t tick(int px) const; + /** + * @brief Reports an edit result, refreshes the widget, and notifies listeners on success. + * + * @param result Result of the attempted operation. + */ void changed(xtc_result result); + /** + * @brief Resizes the timeline's scrollable area to fit its tracks and clip ends. + */ void extent(); }; #endif diff --git a/src/main.c b/src/main.c index b14c427..5fd543f 100644 --- a/src/main.c +++ b/src/main.c @@ -6,6 +6,18 @@ #include #include "xtc/xtc_project.h" +/** + * @brief Runs the xtc-check command-line entry point. + * + * Supports --version, --help, and --validate PROJECT. With no arguments it + * creates and destroys the application shell to check core startup/shutdown. + * Any other argument count prints usage to stderr. + * + * @param argc Number of command-line arguments, including the program name. + * @param argv Command-line argument vector; argv[0] is the program name. + * @return EXIT_SUCCESS on success, or EXIT_FAILURE for invalid usage, + * validation failure, or core initialization failure. + */ int main(int argc, char **argv) { if (argc == 2 && !strcmp(argv[1], "--version")) { diff --git a/src/midi/instrument.c b/src/midi/instrument.c index 0540e62..1fdb0d0 100644 --- a/src/midi/instrument.c +++ b/src/midi/instrument.c @@ -32,6 +32,15 @@ struct xtc_midi_plan { uint64_t cutoff[16], next_position, age; }; +/** + * @brief Decodes a complete note or all-notes-off MIDI message into a block event. + * + * @param bytes Borrowed MIDI message bytes. + * @param size Number of message bytes. + * @param offset Sample offset within the current block. + * @param out Receives the operation result. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_midi_decode(const unsigned char *bytes, size_t size, unsigned int offset, xtc_midi_event *out) { xtc_midi_event e; @@ -54,6 +63,13 @@ xtc_result xtc_midi_decode(const unsigned char *bytes, size_t size, unsigned int return XTC_OK; } +/** + * @brief Orders note endpoints by position and endpoint delta for overlap counting. + * + * @param left Borrowed sort element to compare. + * @param right Borrowed sort element to compare. + * @return Negative, zero, or positive according to endpoint ordering. + */ static int compare_endpoint(const void *left, const void *right) { const endpoint *a = left, *b = right; @@ -62,6 +78,11 @@ static int compare_endpoint(const void *left, const void *right) return a->change - b->change; /* End before start at the same sample. */ } +/** + * @brief Clears live voices, scheduling continuity, and panic suppression. + * + * @param p Instrument plan and its single-owner runtime state. + */ void xtc_midi_plan_reset(xtc_midi_plan *p) { if (!p) @@ -72,6 +93,14 @@ void xtc_midi_plan_reset(xtc_midi_plan *p) p->age = 0; } +/** + * @brief Compiles scheduled notes and mixer state into a bounded sine-instrument plan. + * + * @param project Project model; borrowed for this call. + * @param rate Sample rate in hertz. + * @param out Receives the newly allocated object; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int rate, xtc_midi_plan **out) { xtc_midi_plan *p; @@ -155,11 +184,23 @@ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int rate, x return XTC_OK; } +/** + * @brief Releases scheduled notes and instrument state; accepts NULL. + * + * @param p Instrument plan and its single-owner runtime state. + */ void xtc_midi_plan_destroy(xtc_midi_plan *p) { free(p); } +/** + * @brief Clears live voices and suppresses already-started scheduled notes on one MIDI channel. + * + * @param p Instrument plan and its single-owner runtime state. + * @param channel MIDI channel, 0 to 15. + * @param position Absolute sample position at the start of the block. + */ static void channel_off(xtc_midi_plan *p, unsigned int channel, uint64_t position) { size_t i; @@ -169,6 +210,12 @@ static void channel_off(xtc_midi_plan *p, unsigned int channel, uint64_t positio p->cutoff[channel] = position == UINT64_MAX ? position : position + 1; } +/** + * @brief Silences live voices and suppresses scheduled notes already started at the given position. + * + * @param p Instrument plan and its single-owner runtime state. + * @param position Absolute sample position at the start of the block. + */ void xtc_midi_plan_panic(xtc_midi_plan *p, uint64_t position) { unsigned int c; @@ -179,6 +226,13 @@ void xtc_midi_plan_panic(xtc_midi_plan *p, uint64_t position) p->next_position = position; } +/** + * @brief Applies a live MIDI event, stealing the oldest voice when necessary. + * + * @param p Instrument plan and its single-owner runtime state. + * @param e MIDI event to apply. + * @param position Absolute sample position at the start of the block. + */ static void event(xtc_midi_plan *p, const xtc_midi_event *e, uint64_t position) { size_t i, slot = SIZE_MAX; @@ -218,6 +272,15 @@ static void event(xtc_midi_plan *p, const xtc_midi_event *e, uint64_t position) p->voices[slot].age = p->age++; } +/** + * @brief Accumulates scheduled and live sine voices over an event-free sample interval. + * + * @param p Instrument plan and its single-owner runtime state. + * @param position Absolute sample position at the start of the block. + * @param out Destination sample buffer with room for the requested frames. + * @param right Destination right-channel buffer; NULL selects mono rendering. + * @param frames Number of audio frames. + */ static void segment(xtc_midi_plan *p, uint64_t position, float *out, float *right, size_t frames) { size_t i, j; @@ -253,6 +316,18 @@ static void segment(xtc_midi_plan *p, uint64_t position, float *out, float *righ } } +/** + * @brief Renders scheduled and live MIDI with sample-offset events and track gain and pan. + * + * @param p Instrument plan and its single-owner runtime state. + * @param position Absolute sample position at the start of the block. + * @param events Borrowed MIDI events sorted by sample offset. + * @param count Number of MIDI events. + * @param out Destination sample buffer with room for the requested frames. + * @param right Destination right-channel buffer; NULL selects mono rendering. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_midi_plan_render_stereo(xtc_midi_plan *p, uint64_t position, const xtc_midi_event *events, size_t count, float *out, float *right, size_t frames) { @@ -285,6 +360,17 @@ xtc_result xtc_midi_plan_render_stereo(xtc_midi_plan *p, uint64_t position, cons return XTC_OK; } +/** + * @brief Renders scheduled and live MIDI to mono without stereo pan attenuation. + * + * @param p Instrument plan and its single-owner runtime state. + * @param position Absolute sample position at the start of the block. + * @param events Borrowed MIDI events sorted by sample offset. + * @param count Number of MIDI events. + * @param out Destination sample buffer with room for the requested frames. + * @param frames Number of audio frames. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_midi_plan_render(xtc_midi_plan *p, uint64_t position, const xtc_midi_event *events, size_t count, float *out, size_t frames) { diff --git a/src/midi/model.c b/src/midi/model.c index ec3cfa5..523d821 100644 --- a/src/midi/model.c +++ b/src/midi/model.c @@ -2,6 +2,16 @@ #include "../graph/graph_internal.h" #include +/** + * @brief Adds a MIDI clip and ensures an instrument output route exists. + * + * @param p Project model; borrowed for this call. + * @param track Stable track identifier. + * @param start Start position in musical ticks. + * @param length Duration in musical ticks. + * @param out Receives the operation result. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_add_midi_clip(xtc_project *p, uint64_t track, uint64_t start, uint64_t length, uint64_t *out) { xtc_project *c = NULL; @@ -40,6 +50,13 @@ xtc_result xtc_project_add_midi_clip(xtc_project *p, uint64_t track, uint64_t st return r; } +/** + * @brief Copies the number of notes in the project. + * + * @param p Project model; borrowed for this call. + * @param out Receives the operation result. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_note_count(const xtc_project *p, size_t *out) { if (!p || !out) @@ -48,6 +65,14 @@ xtc_result xtc_project_note_count(const xtc_project *p, size_t *out) return XTC_OK; } +/** + * @brief Copies a note snapshot selected by dense list index. + * + * @param p Project model; borrowed for this call. + * @param i Zero-based index in the dense list. + * @param out Receives the operation result. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_get_note(const xtc_project *p, size_t i, xtc_note_info *out) { if (!p || !out) @@ -58,6 +83,18 @@ xtc_result xtc_project_get_note(const xtc_project *p, size_t i, xtc_note_info *o return XTC_OK; } +/** + * @brief Validates a note's MIDI fields and range within a MIDI clip source. + * + * @param p Project model; borrowed for this call. + * @param clip Stable clip identifier. + * @param start Start position in musical ticks. + * @param length Duration in musical ticks. + * @param pitch MIDI note number, 0 to 127. + * @param velocity MIDI note velocity, 1 to 127. + * @param channel MIDI channel, 0 to 15. + * @return XTC_OK on success, or an error code describing the failure. + */ static xtc_result valid_note(const xtc_project *p, uint64_t clip, uint64_t start, uint64_t length, unsigned int pitch, unsigned int velocity, unsigned int channel) { @@ -73,6 +110,19 @@ static xtc_result valid_note(const xtc_project *p, uint64_t clip, uint64_t start return XTC_OK; } +/** + * @brief Adds a validated MIDI note with a new stable identifier. + * + * @param p Project model; borrowed for this call. + * @param clip Stable clip identifier. + * @param start Start position in musical ticks. + * @param length Duration in musical ticks. + * @param pitch MIDI note number, 0 to 127. + * @param velocity MIDI note velocity, 1 to 127. + * @param channel MIDI channel, 0 to 15. + * @param out Receives the operation result. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_add_note(xtc_project *p, uint64_t clip, uint64_t start, uint64_t length, unsigned int pitch, unsigned int velocity, unsigned int channel, uint64_t *out) { @@ -98,6 +148,18 @@ xtc_result xtc_project_add_note(xtc_project *p, uint64_t clip, uint64_t start, u return XTC_OK; } +/** + * @brief Replaces a note's source range, pitch, velocity, and channel after validation. + * + * @param p Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @param start Start position in musical ticks. + * @param length Duration in musical ticks. + * @param pitch MIDI note number, 0 to 127. + * @param velocity MIDI note velocity, 1 to 127. + * @param channel MIDI channel, 0 to 15. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_edit_note(xtc_project *p, uint64_t id, uint64_t start, uint64_t length, unsigned int pitch, unsigned int velocity, unsigned int channel) { @@ -121,6 +183,13 @@ xtc_result xtc_project_edit_note(xtc_project *p, uint64_t id, uint64_t start, ui return XTC_OK; } +/** + * @brief Removes a note selected by stable identifier. + * + * @param p Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_remove_note(xtc_project *p, uint64_t id) { size_t i; diff --git a/src/mixer/mix_internal.h b/src/mixer/mix_internal.h index 104f6fd..cfd7a30 100644 --- a/src/mixer/mix_internal.h +++ b/src/mixer/mix_internal.h @@ -17,8 +17,32 @@ typedef struct { int any_solo; } xtc_mix_snapshot; -xtc_result xtc_mix_prepare(const xtc_project *, unsigned int rate, xtc_mix_snapshot *); -size_t xtc_mix_track(const xtc_mix_snapshot *, uint64_t id); +/** + * @brief Builds a render-ready snapshot from a project's mix and automation. + * + * @param project Project to snapshot; borrowed and not modified. + * @param rate Sample rate used for automation tick conversion. + * @param m Receives the snapshot. + * @return XTC_OK on success, or a validation/conversion error. + */ +xtc_result xtc_mix_prepare(const xtc_project * project, unsigned int rate, xtc_mix_snapshot * m); +/** + * @brief Finds the dense index of a snapshot track by stable identifier. + * + * @param m Mixer snapshot. + * @param id Stable identifier of the object to find or edit. + * @return Dense track index, or SIZE_MAX if absent. + */ +size_t xtc_mix_track(const xtc_mix_snapshot * m, uint64_t id); /* side 0/1 stereo, 2 mono (gain only). */ -float xtc_mix_level(const xtc_mix_snapshot *, size_t track, uint64_t sample, unsigned int side); +/** + * @brief Evaluates track gain, pan, mute, solo, and automation at a sample position. + * + * @param m Mixer snapshot. + * @param track Dense snapshot track index. + * @param sample Absolute sample position. + * @param side Output side: 0 for left, 1 for right, 2 for mono gain only. + * @return Linear level for the chosen side, or zero for an absent or inaudible track. + */ +float xtc_mix_level(const xtc_mix_snapshot * m, size_t track, uint64_t sample, unsigned int side); #endif diff --git a/src/mixer/model.c b/src/mixer/model.c index 41d5646..29262d6 100644 --- a/src/mixer/model.c +++ b/src/mixer/model.c @@ -2,6 +2,13 @@ #include #include +/** + * @brief Finds the dense index of a project track by stable ID. + * + * @param p Project to search; must not be NULL. + * @param id Stable track identifier. + * @return Dense track index, or SIZE_MAX when no track has the ID. + */ static size_t find_track(const xtc_project *p, uint64_t id) { size_t i; @@ -11,6 +18,17 @@ static size_t find_track(const xtc_project *p, uint64_t id) return SIZE_MAX; } +/** + * @brief Sets a track's gain, pan, mute, and solo controls after validation. + * + * @param p Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @param gain Linear gain, 0 to 4 for track controls. + * @param pan Pan position from -1 (left) to 1 (right). + * @param mute Track mute flag, 0 or 1. + * @param solo Track solo flag, 0 or 1. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_set_mix(xtc_project *p, uint64_t id, float gain, float pan, int mute, int solo) { size_t i; @@ -27,6 +45,13 @@ xtc_result xtc_project_set_mix(xtc_project *p, uint64_t id, float gain, float pa return XTC_OK; } +/** + * @brief Inserts an automation point, replacing any point at the same track, parameter, and tick. + * + * @param p Project model; borrowed for this call. + * @param a Automation point to read or update. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_set_automation(xtc_project *p, const xtc_automation_point *a) { size_t i; @@ -49,6 +74,15 @@ xtc_result xtc_project_set_automation(xtc_project *p, const xtc_automation_point return XTC_OK; } +/** + * @brief Removes the automation point at the specified track, parameter, and tick. + * + * @param p Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @param param Automation lane selector. + * @param tick Musical position in ticks. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_remove_automation(xtc_project *p, uint64_t id, xtc_automation_parameter param, uint64_t tick) { size_t i; @@ -63,6 +97,13 @@ xtc_result xtc_project_remove_automation(xtc_project *p, uint64_t id, xtc_automa return XTC_OK; } +/** + * @brief Copies the number of automation points in the project. + * + * @param p Project model; borrowed for this call. + * @param n Receives the item count. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_automation_count(const xtc_project *p, size_t *n) { if (!p || !n) @@ -71,6 +112,14 @@ xtc_result xtc_project_automation_count(const xtc_project *p, size_t *n) return XTC_OK; } +/** + * @brief Copies an automation point selected by dense list index. + * + * @param p Project model; borrowed for this call. + * @param i Zero-based index in the dense list. + * @param a Automation point to read or update. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_get_automation(const xtc_project *p, size_t i, xtc_automation_point *a) { if (!p || !a) diff --git a/src/mixer/render.c b/src/mixer/render.c index 3e9a561..1a1dcf6 100644 --- a/src/mixer/render.c +++ b/src/mixer/render.c @@ -2,6 +2,13 @@ #include #include +/** + * @brief Finds the dense index of a snapshot track by stable ID. + * + * @param m Snapshot to search; must not be NULL. + * @param id Stable track identifier. + * @return Dense track index, or SIZE_MAX when the ID is absent. + */ size_t xtc_mix_track(const xtc_mix_snapshot *m, uint64_t id) { size_t i; @@ -11,6 +18,13 @@ size_t xtc_mix_track(const xtc_mix_snapshot *m, uint64_t id) return SIZE_MAX; } +/** + * @brief Orders automation points by track, parameter, then musical tick. + * + * @param left Borrowed sort element to compare. + * @param right Borrowed sort element to compare. + * @return Negative, zero, or positive according to track, parameter, and tick ordering. + */ static int compare(const void *left, const void *right) { const xtc_mix_point *a = left, *b = right; @@ -21,6 +35,14 @@ static int compare(const void *left, const void *right) return a->tick < b->tick ? -1 : a->tick != b->tick; } +/** + * @brief Copies track controls and compiles sorted automation lanes into sample positions. + * + * @param p Project model; borrowed for this call. + * @param rate Sample rate in hertz. + * @param m Mixer snapshot. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_mix_prepare(const xtc_project *p, unsigned int rate, xtc_mix_snapshot *m) { size_t i; @@ -55,6 +77,15 @@ xtc_result xtc_mix_prepare(const xtc_project *p, unsigned int rate, xtc_mix_snap return XTC_OK; } +/** + * @brief Evaluates a static mixer value or linearly interpolates its automation lane. + * + * @param m Mixer snapshot. + * @param track Dense snapshot track index. + * @param param Automation lane selector. + * @param sample Absolute sample position. + * @return Interpolated automation value, or the track control when its lane is empty. + */ static float value(const xtc_mix_snapshot *m, size_t track, unsigned int param, uint64_t sample) { size_t low = 0, high = m->lengths[track][param], count = high; @@ -77,6 +108,15 @@ static float value(const xtc_mix_snapshot *m, size_t track, unsigned int param, (p[low].value - p[low - 1].value)); } +/** + * @brief Evaluates track gain, pan, mute, solo, and automation at a sample position. + * + * @param m Mixer snapshot. + * @param track Dense snapshot track index. + * @param sample Absolute sample position. + * @param side Output side: 0 for left, 1 for right, 2 for mono gain only. + * @return Linear level for the chosen side, or zero for an absent or inaudible track. + */ float xtc_mix_level(const xtc_mix_snapshot *m, size_t track, uint64_t sample, unsigned int side) { float gain, pan; diff --git a/src/native/native.c b/src/native/native.c index 6f0cafd..3c5837b 100644 --- a/src/native/native.c +++ b/src/native/native.c @@ -9,6 +9,17 @@ struct xtc_native { uint64_t next_position; }; +/** + * @brief Checks that a native descriptor is compatible and complete. + * + * Validates the ABI version and structure size, the kind, the presence of all + * callbacks, and that the identifier is a nonempty printable ASCII string. + * + * @param d Descriptor to validate; must not be NULL. + * @return XTC_OK if the descriptor is usable, XTC_ERR_INVALID_ARGUMENT for a + * missing descriptor or invalid fields, or XTC_ERR_UNSUPPORTED for an + * ABI version or structure-size mismatch. + */ xtc_result xtc_native_validate(const xtc_native_descriptor *d) { size_t i; @@ -29,6 +40,16 @@ xtc_result xtc_native_validate(const xtc_native_descriptor *d) return XTC_ERR_INVALID_ARGUMENT; } +/** + * @brief Copies a native descriptor and prepares a processor instance on the control thread. + * + * @param d Native processor descriptor. + * @param rate Sample rate in hertz. + * @param frames Number of audio frames. + * @param value Value to encode, write, or apply. + * @param out Receives the newly allocated object; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_native_create(const xtc_native_descriptor *d, double rate, size_t frames, float value, xtc_native **out) { xtc_native *p; @@ -57,6 +78,11 @@ xtc_result xtc_native_create(const xtc_native_descriptor *d, double rate, size_t return XTC_OK; } +/** + * @brief Destroys native state and releases the host wrapper; accepts NULL. + * + * @param p Native processor instance. + */ void xtc_native_destroy(xtc_native *p) { if (p) { @@ -65,6 +91,11 @@ void xtc_native_destroy(xtc_native *p) } } +/** + * @brief Resets native state and transport continuity; accepts NULL. + * + * @param p Native processor instance. + */ void xtc_native_reset(xtc_native *p) { if (p) { @@ -73,6 +104,13 @@ void xtc_native_reset(xtc_native *p) } } +/** + * @brief Processes a bounded native block, resetting on discontinuity and silencing callback failures. + * + * @param p Native processor instance. + * @param b Borrowed processing buffers and block metadata. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_native_process(xtc_native *p, const xtc_native_block *b) { size_t i; diff --git a/src/persistence/project_file.c b/src/persistence/project_file.c index e55a261..f828bc1 100644 --- a/src/persistence/project_file.c +++ b/src/persistence/project_file.c @@ -19,6 +19,12 @@ typedef struct { size_t count; } reader; +/** + * @brief Reads and tokenizes one bounded printable-ASCII project record. + * + * @param input Reader holding the file and reusable line/token buffers. + * @return XTC_OK on success, or an error code describing the failure. + */ static xtc_result line(reader *input) { size_t used = 0U; @@ -54,6 +60,13 @@ static xtc_result line(reader *input) return input->count == 0U ? XTC_ERR_FORMAT : XTC_OK; } +/** + * @brief Parses an unsigned decimal integer with overflow detection. + * + * @param text NUL-terminated encoded text. + * @param value Receives the decoded value. + * @return Nonzero for a valid decimal value, zero for empty, malformed, or overflowing input. + */ static int number(const char *text, uint64_t *value) { uint64_t result = 0U; @@ -73,6 +86,12 @@ static int number(const char *text, uint64_t *value) return 1; } +/** + * @brief Converts one ASCII hexadecimal digit to its numeric value. + * + * @param byte ASCII character to decode. + * @return Digit value from 0 to 15, or -1 for a non-hexadecimal character. + */ static int hex_digit(char byte) { if (byte >= '0' && byte <= '9') @@ -84,6 +103,13 @@ static int hex_digit(char byte) return -1; } +/** + * @brief Decodes a hexadecimal project name and validates its UTF-8 contents. + * + * @param text NUL-terminated encoded text. + * @param name NUL-terminated name. + * @return Nonzero for a valid decoded name, zero for malformed or invalid input. + */ static int decode_name(const char *text, char *name) { size_t length = strlen(text), i; @@ -99,6 +125,13 @@ static int decode_name(const char *text, char *name) return xtc_project_valid_name(name); } +/** + * @brief Decodes eight hexadecimal IEEE-754 bits into a finite float. + * + * @param text NUL-terminated encoded text. + * @param value Receives the decoded value. + * @return Nonzero for a finite decoded float, zero for an unsupported representation or invalid input. + */ static int decode_float(const char *text, float *value) { uint32_t bits = 0U; @@ -131,6 +164,13 @@ static int decode_float(const char *text, float *value) return XTC_ERR_FORMAT; \ } while (0) +/** + * @brief Parses versioned project records into a temporary project. + * + * @param input Reader holding the file and reusable line/token buffers. + * @param project Project model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ static xtc_result read_project(reader *input, xtc_project *project) { uint64_t version, value, next_id, node_count, edge_count, format_version; @@ -317,6 +357,13 @@ static xtc_result read_project(reader *input, xtc_project *project) return xtc_project_validate(project); } +/** + * @brief Writes a name as hexadecimal bytes without a terminator. + * + * @param file Open file stream at the required read or write position. + * @param name NUL-terminated name. + * @return Nonzero if every encoded byte was written, zero on a write failure. + */ static int write_name(FILE *file, const char *name) { const unsigned char *cursor = (const unsigned char *)name; @@ -326,6 +373,13 @@ static int write_name(FILE *file, const char *name) return fputc('\n', file) != EOF; } +/** + * @brief Serializes project metadata and graph records in the current format. + * + * @param file Open file stream at the required read or write position. + * @param project Project model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ static xtc_result write_project(FILE *file, const xtc_project *project) { const xtc_graph *graph = project->graph; @@ -402,6 +456,12 @@ static xtc_result write_project(FILE *file, const xtc_project *project) return fputs("END\n", file) == EOF ? XTC_ERR_IO : XTC_OK; } +/** + * @brief Measures a file path up to the supported 4096-byte bound. + * + * @param path NUL-terminated file or asset path. + * @return Path length, capped at 4096; zero for NULL. + */ static size_t path_length(const char *path) { size_t size = 0U; @@ -412,6 +472,13 @@ static size_t path_length(const char *path) return size == 4096U ? 0U : size; } +/** + * @brief Loads and validates a project before transactionally replacing the current state. + * + * @param project Project model; borrowed for this call. + * @param path NUL-terminated file or asset path. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_load(xtc_project *project, const char *path) { xtc_project *candidate = NULL; @@ -436,6 +503,13 @@ xtc_result xtc_project_load(xtc_project *project, const char *path) return result; } +/** + * @brief Saves a versioned project through a synced temporary file and atomic rename. + * + * @param project Project model; borrowed for this call. + * @param path NUL-terminated file or asset path. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_save(const xtc_project *project, const char *path) { size_t length = path_length(path); diff --git a/src/project/history.c b/src/project/history.c index 5468a07..09c2635 100644 --- a/src/project/history.c +++ b/src/project/history.c @@ -10,6 +10,12 @@ struct xtc_history { size_t next_node; }; +/** + * @brief Raises history's identifier high-water marks to include the project's allocations. + * + * @param h Snapshot history handle. + * @param p Project model; borrowed for this call. + */ static void remember_ids(xtc_history *h, const xtc_project *p) { if (p->next_track_id > h->next_track) @@ -22,6 +28,11 @@ static void remember_ids(xtc_history *h, const xtc_project *p) h->next_node = p->graph->next_id; } +/** + * @brief Releases all retained snapshots and history storage; accepts NULL. + * + * @param h Snapshot history handle. + */ void xtc_history_destroy(xtc_history *h) { size_t i; @@ -32,6 +43,13 @@ void xtc_history_destroy(xtc_history *h) free(h); } +/** + * @brief Creates snapshot history with an independent copy of the initial project. + * + * @param p Project model; borrowed for this call. + * @param out Receives the newly allocated object; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_history_create(const xtc_project *p, xtc_history **out) { xtc_history *h; @@ -55,6 +73,13 @@ xtc_result xtc_history_create(const xtc_project *p, xtc_history **out) return XTC_OK; } +/** + * @brief Appends a project snapshot, discarding redo states and evicting the oldest state if full. + * + * @param h Snapshot history handle. + * @param p Project model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_history_record(xtc_history *h, const xtc_project *p) { xtc_project *copy; @@ -79,11 +104,26 @@ xtc_result xtc_history_record(xtc_history *h, const xtc_project *p) return XTC_OK; } +/** + * @brief Checks whether an adjacent undo or redo snapshot exists. + * + * @param h Snapshot history handle. + * @param redo Zero for undo, one for redo. + * @return Nonzero when the requested history state exists, zero otherwise. + */ static int available(const xtc_history *h, int redo) { return redo ? h->cursor + 1 < h->count : h->cursor > 0; } +/** + * @brief Clones the adjacent undo or redo state without moving the history cursor. + * + * @param h Snapshot history handle. + * @param redo Zero for undo, one for redo. + * @param out Receives the newly allocated object; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_history_checkout(const xtc_history *h, int redo, xtc_project **out) { if (!out) @@ -105,6 +145,13 @@ xtc_result xtc_history_checkout(const xtc_history *h, int redo, xtc_project **ou } } +/** + * @brief Moves the history cursor after the checked-out project has been installed. + * + * @param h Snapshot history handle. + * @param redo Zero for undo, one for redo. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_history_accept(xtc_history *h, int redo) { if (!h || (redo != 0 && redo != 1)) diff --git a/src/project/project.c b/src/project/project.c index f571442..a6a409b 100644 --- a/src/project/project.c +++ b/src/project/project.c @@ -5,6 +5,12 @@ #include #include +/** + * @brief Checks for a nonempty UTF-8 name of at most 63 bytes without ASCII controls. + * + * @param name NUL-terminated name. + * @return Nonzero for a valid name, zero otherwise. + */ int xtc_project_valid_name(const char *name) { size_t length = 0U, i = 0U; @@ -43,6 +49,13 @@ int xtc_project_valid_name(const char *name) return 1; } +/** + * @brief Finds a project's dense track index by stable identifier. + * + * @param project Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @return Dense track index, or SIZE_MAX if absent. + */ static size_t track_index(const xtc_project *project, uint64_t id) { size_t i; @@ -52,6 +65,13 @@ static size_t track_index(const xtc_project *project, uint64_t id) return SIZE_MAX; } +/** + * @brief Finds a project's dense clip index by stable identifier. + * + * @param project Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @return Dense clip index, or SIZE_MAX if absent. + */ static size_t clip_index(const xtc_project *project, uint64_t id) { size_t i; @@ -61,12 +81,27 @@ static size_t clip_index(const xtc_project *project, uint64_t id) return SIZE_MAX; } +/** + * @brief Checks that a nonempty timeline range and its source slice fit their bounds. + * + * @param start Timeline start tick. + * @param length Nonzero duration in ticks. + * @param offset Source offset in ticks. + * @param source Source duration in ticks. + * @return Nonzero if both timeline and source bounds are valid, zero otherwise. + */ static int valid_range(uint64_t start, uint64_t length, uint64_t offset, uint64_t source) { return start <= XTC_PROJECT_MAX_TICK && length != 0U && length <= XTC_PROJECT_MAX_TICK - start && source <= XTC_PROJECT_MAX_TICK && offset <= source && length <= source - offset; } +/** + * @brief Allocates an empty project with an output-only graph policy and 120 BPM tempo. + * + * @param out_project Receives the newly allocated project; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_create(xtc_project **out_project) { xtc_project *project; @@ -88,6 +123,11 @@ xtc_result xtc_project_create(xtc_project **out_project) return XTC_OK; } +/** + * @brief Releases a project and its graph; accepts NULL. + * + * @param project Project model; borrowed for this call. + */ void xtc_project_destroy(xtc_project *project) { if (project != NULL) { @@ -96,11 +136,24 @@ void xtc_project_destroy(xtc_project *project) } } +/** + * @brief Returns the project's borrowed graph, or NULL for a NULL project. + * + * @param project Project model; borrowed for this call. + * @return Borrowed graph valid until project mutation or destruction, or NULL for NULL. + */ const xtc_graph *xtc_project_graph(const xtc_project *project) { return project == NULL ? NULL : project->graph; } +/** + * @brief Copies and validates a graph before replacing the project's graph. + * + * @param project Project model; borrowed for this call. + * @param source Graph model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_set_graph(xtc_project *project, const xtc_graph *source) { xtc_graph *copy; @@ -126,6 +179,13 @@ xtc_result xtc_project_set_graph(xtc_project *project, const xtc_graph *source) return XTC_OK; } +/** + * @brief Allocates an independent copy of project metadata and its graph. + * + * @param source Project model; borrowed for this call. + * @param out_project Receives the newly allocated project; caller owns it on success. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_clone(const xtc_project *source, xtc_project **out_project) { xtc_project *copy; @@ -149,6 +209,14 @@ xtc_result xtc_project_clone(const xtc_project *source, xtc_project **out_projec return XTC_OK; } +/** + * @brief Copies the current track and clip counts. + * + * @param project Project model; borrowed for this call. + * @param tracks Receives the number of tracks. + * @param clips Receives the number of clips. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_get_counts(const xtc_project *project, size_t *tracks, size_t *clips) { if (project == NULL || tracks == NULL || clips == NULL) @@ -158,6 +226,14 @@ xtc_result xtc_project_get_counts(const xtc_project *project, size_t *tracks, si return XTC_OK; } +/** + * @brief Copies a track snapshot selected by dense list index. + * + * @param project Project model; borrowed for this call. + * @param index Zero-based index in the dense list. + * @param track Receives a copy of the track metadata. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_get_track(const xtc_project *project, size_t index, xtc_track_info *track) { if (project == NULL || track == NULL) @@ -168,6 +244,14 @@ xtc_result xtc_project_get_track(const xtc_project *project, size_t index, xtc_t return XTC_OK; } +/** + * @brief Copies a clip snapshot selected by dense list index. + * + * @param project Project model; borrowed for this call. + * @param index Zero-based index in the dense list. + * @param clip Receives a copy of the clip metadata. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_get_clip(const xtc_project *project, size_t index, xtc_clip_info *clip) { if (project == NULL || clip == NULL) @@ -178,6 +262,14 @@ xtc_result xtc_project_get_clip(const xtc_project *project, size_t index, xtc_cl return XTC_OK; } +/** + * @brief Copies a clip snapshot selected by stable identifier. + * + * @param project Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @param clip Receives a copy of the clip metadata. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_find_clip(const xtc_project *project, uint64_t id, xtc_clip_info *clip) { if (project == NULL) @@ -185,6 +277,14 @@ xtc_result xtc_project_find_clip(const xtc_project *project, uint64_t id, xtc_cl return xtc_project_get_clip(project, clip_index(project, id), clip); } +/** + * @brief Appends a track with unity gain and a new stable identifier. + * + * @param project Project model; borrowed for this call. + * @param name NUL-terminated name. + * @param id Receives the new stable identifier. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_add_track(xtc_project *project, const char *name, uint64_t *id) { xtc_track_info track = {0}; @@ -200,6 +300,13 @@ xtc_result xtc_project_add_track(xtc_project *project, const char *name, uint64_ return XTC_OK; } +/** + * @brief Removes a track and its clips, notes, and automation. + * + * @param project Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_remove_track(xtc_project *project, uint64_t id) { size_t index, i, count = 0U; @@ -228,6 +335,17 @@ xtc_result xtc_project_remove_track(xtc_project *project, uint64_t id) return XTC_OK; } +/** + * @brief Appends a silent region on an existing track with a new stable identifier. + * + * @param project Project model; borrowed for this call. + * @param track Stable track identifier. + * @param name NUL-terminated name. + * @param start Start position in musical ticks. + * @param length Duration in musical ticks. + * @param id Receives the new stable identifier. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_add_clip(xtc_project *project, uint64_t track, const char *name, uint64_t start, uint64_t length, uint64_t *id) { @@ -248,6 +366,13 @@ xtc_result xtc_project_add_clip(xtc_project *project, uint64_t track, const char return XTC_OK; } +/** + * @brief Removes a clip and its notes. + * + * @param project Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_remove_clip(xtc_project *project, uint64_t id) { size_t index; @@ -269,6 +394,17 @@ xtc_result xtc_project_remove_clip(xtc_project *project, uint64_t id) return XTC_OK; } +/** + * @brief Atomically moves or trims a clip within its existing source bounds. + * + * @param project Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @param track Stable track identifier. + * @param start Start position in musical ticks. + * @param length Duration in musical ticks. + * @param offset Sample offset within the current block. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_edit_clip(xtc_project *project, uint64_t id, uint64_t track, uint64_t start, uint64_t length, uint64_t offset) { @@ -287,11 +423,24 @@ xtc_result xtc_project_edit_clip(xtc_project *project, uint64_t id, uint64_t tra return XTC_OK; } +/** + * @brief Returns the constant tempo in milli-BPM, or zero for a NULL project. + * + * @param project Project model; borrowed for this call. + * @return Tempo in milli-BPM, or zero for NULL. + */ unsigned int xtc_project_tempo(const xtc_project *project) { return project == NULL ? 0U : project->tempo; } +/** + * @brief Sets the constant tempo within the range 20000 to 300000 milli-BPM. + * + * @param project Project model; borrowed for this call. + * @param tempo Tempo in milli-BPM. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_set_tempo(xtc_project *project, unsigned int tempo) { if (project == NULL || tempo < 20000U || tempo > 300000U) @@ -300,6 +449,15 @@ xtc_result xtc_project_set_tempo(xtc_project *project, unsigned int tempo) return XTC_OK; } +/** + * @brief Converts a musical tick to an absolute sample position, rounding down. + * + * @param project Project model; borrowed for this call. + * @param tick Musical position in ticks. + * @param rate Sample rate in hertz. + * @param sample Receives the sample position. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_tick_to_sample(const xtc_project *project, uint64_t tick, unsigned int rate, uint64_t *sample) { if (project == NULL || sample == NULL || rate == 0U || rate > 768000U) @@ -310,6 +468,15 @@ xtc_result xtc_project_tick_to_sample(const xtc_project *project, uint64_t tick, return XTC_OK; } +/** + * @brief Converts an absolute sample position to a musical tick, rounding down. + * + * @param project Project model; borrowed for this call. + * @param sample Absolute sample position. + * @param rate Sample rate in hertz. + * @param tick Receives the musical tick. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_sample_to_tick(const xtc_project *project, uint64_t sample, unsigned int rate, uint64_t *tick) { uint64_t scale, value, divisor, whole; @@ -327,6 +494,12 @@ xtc_result xtc_project_sample_to_tick(const xtc_project *project, uint64_t sampl return XTC_OK; } +/** + * @brief Validates project IDs, timeline ranges, names, graph, notes, and automation. + * + * @param project Project model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_validate(const xtc_project *project) { size_t i, j; @@ -395,6 +568,14 @@ xtc_result xtc_project_validate(const xtc_project *project) return XTC_OK; } +/** + * @brief Sets an absolute audio asset reference, or clears it to make a region silent. + * + * @param project Project model; borrowed for this call. + * @param id Stable identifier of the object to find or edit. + * @param path NUL-terminated file or asset path. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_set_clip_asset(xtc_project *project, uint64_t id, const char *path) { size_t index, length = 0U, i; diff --git a/src/project/project_internal.h b/src/project/project_internal.h index 3a53804..bc93d08 100644 --- a/src/project/project_internal.h +++ b/src/project/project_internal.h @@ -19,6 +19,18 @@ struct xtc_project { xtc_clip_info clips[XTC_PROJECT_MAX_CLIPS]; }; +/** + * @brief Checks for a nonempty UTF-8 name of at most 63 bytes without ASCII controls. + * + * @param name NUL-terminated name. + * @return Nonzero for a valid name, zero otherwise. + */ int xtc_project_valid_name(const char *name); +/** + * @brief Validates project IDs, timeline ranges, names, graph, notes, and automation. + * + * @param project Project model; borrowed for this call. + * @return XTC_OK on success, or an error code describing the failure. + */ xtc_result xtc_project_validate(const xtc_project *project); #endif