diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3df08aa..99ef855 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -116,4 +116,263 @@ RECOVERY.md. Native graphs remain session-only, with unsupported saves preservin files. No stable binary plugin ABI, external host, system package, autosave or physical-device/endurance qualification is claimed. +## Phase 10 — Engine Correctness and Reliability + +**Goal:** Verify that the existing implementation behaves correctly under realistic workloads before adding more features. + +### Tasks + +- Audit graph compilation, topological ordering, port compatibility, and cycle detection. +- Test graph replacement while audio is running, including safe reclamation of old processing graphs. +- Verify that audio-thread operations are bounded and non-blocking. +- Test repeated initialization, start/stop cycles, device failures, shutdown, and partial initialization. +- Run AddressSanitizer, UndefinedBehaviorSanitizer, leak checks, and static analysis where available. +- Add regression tests for every bug discovered. +- Document known limitations and thread ownership for shared resources. + +### Acceptance criteria + +- Reproducible tests cover engine lifecycle, graph changes, error handling, and DSP output. +- No known critical real-time safety defects remain. +- Resource ownership and cleanup behavior are documented. + +## Phase 11 — Complete Minimal Music-Making Workflow + +**Goal:** Ensure a user can create, save, reopen, and play a small musical project without manually editing project files. + +### Tasks + +- Verify project creation, track creation, clip arrangement, routing, and transport as one integrated workflow. +- Ensure play, pause, stop, seek, and return-to-start behavior are consistent. +- Save, close, reopen, and continue editing projects. +- Provide useful defaults and actionable error messages. +- Keep the sequencer as the default workspace, with the modular graph available when needed. +- Test that GUI state and engine state remain synchronized. + +### Acceptance criteria + +- A user can create and reopen a small project and reproduce the same arrangement and routing. +- Common operations do not require manual project-file edits. +- Transport and GUI state remain consistent during normal playback and error conditions. + +## Phase 12 — Audio Quality and Transport Correctness + +**Goal:** Make timing and audio output predictable and musically reliable. + +### Tasks + +- Define precise behavior for tempo and meter changes. +- Verify conversion between musical positions and sample positions without accumulated timing drift. +- Test clips and events that cross processing-block boundaries. +- Prevent clicks at clip boundaries and during playback start/stop where appropriate. +- Implement gain ramps and parameter smoothing where abrupt changes could create artifacts. +- Test silence, clipping, denormal values, channel layouts, and unusual supported buffer sizes. +- Document latency reporting and identify whether latency compensation is required by the current architecture. +- Add deterministic tests for sample-accurate event scheduling where supported. + +### Acceptance criteria + +- Automated tests verify timing conversions and expected sample output. +- Tests cover events and clips at block boundaries and tempo changes. +- Listening tests cover transitions, start/stop behavior, and representative projects. +- Known limitations involving latency and synchronization are documented. + +## Phase 13 — Recording and Recovery + +**Goal:** Protect recorded material and handle recording failures predictably. + +### Tasks + +- Test long recordings and recording-buffer saturation. +- Handle disk-full conditions, write errors, interrupted recording, and unavailable destinations. +- Keep file I/O outside the real-time audio callback. +- Define and implement recovery or clear reporting for incomplete recordings. +- Make recording destinations and supported file formats explicit. +- Protect existing files from accidental overwrites. +- Test reopening projects when media files are missing, incomplete, or inaccessible. +- Ensure shutdown and project closure do not silently discard pending recorded data. + +### Acceptance criteria + +- Recording failures are reported clearly and do not cause unsafe audio-thread behavior. +- Valid recorded material is preserved whenever possible. +- Incomplete recordings and missing media are detected and explained. +- Recording and recovery behavior is covered by automated or repeatable integration tests. + +## Phase 14 — Native Plugin, Instrument, and Effect Architecture + +**Goal:** Provide a stable internal extension interface before hosting external plugins. + +### Tasks + +- Finalize the native C node API, including initialization, processing, reset, and destruction. +- Define parameter metadata, defaults, valid ranges, and automation behavior. +- Specify audio channel layouts and MIDI/event handling. +- Define node state serialization and restoration requirements. +- Establish rules for node creation, failure, reconfiguration, and destruction. +- Ensure the GUI uses documented public APIs rather than internal engine structures. +- Provide a small example instrument and effect implemented against the public interface. +- Document ABI/API compatibility expectations for native extensions. + +### Acceptance criteria + +- A developer can implement a small native instrument or effect using documented interfaces. +- Invalid parameters, unsupported layouts, and failed initialization are handled safely. +- Node state can be saved and restored where required. +- No C++ or FLTK types leak into the public C API. + +## Phase 15 — External Plugin Hosting + +**Goal:** Add external plugin support only after the internal node lifecycle is reliable. + +### Tasks + +- Evaluate candidate plugin formats for licensing, dependencies, maintenance, and Linux support. +- Record the selected format and rationale in an architecture decision document. +- Implement plugin discovery and loading outside the real-time callback. +- Map plugin audio ports, parameters, MIDI/events, and channel layouts to the engine's node model. +- Support plugin state save/load and clear reporting for missing or incompatible plugins. +- Test plugins with different channel counts, parameter sets, and processing requirements. +- Define how plugin failures are reported and how the engine recovers. +- Document crash-isolation limitations; an in-process plugin must not be assumed safe merely because the host is safe. + +### Acceptance criteria + +- A small, documented set of compatible third-party plugins can be loaded, connected, saved, reopened, and played. +- Missing or incompatible plugins do not silently corrupt the project. +- Plugin loading and destruction do not occur in the real-time callback. +- The chosen format's dependencies and licensing are documented. + +## Phase 16 — Automation and Expressive Control + +**Goal:** Make automation and MIDI control reliable, sample-timed, and usable. + +### Tasks + +- Support automation for common parameters such as volume, pan, instrument parameters, and effect parameters. +- Define interpolation modes and behavior at clip boundaries. +- Evaluate automation at the correct time and sample offsets for each processing block. +- Smooth parameter changes where needed to avoid zipper noise or audio artifacts. +- Add MIDI controller mapping where appropriate. +- Handle note-off, panic, seek, stop, project teardown, and device disconnect correctly. +- Define deterministic ordering for MIDI and automation events that occur at the same sample position. +- Test overlapping notes, repeated note-ons, and interrupted playback. + +### Acceptance criteria + +- Automation and MIDI control reproduce consistent results across repeated playback and project reloads. +- Held notes are released safely on stop, panic, seek, and relevant failure paths. +- Event ordering and interpolation behavior are documented and tested. + +## Phase 17 — Editing Workflow, Undo/Redo, and Usability + +**Goal:** Make common editing tasks efficient and protect users from accidental changes. + +### Tasks + +- Make common operations undoable, including create, delete, move, trim, duplicate, and connect. +- Define undo behavior for recording and destructive operations. +- Add consistent keyboard shortcuts and context menus for frequently used actions. +- Implement predictable snapping, grid settings, zoom, scrolling, and selection behavior. +- Improve clip labels, track organization, and visual feedback. +- Make destructive actions difficult to trigger accidentally. +- Keep the GUI responsive while audio is running. +- Ensure undo/redo updates project state and the active engine graph consistently. +- Test keyboard focus and accessibility of essential controls where practical. + +### Acceptance criteria + +- Common editing workflows can be completed without unnecessary dialogs or manual workarounds. +- Undo/redo restores the intended project state and corresponding engine state. +- GUI operations do not block the real-time audio thread. +- Interaction behavior is documented and covered by relevant tests. + +## Phase 18 — Project Format, Compatibility, and Portability + +**Goal:** Keep project files valid, recoverable, and maintainable over time. + +### Tasks + +- Version the project format and implement explicit migration logic when the format changes. +- Validate project data before constructing engine objects from it. +- Handle malformed files, unknown node types, missing assets, and unavailable plugins. +- Define project-folder behavior and reliable media path handling. +- Make saving robust against interrupted writes, for example by writing a temporary file and replacing the destination safely where supported. +- Test save/load round trips and migration from every format version that is supported. +- Keep the format documented and preferably human-inspectable. +- Avoid silently discarding unknown data during load/save unless the format policy explicitly requires it. + +### Acceptance criteria + +- Supported older projects load correctly or produce clear, actionable compatibility errors. +- Malformed project files do not crash the application or trigger unsafe allocations. +- Missing media and plugins are reported clearly. +- Save/load round trips and format migrations have automated tests. + +## Phase 19 — Performance, Scalability, and Stress Testing + +**Goal:** Measure performance and establish predictable behavior as projects grow. + +### Tasks + +- Create repeatable benchmarks for increasing node counts and audio channel counts. +- Measure processing time at supported sample rates and block sizes. +- Profile graph compilation separately from real-time audio processing. +- Identify unnecessary buffer copies, allocations, and redundant processing. +- Test large arrangements, many clips, automation events, and active meters. +- Define bounded processing limits and meaningful overload diagnostics. +- Test under CPU pressure and during frequent GUI redraws. +- Optimize only after profiling identifies a meaningful bottleneck. +- Record baseline benchmark results so regressions can be detected. + +### Acceptance criteria + +- Performance targets and test conditions are documented. +- Repeatable benchmarks cover representative small and large projects. +- Overload behavior is defined and does not cause unbounded work in the callback. +- Significant performance regressions can be detected through repeatable tests. + +## Phase 20 — Release Engineering, Packaging, and Maintenance + +**Goal:** Make the application straightforward to build, install, use, diagnose, and maintain. + +### Tasks + +- Provide reproducible release builds and clean build instructions. +- Add Makefile targets for building, testing, installing, and uninstalling. +- Package the application and required resources for the initial supported Linux target. +- Review licenses and attribution for direct and bundled dependencies. +- Document supported audio backends, required system packages, and configuration. +- Add useful crash and error diagnostics without compromising real-time processing. +- Test fresh installations, upgrades, missing dependencies, and removal. +- Publish a basic user manual, developer documentation, and a known-limitations list. +- Define a release checklist and a policy for handling regressions and project-format compatibility. + +### Acceptance criteria + +- A person other than the primary developer can build or install the application using the documented steps. +- The installed application can complete the supported core workflow. +- Installation and removal are tested on the initial target environment. +- Dependencies, licensing, limitations, and troubleshooting steps are documented. +- A release can be reproduced from a clean checkout with recorded build instructions. + +--- + +## Recommended Execution Order + +Reorder the phases when the audit reveals a blocking dependency. Do not mark a phase complete merely because code exists: its acceptance criteria must be demonstrated by builds, tests, or documented manual verification. + +## Definition of Done for Each Phase + +A phase is complete only when: + +- The project builds with warnings enabled. +- Relevant automated tests pass, and manual tests are documented where automation is impractical. +- Error paths, resource ownership, and thread boundaries have been reviewed. +- Real-time audio constraints remain satisfied. +- Documentation is updated alongside API or behavior changes. +- No unrelated functionality was silently removed. +- Untested behavior and known limitations are reported honestly. +- The phase's acceptance criteria are explicitly checked off. + Do not skip phases without documenting why and preserving acceptance criteria. diff --git a/include/xtc/xtc_audio.h b/include/xtc/xtc_audio.h index 4096919..6d65285 100644 --- a/include/xtc/xtc_audio.h +++ b/include/xtc/xtc_audio.h @@ -1,33 +1,35 @@ #ifndef XTC_AUDIO_H #define XTC_AUDIO_H + #include "xtc_project.h" + #ifdef __cplusplus extern "C" { #endif + typedef struct xtc_audio_plan xtc_audio_plan; typedef struct xtc_recorder xtc_recorder; + /* Control-thread WAV import: PCM16/24/32 or float32, mono/stereo downmixed to * 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. */ -xtc_result xtc_project_import_audio(xtc_project *project, uint64_t track, - const char *path, uint64_t start_tick, uint64_t *out_clip); +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. */ 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. */ -xtc_result xtc_audio_plan_create(const xtc_project *project, unsigned int rate, - xtc_audio_plan **out_plan); + * 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. */ +xtc_result xtc_audio_plan_create(const xtc_project *project, unsigned int rate, xtc_audio_plan **out_plan); void xtc_audio_plan_destroy(xtc_audio_plan *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); -xtc_result xtc_audio_plan_render(const xtc_audio_plan *plan, uint64_t position, - float *output, size_t frames); +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. * push accepts whole blocks; overflow is sticky and all later input is dropped. @@ -35,14 +37,15 @@ 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. */ -xtc_result xtc_recorder_create(const char *path, unsigned int rate, size_t capacity, - xtc_recorder **out_recorder); +xtc_result xtc_recorder_create(const char *path, unsigned int rate, size_t capacity, xtc_recorder **out_recorder); void xtc_recorder_push(xtc_recorder *recorder, const float *input, size_t frames); xtc_result xtc_recorder_drain(xtc_recorder *recorder); xtc_result xtc_recorder_finish(xtc_recorder *recorder); xtc_result xtc_recorder_status(const xtc_recorder *recorder, uint64_t *out_frames); void xtc_recorder_destroy(xtc_recorder *recorder); + #ifdef __cplusplus } #endif + #endif diff --git a/include/xtc/xtc_backend.h b/include/xtc/xtc_backend.h index 679f6f8..c68495f 100644 --- a/include/xtc/xtc_backend.h +++ b/include/xtc/xtc_backend.h @@ -1,20 +1,27 @@ #ifndef XTC_BACKEND_H #define XTC_BACKEND_H + #include "xtc_engine.h" #include "xtc_audio.h" #include "xtc_midi.h" + #ifdef __cplusplus extern "C" { #endif typedef struct xtc_backend xtc_backend; -typedef enum { XTC_BACKEND_JACK = 1 } xtc_backend_kind; + +typedef enum { + XTC_BACKEND_JACK = 1 +} xtc_backend_kind; + typedef struct { xtc_backend_kind kind; const char *client_name; /* Borrowed for open only; nonempty. */ size_t max_frames; /* 1..65536, at least the server's current block. */ size_t channel_count; /* 1 mono or 2 stereo output; input/recording remain mono. */ } xtc_backend_config; + typedef struct { float peak_left, peak_right; /* Last-block output peaks, zero while stopped. */ unsigned int sample_rate; @@ -23,7 +30,7 @@ typedef struct { unsigned int processed_blocks; unsigned int midi_dropped; /* Malformed/overflowed input; unsigned wrap. */ unsigned int asset_issues; /* Unique missing/unreadable/unsupported assets. */ - uint64_t sample_position; /* Successful callback frames; retained across stop/submit. */ + uint64_t sample_position; /* Successful callback frames; retained across stop/submit. */ unsigned int submitted_revision, active_revision; int running; int server_lost; @@ -66,22 +73,21 @@ xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_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); -xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction direction, - const char **out_name); +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. */ typedef int (*xtc_backend_port_visitor)(const char *name, void *context); -xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction direction, - xtc_backend_port_visitor visitor, void *context); +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. */ -xtc_result xtc_backend_connect(xtc_backend *backend, xtc_port_direction own_direction, - const char *remote_name); -xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction own_direction, - const char *remote_name); +xtc_result xtc_backend_connect(xtc_backend *backend, xtc_port_direction own_direction, const char *remote_name); +xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction own_direction, const char *remote_name); + #ifdef __cplusplus } #endif + #endif diff --git a/include/xtc/xtc_engine.h b/include/xtc/xtc_engine.h index 9159568..2266889 100644 --- a/include/xtc/xtc_engine.h +++ b/include/xtc/xtc_engine.h @@ -33,9 +33,9 @@ typedef enum { XTC_NODE_OSCILLATOR, XTC_NODE_METER, XTC_NODE_OUTPUT, - XTC_NODE_AUDIO, /* Timeline mix source, output only. */ + XTC_NODE_AUDIO, /* Timeline mix source, output only. */ XTC_NODE_INSTRUMENT, /* Internal MIDI instrument, output only. */ - XTC_NODE_NATIVE /* Trusted source/effect; see xtc_native.h. */ + XTC_NODE_NATIVE /* Trusted source/effect; see xtc_native.h. */ } xtc_node_type; typedef struct { @@ -48,18 +48,32 @@ typedef struct { * monotonically assigned identifiers; removed IDs are never reused. Output * parameters are written only on success, except create/compile/apply_edit * which set their output pointer to NULL on failure. */ -typedef struct { size_t id; xtc_node_type type; float value; } xtc_node_info; +typedef struct { + size_t id; + xtc_node_type type; + float value; +} xtc_node_info; + typedef struct { size_t source_node, source_port, destination_node, destination_port; } xtc_connection_info; + typedef enum { - XTC_EDIT_ADD_NODE = 1, XTC_EDIT_REMOVE_NODE, - XTC_EDIT_CONNECT, XTC_EDIT_DISCONNECT + XTC_EDIT_ADD_NODE = 1, + XTC_EDIT_REMOVE_NODE, + XTC_EDIT_CONNECT, + XTC_EDIT_DISCONNECT } xtc_graph_edit_kind; + typedef struct { xtc_graph_edit_kind kind; + union { - struct { xtc_node_type type; float value; } add; + struct { + xtc_node_type type; + float value; + } add; + size_t node_id; xtc_connection_info connection; } data; @@ -76,22 +90,18 @@ 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. */ -xtc_result xtc_graph_apply_edit(const xtc_graph *source, const xtc_graph_edit *edit, - xtc_graph **out_graph, size_t *out_added_id); +xtc_result xtc_graph_apply_edit(const xtc_graph *source, const xtc_graph_edit *edit, xtc_graph **out_graph, + size_t *out_added_id); xtc_result xtc_graph_create(size_t max_nodes, xtc_graph **out_graph); void xtc_graph_destroy(xtc_graph *graph); xtc_result xtc_graph_add_gain(xtc_graph *graph, float gain, size_t *out_node_id); -xtc_result xtc_graph_connect(xtc_graph *graph, size_t source_node, - size_t destination_node); +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. */ -xtc_result xtc_graph_get_port(const xtc_graph *graph, size_t node_id, - size_t port_id, xtc_port_info *out_port); -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); -xtc_result xtc_graph_add_oscillator(xtc_graph *graph, float frequency, - size_t *out_node_id); +xtc_result xtc_graph_get_port(const xtc_graph *graph, size_t node_id, size_t port_id, xtc_port_info *out_port); +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); +xtc_result xtc_graph_add_oscillator(xtc_graph *graph, float frequency, size_t *out_node_id); xtc_result xtc_graph_add_meter(xtc_graph *graph, size_t *out_node_id); xtc_result xtc_graph_add_instrument(xtc_graph *graph, size_t *out_node_id); xtc_result xtc_graph_add_audio(xtc_graph *graph, size_t *out_node_id); @@ -99,8 +109,7 @@ 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. */ -xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate, - size_t max_frames, xtc_plan **out_plan); +xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate, size_t max_frames, xtc_plan **out_plan); 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. @@ -113,16 +122,15 @@ 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, const float *midi_left, const float *midi_right, - float *left, float *right, size_t frames); -xtc_result xtc_plan_process_sources(xtc_plan *plan, const float *input, const float *timeline, - const float *instrument, float *output, size_t frames); +xtc_result xtc_plan_process_stereo(xtc_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); +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. */ -xtc_result xtc_plan_process_audio(xtc_plan *plan, const float *input, const float *timeline, - float *output, size_t frames); -xtc_result xtc_plan_process(xtc_plan *plan, const float *input, float *output, - size_t frame_count); +xtc_result xtc_plan_process_audio(xtc_plan *plan, const float *input, const float *timeline, float *output, + size_t frames); +xtc_result xtc_plan_process(xtc_plan *plan, const float *input, float *output, size_t frame_count); /* Resets oscillator phases and meters, without allocation. */ xtc_result xtc_plan_reset(xtc_plan *plan); /* Last block absolute peak, valid for meter nodes only. */ @@ -130,9 +138,8 @@ xtc_result xtc_plan_meter_peak(const xtc_plan *plan, size_t node_id, float *out_ 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. */ -xtc_result xtc_graph_process_offline(xtc_graph *graph, const float *input, - float *output, size_t frame_count, - float input_gain); +xtc_result xtc_graph_process_offline(xtc_graph *graph, const float *input, float *output, size_t frame_count, + float input_gain); #ifdef __cplusplus } diff --git a/include/xtc/xtc_midi.h b/include/xtc/xtc_midi.h index 3a5e61b..eac5eab 100644 --- a/include/xtc/xtc_midi.h +++ b/include/xtc/xtc_midi.h @@ -1,40 +1,50 @@ #ifndef XTC_MIDI_H #define XTC_MIDI_H + #include "xtc_project.h" + #ifdef __cplusplus extern "C" { #endif + #define XTC_PROJECT_MAX_NOTES 4096U #define XTC_MIDI_MAX_EVENTS 256U #define XTC_MIDI_VOICES 32U + typedef struct { uint64_t id, clip_id, start_tick, length_tick; /* Positions in clip source. */ - unsigned int pitch, velocity, channel; /* 0..127, 1..127, 0..15. */ + unsigned int pitch, velocity, channel; /* 0..127, 1..127, 0..15. */ } xtc_note_info; -typedef enum { XTC_MIDI_NOTE_ON = 1, XTC_MIDI_NOTE_OFF, XTC_MIDI_ALL_OFF } xtc_midi_type; + +typedef enum { + XTC_MIDI_NOTE_ON = 1, + XTC_MIDI_NOTE_OFF, + XTC_MIDI_ALL_OFF +} xtc_midi_type; + typedef struct { unsigned int offset; /* Sample offset within THIS block, strictly < frames. */ xtc_midi_type type; unsigned int channel, pitch, velocity; } xtc_midi_event; 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. */ -xtc_result xtc_project_add_midi_clip(xtc_project *project, uint64_t track, - uint64_t start, uint64_t length, uint64_t *out_id); +xtc_result xtc_project_add_midi_clip(xtc_project *project, uint64_t track, uint64_t start, uint64_t length, + uint64_t *out_id); xtc_result xtc_project_note_count(const xtc_project *project, size_t *out_count); xtc_result xtc_project_get_note(const xtc_project *project, size_t index, xtc_note_info *out_note); -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); -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); +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); +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); 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. */ -xtc_result xtc_midi_decode(const unsigned char *bytes, size_t size, unsigned int offset, - xtc_midi_event *out_event); +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. */ @@ -50,10 +60,13 @@ void xtc_midi_plan_panic(xtc_midi_plan *plan, uint64_t position); * 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); -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); +xtc_result xtc_midi_plan_render_stereo(xtc_midi_plan *, uint64_t, const xtc_midi_event *, size_t, float *left, + float *right, size_t); +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); + #ifdef __cplusplus } #endif + #endif diff --git a/include/xtc/xtc_mixer.h b/include/xtc/xtc_mixer.h index 8473c24..8a81165 100644 --- a/include/xtc/xtc_mixer.h +++ b/include/xtc/xtc_mixer.h @@ -1,26 +1,33 @@ #ifndef XTC_MIXER_H #define XTC_MIXER_H + #include "xtc_project.h" + #ifdef __cplusplus extern "C" { #endif + #define XTC_MAX_AUTOMATION_POINTS 1024U #define XTC_HISTORY_EDITS 32U -typedef enum { XTC_AUTOMATION_GAIN = 0, XTC_AUTOMATION_PAN = 1 } xtc_automation_parameter; + +typedef enum { + XTC_AUTOMATION_GAIN = 0, + XTC_AUTOMATION_PAN = 1 +} xtc_automation_parameter; + typedef struct { uint64_t track_id, tick; xtc_automation_parameter parameter; float value; } xtc_automation_point; + /* Gain 0..4, pan -1..1, mute/solo 0 or 1. Track audio and scheduled MIDI only. * 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_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_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 *); /* Control-thread snapshot history; 32 edits plus baseline, no assets/DSP/GUI. @@ -29,12 +36,15 @@ xtc_result xtc_project_get_automation(const xtc_project *, size_t, xtc_automatio * Allocation ID high-water marks survive checkout/branching. * 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); + #ifdef __cplusplus } #endif + #endif diff --git a/include/xtc/xtc_native.h b/include/xtc/xtc_native.h index dff4956..d019e19 100644 --- a/include/xtc/xtc_native.h +++ b/include/xtc/xtc_native.h @@ -1,12 +1,21 @@ #ifndef XTC_NATIVE_H #define XTC_NATIVE_H + #include "xtc_midi.h" + #ifdef __cplusplus extern "C" { #endif + #define XTC_NATIVE_ABI_VERSION 1U #define XTC_NATIVE_ID_BYTES 64U -typedef enum { XTC_NATIVE_EFFECT = 1, XTC_NATIVE_SOURCE, XTC_NATIVE_INSTRUMENT } xtc_native_kind; + +typedef enum { + XTC_NATIVE_EFFECT = 1, + XTC_NATIVE_SOURCE, + XTC_NATIVE_INSTRUMENT +} xtc_native_kind; + typedef struct { const float *input; /* NULL is silence; may equal output. Borrowed for call. */ float *output; /* Write every frame, including silence. */ @@ -15,6 +24,7 @@ typedef struct { const xtc_midi_event *events; /* Instruments only, sorted block offsets. */ size_t event_count; } xtc_native_block; + typedef struct { unsigned int abi_version; size_t struct_size; /* Must equal sizeof(xtc_native_descriptor) for ABI 1. */ @@ -29,11 +39,12 @@ typedef struct { 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, xtc_native **out_processor); +xtc_result xtc_native_create(const xtc_native_descriptor *, double rate, size_t max_frames, float parameter, + xtc_native **out_processor); void xtc_native_destroy(xtc_native *); void xtc_native_reset(xtc_native *); /* Bounded, single-owner, realtime-safe host path (callbacks must obey contract). @@ -46,11 +57,11 @@ xtc_result xtc_native_process(xtc_native *, const xtc_native_block *); * 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); +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); + #ifdef __cplusplus } #endif + #endif diff --git a/include/xtc/xtc_project.h b/include/xtc/xtc_project.h index 4461c0b..cd9f865 100644 --- a/include/xtc/xtc_project.h +++ b/include/xtc/xtc_project.h @@ -1,9 +1,12 @@ #ifndef XTC_PROJECT_H #define XTC_PROJECT_H + #include "xtc_engine.h" + #ifdef __cplusplus extern "C" { #endif + #define XTC_PROJECT_MAX_TRACKS 64U #define XTC_PROJECT_MAX_CLIPS 1024U #define XTC_PROJECT_NAME_BYTES 64U @@ -11,9 +14,14 @@ extern "C" { #define XTC_TICKS_PER_QUARTER 960U #define XTC_PROJECT_MAX_TICK UINT64_C(38400000) typedef struct xtc_project xtc_project; -typedef struct { uint64_t id; char name[XTC_PROJECT_NAME_BYTES]; - float gain, pan; int mute, solo; + +typedef struct { + uint64_t id; + char name[XTC_PROJECT_NAME_BYTES]; + float gain, pan; + int mute, solo; } xtc_track_info; + /* Arrangement region: empty asset_path means silent metadata. * All musical positions are integer ticks, constant tempo and 4/4 meter. */ typedef struct { @@ -22,6 +30,7 @@ typedef struct { char asset_path[XTC_ASSET_PATH_BYTES]; int midi; /* 0 silent/audio region, 1 MIDI clip; no asset path in MIDI. */ } xtc_clip_info; + /* Single control-thread ownership. Names: nonempty UTF-8, <=63 bytes, no ASCII controls. * IDs are monotonic, never reused, UINT64_MAX reserved. Snapshots are copies. */ xtc_result xtc_project_create(xtc_project **out_project); @@ -34,12 +43,12 @@ xtc_result xtc_project_find_clip(const xtc_project *project, uint64_t id, xtc_cl xtc_result xtc_project_add_track(xtc_project *project, const char *name, uint64_t *out_id); /* Removing a track also removes its clips. */ xtc_result xtc_project_remove_track(xtc_project *project, uint64_t id); -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); +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); xtc_result xtc_project_remove_clip(xtc_project *project, uint64_t id); /* Atomic move/trim: source bounds remain unchanged, no destructive edits. */ -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); +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. */ const xtc_graph *xtc_project_graph(const xtc_project *project); @@ -49,16 +58,18 @@ unsigned int xtc_project_tempo(const xtc_project *project); 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. */ -xtc_result xtc_project_tick_to_sample(const xtc_project *project, uint64_t tick, - unsigned int sample_rate, uint64_t *out_sample); -xtc_result xtc_project_sample_to_tick(const xtc_project *project, uint64_t sample, - unsigned int sample_rate, uint64_t *out_tick); +xtc_result xtc_project_tick_to_sample(const xtc_project *project, uint64_t tick, unsigned int sample_rate, + uint64_t *out_sample); +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. */ xtc_result xtc_project_save(const xtc_project *project, const char *path); xtc_result xtc_project_load(xtc_project *project, const char *path); + #ifdef __cplusplus } #endif + #endif diff --git a/include/xtc/xtc_realtime.h b/include/xtc/xtc_realtime.h index 0d63105..0abb0fc 100644 --- a/include/xtc/xtc_realtime.h +++ b/include/xtc/xtc_realtime.h @@ -1,11 +1,14 @@ #ifndef XTC_REALTIME_H #define XTC_REALTIME_H + #include "xtc_engine.h" + #ifdef __cplusplus extern "C" { #endif typedef struct xtc_realtime xtc_realtime; + typedef struct { unsigned int processed_blocks; /* Wraps modulo UINT_MAX + 1. */ xtc_result last_result; @@ -15,8 +18,7 @@ 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. */ -xtc_result xtc_realtime_create(double sample_rate, size_t max_frames, - xtc_realtime **out_engine); +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. */ void xtc_realtime_destroy(xtc_realtime *engine); /* Compiles/copies a graph. One pending plan allowed; full mailbox returns @@ -24,23 +26,23 @@ void xtc_realtime_destroy(xtc_realtime *engine); xtc_result xtc_realtime_submit(xtc_realtime *engine, const xtc_graph *graph); /* Frees acknowledged retired state, never the audio thread's active plan. */ void xtc_realtime_collect(xtc_realtime *engine); -xtc_result xtc_realtime_get_status(const xtc_realtime *engine, - xtc_realtime_status *out_status); +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, const float *audio_right, const float *midi_left, const float *midi_right, - float *left, float *right, size_t frames); +xtc_result xtc_realtime_process_stereo(xtc_realtime *, 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); xtc_result xtc_realtime_process_sources(xtc_realtime *engine, const float *input, const float *timeline, - const float *instrument, float *output, size_t frames); -xtc_result xtc_realtime_process_audio(xtc_realtime *engine, const float *input, const float *timeline, - float *output, size_t frames); -xtc_result xtc_realtime_process(xtc_realtime *engine, const float *input, - float *output, size_t frame_count); + const float *instrument, float *output, size_t frames); +xtc_result xtc_realtime_process_audio(xtc_realtime *engine, const float *input, const float *timeline, float *output, + size_t frames); +xtc_result xtc_realtime_process(xtc_realtime *engine, const float *input, float *output, size_t frame_count); + #ifdef __cplusplus } #endif + #endif diff --git a/src/audio/arrangement.c b/src/audio/arrangement.c index dc56f11..fab7c13 100644 --- a/src/audio/arrangement.c +++ b/src/audio/arrangement.c @@ -5,7 +5,12 @@ #include #include #include -typedef struct { uint64_t start, end, offset; size_t asset, track; } region; + +typedef struct { + uint64_t start, end, offset; + size_t asset, track; +} region; + struct xtc_audio_plan { xtc_mix_snapshot mix; xtc_wave waves[64]; @@ -15,8 +20,9 @@ struct xtc_audio_plan { char first_issue[XTC_ASSET_PATH_BYTES]; unsigned int rate; }; -xtc_result xtc_project_import_audio(xtc_project *project, uint64_t track, - const char *path, uint64_t start, uint64_t *out_clip) + +xtc_result xtc_project_import_audio(xtc_project *project, uint64_t track, const char *path, uint64_t start, + uint64_t *out_clip) { xtc_wave wave = {0}; xtc_project *candidate = NULL; @@ -24,121 +30,183 @@ xtc_result xtc_project_import_audio(xtc_project *project, uint64_t track, uint64_t length, id = 0; size_t i, source = SIZE_MAX, output = SIZE_MAX; xtc_result result; - if (!project || !path || !out_clip) return XTC_ERR_INVALID_ARGUMENT; + if (!project || !path || !out_clip) + return XTC_ERR_INVALID_ARGUMENT; absolute = realpath(path, NULL); - if (!absolute) return XTC_ERR_IO; - if (strlen(absolute) >= XTC_ASSET_PATH_BYTES) { free(absolute); return XTC_ERR_CAPACITY; } + if (!absolute) + return XTC_ERR_IO; + if (strlen(absolute) >= XTC_ASSET_PATH_BYTES) { + free(absolute); + return XTC_ERR_CAPACITY; + } result = xtc_wave_load(absolute, &wave); - if (result != XTC_OK) { free(absolute); return result; } - length = ((uint64_t)wave.frames * project->tempo * XTC_TICKS_PER_QUARTER + - (uint64_t)wave.rate * 60000U - 1U) / ((uint64_t)wave.rate * 60000U); + if (result != XTC_OK) { + free(absolute); + return result; + } + length = ((uint64_t)wave.frames * project->tempo * XTC_TICKS_PER_QUARTER + (uint64_t)wave.rate * 60000U - 1U) / + ((uint64_t)wave.rate * 60000U); free(wave.samples); result = xtc_project_clone(project, &candidate); - if (result == XTC_OK) result = xtc_project_add_clip(candidate, track, "Audio", start, length, &id); - if (result == XTC_OK) result = xtc_project_set_clip_asset(candidate, id, absolute); + if (result == XTC_OK) + result = xtc_project_add_clip(candidate, track, "Audio", start, length, &id); + if (result == XTC_OK) + result = xtc_project_set_clip_asset(candidate, id, absolute); free(absolute); if (result == XTC_OK) { for (i = 0; i < candidate->graph->node_count; ++i) { xtc_node *n = candidate->graph->nodes + i; - if (n->type == XTC_NODE_AUDIO) source = n->id; - if (n->type == XTC_NODE_OUTPUT) output = n->id; + if (n->type == XTC_NODE_AUDIO) + source = n->id; + if (n->type == XTC_NODE_OUTPUT) + output = n->id; } if (source == SIZE_MAX) { result = xtc_graph_add_audio(candidate->graph, &source); - if (result == XTC_OK && output == SIZE_MAX) result = xtc_graph_add_output(candidate->graph, &output); - if (result == XTC_OK) result = xtc_graph_connect(candidate->graph, source, output); + if (result == XTC_OK && output == SIZE_MAX) + result = xtc_graph_add_output(candidate->graph, &output); + if (result == XTC_OK) + result = xtc_graph_connect(candidate->graph, source, output); } } if (result == XTC_OK) { - xtc_project old = *project; *project = *candidate; *candidate = old; + xtc_project old = *project; + *project = *candidate; + *candidate = old; *out_clip = id; } xtc_project_destroy(candidate); return result; } + void xtc_audio_plan_destroy(xtc_audio_plan *plan) { size_t i; - if (!plan) return; - for (i = 0; i < plan->assets; ++i) free(plan->waves[i].samples); + if (!plan) + return; + for (i = 0; i < plan->assets; ++i) + free(plan->waves[i].samples); free(plan); } + xtc_result xtc_audio_plan_create(const xtc_project *project, unsigned int rate, xtc_audio_plan **out) { xtc_audio_plan *plan; size_t i, total = 0; xtc_result result = XTC_OK; - if (!out) return XTC_ERR_INVALID_ARGUMENT; + if (!out) + return XTC_ERR_INVALID_ARGUMENT; *out = NULL; - if (!project || !rate || rate > 768000) return XTC_ERR_INVALID_ARGUMENT; + if (!project || !rate || rate > 768000) + return XTC_ERR_INVALID_ARGUMENT; plan = calloc(1, sizeof(*plan)); - if (!plan) return XTC_ERR_OUT_OF_MEMORY; + if (!plan) + return XTC_ERR_OUT_OF_MEMORY; plan->rate = rate; result = xtc_mix_prepare(project, rate, &plan->mix); - if (result != XTC_OK) { free(plan); return result; } + if (result != XTC_OK) { + free(plan); + return result; + } for (i = 0; i < project->clip_count; ++i) { const xtc_clip_info *clip = project->clips + i; - size_t a; region *r; - if (!clip->asset_path[0]) continue; - for (a = 0; a < plan->assets; ++a) if (!strcmp(plan->paths[a], clip->asset_path)) break; + size_t a; + region *r; + if (!clip->asset_path[0]) + continue; + for (a = 0; a < plan->assets; ++a) + if (!strcmp(plan->paths[a], clip->asset_path)) + break; if (a == plan->assets) { - if (a == 64) { result = XTC_ERR_CAPACITY; break; } - strcpy(plan->paths[a], clip->asset_path); ++plan->assets; + if (a == 64) { + result = XTC_ERR_CAPACITY; + break; + } + strcpy(plan->paths[a], clip->asset_path); + ++plan->assets; result = xtc_wave_load(clip->asset_path, plan->waves + a); - if (result == XTC_ERR_CAPACITY || result == XTC_ERR_OUT_OF_MEMORY) break; + if (result == XTC_ERR_CAPACITY || result == XTC_ERR_OUT_OF_MEMORY) + break; if (result != XTC_OK) { ++plan->issues; - if (!plan->first_issue[0]) strcpy(plan->first_issue, clip->asset_path); + if (!plan->first_issue[0]) + strcpy(plan->first_issue, clip->asset_path); } result = XTC_OK; total += plan->waves[a].frames; - if (total > 67108864U) { result = XTC_ERR_CAPACITY; break; } + if (total > 67108864U) { + result = XTC_ERR_CAPACITY; + break; + } } r = plan->regions + plan->count++; - r->asset = a; r->track = xtc_mix_track(&plan->mix, clip->track_id); + r->asset = a; + r->track = xtc_mix_track(&plan->mix, clip->track_id); result = xtc_project_tick_to_sample(project, clip->start_tick, rate, &r->start); - if (result == XTC_OK) result = xtc_project_tick_to_sample(project, clip->start_tick + clip->length_tick, rate, &r->end); - if (result == XTC_OK) result = xtc_project_tick_to_sample(project, clip->offset_tick, rate, &r->offset); - if (result != XTC_OK) break; + if (result == XTC_OK) + result = xtc_project_tick_to_sample(project, clip->start_tick + clip->length_tick, rate, &r->end); + if (result == XTC_OK) + result = xtc_project_tick_to_sample(project, clip->offset_tick, rate, &r->offset); + if (result != XTC_OK) + break; } - if (result != XTC_OK) { xtc_audio_plan_destroy(plan); return result; } - *out = plan; return XTC_OK; + if (result != XTC_OK) { + xtc_audio_plan_destroy(plan); + return result; + } + *out = plan; + return XTC_OK; } + size_t xtc_audio_plan_issues(const xtc_audio_plan *plan, const char **path) { - if (path) *path = plan ? plan->first_issue : ""; + if (path) + *path = plan ? plan->first_issue : ""; return plan ? plan->issues : 0; } -xtc_result xtc_audio_plan_render_stereo(const xtc_audio_plan *plan, uint64_t position, float *output, float *right, size_t frames) + +xtc_result xtc_audio_plan_render_stereo(const xtc_audio_plan *plan, uint64_t position, float *output, float *right, + size_t frames) { size_t i, j; - if (!plan || !output || (right && right == output) || frames > 65536U || position > UINT64_MAX - frames) return XTC_ERR_INVALID_ARGUMENT; + if (!plan || !output || (right && right == output) || frames > 65536U || position > UINT64_MAX - frames) + return XTC_ERR_INVALID_ARGUMENT; memset(output, 0, frames * sizeof(*output)); - if (right) memset(right, 0, frames * sizeof(*right)); + if (right) + memset(right, 0, frames * sizeof(*right)); for (i = 0; i < plan->count; ++i) { const region *r = plan->regions + i; const xtc_wave *wave = plan->waves + r->asset; uint64_t begin = position > r->start ? position : r->start; uint64_t end = position + frames < r->end ? position + frames : r->end; - if (!wave->samples || end <= begin) continue; + if (!wave->samples || end <= begin) + continue; for (j = (size_t)(begin - position); j < (size_t)(end - position); ++j) { double source = (double)(position + j - r->start + r->offset) * wave->rate / plan->rate; size_t index; double fraction; - if (source >= (double)wave->frames) continue; - index = (size_t)source; fraction = source - (double)index; - float sample = (float)(wave->samples[index] * (1.0 - fraction) + - (index + 1 < wave->frames ? wave->samples[index + 1] : wave->samples[index]) * fraction); + if (source >= (double)wave->frames) + continue; + index = (size_t)source; + fraction = source - (double)index; + float sample = + (float)(wave->samples[index] * (1.0 - fraction) + + (index + 1 < wave->frames ? wave->samples[index + 1] : wave->samples[index]) * fraction); output[j] += sample * xtc_mix_level(&plan->mix, r->track, position + j, right ? 0U : 2U); - if (right) right[j] += sample * xtc_mix_level(&plan->mix, r->track, position + j, 1); + if (right) + right[j] += sample * xtc_mix_level(&plan->mix, r->track, position + j, 1); } } for (j = 0; j < frames; ++j) { - if (!isfinite(output[j])) output[j] = 0; - if (right && !isfinite(right[j])) right[j] = 0; + if (!isfinite(output[j])) + output[j] = 0; + if (right && !isfinite(right[j])) + right[j] = 0; } return XTC_OK; } 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); } +{ + return xtc_audio_plan_render_stereo(p, position, out, NULL, frames); +} diff --git a/src/audio/recorder.c b/src/audio/recorder.c index 7a5d64b..a8b6798 100644 --- a/src/audio/recorder.c +++ b/src/audio/recorder.c @@ -4,6 +4,7 @@ #include #include #include + struct xtc_recorder { float *ring; size_t capacity; @@ -13,39 +14,65 @@ struct xtc_recorder { char *path, *temporary; int finished; }; + xtc_result xtc_recorder_create(const char *path, unsigned int rate, size_t capacity, xtc_recorder **out) { xtc_recorder *r; size_t length = 0; int fd; - if (!out) return XTC_ERR_INVALID_ARGUMENT; + if (!out) + return XTC_ERR_INVALID_ARGUMENT; *out = NULL; - if (!path || !rate || rate > 768000 || !capacity || capacity > 2097152U) return XTC_ERR_INVALID_ARGUMENT; - while (length < 4096 && path[length]) ++length; - if (!length || length == 4096) return XTC_ERR_INVALID_ARGUMENT; - if (sizeof(float) != 4 || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128) return XTC_ERR_UNSUPPORTED; - if (!__atomic_always_lock_free(sizeof(uint64_t), 0) || - !__atomic_always_lock_free(sizeof(unsigned int), 0)) return XTC_ERR_UNSUPPORTED; + if (!path || !rate || rate > 768000 || !capacity || capacity > 2097152U) + return XTC_ERR_INVALID_ARGUMENT; + while (length < 4096 && path[length]) + ++length; + if (!length || length == 4096) + return XTC_ERR_INVALID_ARGUMENT; + if (sizeof(float) != 4 || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128) + return XTC_ERR_UNSUPPORTED; + if (!__atomic_always_lock_free(sizeof(uint64_t), 0) || !__atomic_always_lock_free(sizeof(unsigned int), 0)) + return XTC_ERR_UNSUPPORTED; r = calloc(1, sizeof(*r)); - if (!r) return XTC_ERR_OUT_OF_MEMORY; - r->capacity = capacity; r->rate = rate; + if (!r) + return XTC_ERR_OUT_OF_MEMORY; + r->capacity = capacity; + r->rate = rate; r->ring = malloc(capacity * sizeof(float)); - r->path = malloc(length + 1); r->temporary = calloc(length + 12, 1); - if (!r->ring || !r->path || !r->temporary) { xtc_recorder_destroy(r); return XTC_ERR_OUT_OF_MEMORY; } + r->path = malloc(length + 1); + r->temporary = calloc(length + 12, 1); + if (!r->ring || !r->path || !r->temporary) { + xtc_recorder_destroy(r); + return XTC_ERR_OUT_OF_MEMORY; + } memcpy(r->path, path, length + 1); - memcpy(r->temporary, path, length); memcpy(r->temporary + length, ".tmp.XXXXXX", 12); + memcpy(r->temporary, path, length); + memcpy(r->temporary + length, ".tmp.XXXXXX", 12); fd = mkstemp(r->temporary); - if (fd < 0) { xtc_recorder_destroy(r); return XTC_ERR_IO; } + if (fd < 0) { + xtc_recorder_destroy(r); + return XTC_ERR_IO; + } r->file = fdopen(fd, "w+b"); - if (!r->file) { close(fd); xtc_recorder_destroy(r); return XTC_ERR_IO; } - if (!xtc_wave_header(r->file, rate, 0)) { xtc_recorder_destroy(r); return XTC_ERR_IO; } - *out = r; return XTC_OK; + if (!r->file) { + close(fd); + xtc_recorder_destroy(r); + return XTC_ERR_IO; + } + if (!xtc_wave_header(r->file, rate, 0)) { + xtc_recorder_destroy(r); + return XTC_ERR_IO; + } + *out = r; + return XTC_OK; } + void xtc_recorder_push(xtc_recorder *r, const float *input, size_t frames) { uint64_t write, read; size_t i; - if (!r || !frames || __atomic_load_n(&r->error, __ATOMIC_ACQUIRE)) return; + if (!r || !frames || __atomic_load_n(&r->error, __ATOMIC_ACQUIRE)) + return; write = __atomic_load_n(&r->write_position, __ATOMIC_RELAXED); read = __atomic_load_n(&r->read_position, __ATOMIC_ACQUIRE); if (!input || frames > r->capacity || frames > r->capacity - (size_t)(write - read) || @@ -53,53 +80,73 @@ void xtc_recorder_push(xtc_recorder *r, const float *input, size_t frames) __atomic_store_n(&r->error, (unsigned int)(input ? XTC_ERR_CAPACITY : XTC_ERR_IO), __ATOMIC_RELEASE); return; } - for (i = 0; i < frames; ++i) r->ring[(size_t)((write + i) % r->capacity)] = input[i]; + for (i = 0; i < frames; ++i) + r->ring[(size_t)((write + i) % r->capacity)] = input[i]; __atomic_store_n(&r->write_position, write + frames, __ATOMIC_RELEASE); } + xtc_result xtc_recorder_drain(xtc_recorder *r) { uint64_t read, write; - if (!r || !r->file || r->finished) return XTC_ERR_INVALID_STATE; + if (!r || !r->file || r->finished) + return XTC_ERR_INVALID_STATE; read = __atomic_load_n(&r->read_position, __ATOMIC_RELAXED); write = __atomic_load_n(&r->write_position, __ATOMIC_ACQUIRE); while (read < write) { size_t offset = (size_t)(read % r->capacity); size_t count = r->capacity - offset; - if (count > write - read) count = (size_t)(write - read); + if (count > write - read) + count = (size_t)(write - read); if (!xtc_wave_write(r->file, r->ring + offset, count)) { __atomic_store_n(&r->error, (unsigned int)XTC_ERR_IO, __ATOMIC_RELEASE); return XTC_ERR_IO; } - read += count; r->written += count; + read += count; + r->written += count; __atomic_store_n(&r->read_position, read, __ATOMIC_RELEASE); } return (xtc_result)__atomic_load_n(&r->error, __ATOMIC_ACQUIRE); } + xtc_result xtc_recorder_status(const xtc_recorder *r, uint64_t *frames) { - if (!r || !frames) return XTC_ERR_INVALID_ARGUMENT; + if (!r || !frames) + return XTC_ERR_INVALID_ARGUMENT; *frames = __atomic_load_n(&r->write_position, __ATOMIC_ACQUIRE); return (xtc_result)__atomic_load_n(&r->error, __ATOMIC_ACQUIRE); } + xtc_result xtc_recorder_finish(xtc_recorder *r) { xtc_result result; int failed = 0; - if (!r || !r->file || r->finished) return XTC_ERR_INVALID_STATE; + if (!r || !r->file || r->finished) + return XTC_ERR_INVALID_STATE; result = xtc_recorder_drain(r); - if (result == XTC_ERR_IO) failed = 1; - if (fseek(r->file, 0, SEEK_SET) || !xtc_wave_header(r->file, r->rate, (uint32_t)r->written) || - fflush(r->file) || fsync(fileno(r->file))) failed = 1; - if (fclose(r->file)) failed = 1; + if (result == XTC_ERR_IO) + failed = 1; + if (fseek(r->file, 0, SEEK_SET) || !xtc_wave_header(r->file, r->rate, (uint32_t)r->written) || fflush(r->file) || + fsync(fileno(r->file))) + failed = 1; + if (fclose(r->file)) + failed = 1; r->file = NULL; - if (!failed && rename(r->temporary, r->path)) failed = 1; + if (!failed && rename(r->temporary, r->path)) + failed = 1; r->finished = 1; return failed ? XTC_ERR_IO : result; } + void xtc_recorder_destroy(xtc_recorder *r) { - if (!r) return; - if (r->file) fclose(r->file); - if (r->temporary) unlink(r->temporary); - free(r->temporary); free(r->path); free(r->ring); free(r); + if (!r) + return; + if (r->file) + fclose(r->file); + if (r->temporary) + unlink(r->temporary); + free(r->temporary); + free(r->path); + free(r->ring); + free(r); } diff --git a/src/audio/wave.c b/src/audio/wave.c index d8a4a97..0fe88b2 100644 --- a/src/audio/wave.c +++ b/src/audio/wave.c @@ -3,35 +3,63 @@ #include #include #include + 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; } +{ + return (uint32_t)p[0] | (uint32_t)p[1] << 8U | (uint32_t)p[2] << 16U | (uint32_t)p[3] << 24U; +} + static unsigned int u16(const unsigned char *p) -{ return (unsigned int)p[0] | (unsigned int)p[1] << 8U; } +{ + return (unsigned int)p[0] | (unsigned int)p[1] << 8U; +} + static void put32(unsigned char *p, uint32_t v) -{ size_t i; for (i = 0; i < 4; ++i) { p[i] = (unsigned char)(v & 255U); v >>= 8U; } } +{ + size_t i; + for (i = 0; i < 4; ++i) { + p[i] = (unsigned char)(v & 255U); + v >>= 8U; + } +} + int xtc_wave_header(FILE *file, unsigned int rate, uint32_t frames) { unsigned char h[44] = {0}; - memcpy(h, "RIFF", 4); put32(h + 4, 36U + frames * 4U); - memcpy(h + 8, "WAVEfmt ", 8); put32(h + 16, 16); - h[20] = 3; h[22] = 1; put32(h + 24, rate); put32(h + 28, rate * 4U); - h[32] = 4; h[34] = 32; memcpy(h + 36, "data", 4); put32(h + 40, frames * 4U); + memcpy(h, "RIFF", 4); + put32(h + 4, 36U + frames * 4U); + memcpy(h + 8, "WAVEfmt ", 8); + put32(h + 16, 16); + h[20] = 3; + h[22] = 1; + put32(h + 24, rate); + put32(h + 28, rate * 4U); + h[32] = 4; + h[34] = 32; + memcpy(h + 36, "data", 4); + put32(h + 40, frames * 4U); return fwrite(h, 1, sizeof(h), file) == sizeof(h); } + int xtc_wave_write(FILE *file, const float *input, size_t frames) { unsigned char bytes[4096]; while (frames) { size_t i, count = frames > 1024 ? 1024 : frames; for (i = 0; i < count; ++i) { - uint32_t bits; float value = isfinite(input[i]) ? input[i] : 0.0f; - memcpy(&bits, &value, 4); put32(bytes + i * 4, bits); + uint32_t bits; + float value = isfinite(input[i]) ? input[i] : 0.0f; + memcpy(&bits, &value, 4); + put32(bytes + i * 4, bits); } - if (fwrite(bytes, 4, count, file) != count) return 0; - input += count; frames -= count; + if (fwrite(bytes, 4, count, file) != count) + return 0; + input += count; + frames -= count; } return 1; } + xtc_result xtc_wave_load(const char *path, xtc_wave *out) { FILE *f; @@ -43,73 +71,114 @@ xtc_result xtc_wave_load(const char *path, xtc_wave *out) size_t i, frames, chunks = 0; float *samples = NULL; xtc_result result = XTC_ERR_FORMAT; - if (!path || !out) return XTC_ERR_INVALID_ARGUMENT; + if (!path || !out) + return XTC_ERR_INVALID_ARGUMENT; memset(out, 0, sizeof(*out)); if (sizeof(float) != 4 || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128) return XTC_ERR_UNSUPPORTED; f = fopen(path, "rb"); - if (!f) return XTC_ERR_IO; - if (fseek(f, 0, SEEK_END) || (file_size = ftell(f)) < 12 || fseek(f, 0, SEEK_SET)) goto done; - if (file_size > 268435456L) { result = XTC_ERR_CAPACITY; goto done; } - if (fread(h, 1, 12, f) != 12 || memcmp(h, "RIFF", 4) || memcmp(h + 8, "WAVE", 4)) goto done; + if (!f) + return XTC_ERR_IO; + if (fseek(f, 0, SEEK_END) || (file_size = ftell(f)) < 12 || fseek(f, 0, SEEK_SET)) + goto done; + if (file_size > 268435456L) { + result = XTC_ERR_CAPACITY; + goto done; + } + if (fread(h, 1, 12, f) != 12 || memcmp(h, "RIFF", 4) || memcmp(h + 8, "WAVE", 4)) + goto done; remaining = u32(h + 4); - if (remaining < 4 || remaining + 8U > (uint64_t)file_size) goto done; + if (remaining < 4 || remaining + 8U > (uint64_t)file_size) + goto done; remaining -= 4; while (remaining) { - uint32_t size; uint64_t padded; - if (++chunks > 4096 || remaining < 8 || fread(h, 1, 8, f) != 8) goto done; - size = u32(h + 4); padded = (uint64_t)size + (size & 1U); + uint32_t size; + uint64_t padded; + if (++chunks > 4096 || remaining < 8 || fread(h, 1, 8, f) != 8) + goto done; + size = u32(h + 4); + padded = (uint64_t)size + (size & 1U); remaining -= 8; - if (padded > 0x7fffffffU || padded > remaining) goto done; + if (padded > 0x7fffffffU || padded > remaining) + goto done; if (!memcmp(h, "fmt ", 4)) { - if (format || size < 16 || fread(h + 8, 1, 16, f) != 16) goto done; - format = u16(h + 8); channels = u16(h + 10); rate = u32(h + 12); - align = u16(h + 20); bits = u16(h + 22); - if (fseek(f, (long)(padded - 16), SEEK_CUR)) goto done; + if (format || size < 16 || fread(h + 8, 1, 16, f) != 16) + goto done; + format = u16(h + 8); + channels = u16(h + 10); + rate = u32(h + 12); + align = u16(h + 20); + bits = u16(h + 22); + if (fseek(f, (long)(padded - 16), SEEK_CUR)) + goto done; } else { if (!memcmp(h, "data", 4)) { - if (data_offset) goto done; - data_offset = ftell(f); data_size = size; + if (data_offset) + goto done; + data_offset = ftell(f); + data_size = size; } - if (padded > 0x7fffffffU || fseek(f, (long)padded, SEEK_CUR)) goto done; + if (padded > 0x7fffffffU || fseek(f, (long)padded, SEEK_CUR)) + goto done; } remaining -= padded; } - if ((format != 1 && format != 3) || (channels != 1 && channels != 2) || - (bits != 16 && bits != 24 && bits != 32) || (format == 3 && bits != 32)) { - result = XTC_ERR_UNSUPPORTED; goto done; + if ((format != 1 && format != 3) || (channels != 1 && channels != 2) || (bits != 16 && bits != 24 && bits != 32) || + (format == 3 && bits != 32)) { + result = XTC_ERR_UNSUPPORTED; + goto done; } - if (!rate || rate > 768000 || !data_offset || !data_size || - align != channels * (bits / 8U) || data_size % align) goto done; + if (!rate || rate > 768000 || !data_offset || !data_size || align != channels * (bits / 8U) || data_size % align) + goto done; frames = data_size / align; - if (frames > 16777216U) { result = XTC_ERR_CAPACITY; goto done; } + if (frames > 16777216U) { + result = XTC_ERR_CAPACITY; + goto done; + } samples = malloc(frames * sizeof(*samples)); - if (!samples) { result = XTC_ERR_OUT_OF_MEMORY; goto done; } - if (fseek(f, data_offset, SEEK_SET)) goto done; + if (!samples) { + result = XTC_ERR_OUT_OF_MEMORY; + goto done; + } + if (fseek(f, data_offset, SEEK_SET)) + goto done; for (i = 0; i < frames; ++i) { - unsigned int c; double sum = 0; - if (fread(bytes, 1, align, f) != align) goto done; + unsigned int c; + double sum = 0; + if (fread(bytes, 1, align, f) != align) + goto done; for (c = 0; c < channels; ++c) { const unsigned char *p = bytes + c * (bits / 8U); if (format == 3) { - uint32_t value = u32(p); float sample; memcpy(&sample, &value, 4); - if (!isfinite(sample)) goto done; + uint32_t value = u32(p); + float sample; + memcpy(&sample, &value, 4); + if (!isfinite(sample)) + goto done; sum += sample; } else { - uint32_t value = bits == 16 ? u16(p) : bits == 24 ? - (uint32_t)p[0] | (uint32_t)p[1] << 8U | (uint32_t)p[2] << 16U : u32(p); + uint32_t value = bits == 16 ? u16(p) + : bits == 24 ? (uint32_t)p[0] | (uint32_t)p[1] << 8U | (uint32_t)p[2] << 16U + : u32(p); int64_t signed_value = value; - if (value & (UINT32_C(1) << (bits - 1U))) signed_value -= INT64_C(1) << bits; + if (value & (UINT32_C(1) << (bits - 1U))) + signed_value -= INT64_C(1) << bits; sum += (double)signed_value / (double)(INT64_C(1) << (bits - 1U)); } } samples[i] = (float)(sum / channels); } - if (ferror(f)) goto done; + if (ferror(f)) + goto done; result = XTC_OK; done: - if (fclose(f) && result == XTC_OK) result = XTC_ERR_IO; - if (result == XTC_OK) { out->samples = samples; out->frames = frames; out->rate = rate; } - else free(samples); + if (fclose(f) && result == XTC_OK) + result = XTC_ERR_IO; + if (result == XTC_OK) { + out->samples = samples; + out->frames = frames; + out->rate = rate; + } else + free(samples); return result; } diff --git a/src/audio/wave_internal.h b/src/audio/wave_internal.h index bcccfc8..366b82d 100644 --- a/src/audio/wave_internal.h +++ b/src/audio/wave_internal.h @@ -1,8 +1,15 @@ #ifndef XTC_WAVE_INTERNAL_H #define XTC_WAVE_INTERNAL_H + #include "xtc/xtc_audio.h" #include -typedef struct { float *samples; size_t frames; unsigned int rate; } xtc_wave; + +typedef struct { + float *samples; + size_t frames; + unsigned int rate; +} xtc_wave; + xtc_result xtc_wave_load(const char *path, xtc_wave *out_wave); int xtc_wave_header(FILE *file, unsigned int rate, uint32_t frames); int xtc_wave_write(FILE *file, const float *input, size_t frames); diff --git a/src/backend/jack_backend.c b/src/backend/jack_backend.c index 8658816..d1f1c6f 100644 --- a/src/backend/jack_backend.c +++ b/src/backend/jack_backend.c @@ -33,33 +33,43 @@ static xtc_result render_midi(xtc_backend *backend, uint64_t position, jack_nfra uint32_t i, total; unsigned int requested = __atomic_load_n(&backend->panic_requested, __ATOMIC_ACQUIRE); void *buffer; - if (!backend->midi) return XTC_OK; + if (!backend->midi) + return XTC_OK; if (requested != backend->panic_seen) { - xtc_midi_plan_panic(backend->midi, position); backend->panic_seen = requested; + xtc_midi_plan_panic(backend->midi, position); + backend->panic_seen = requested; } buffer = jack_port_get_buffer(backend->midi_input, frames); total = buffer ? jack_midi_get_event_count(buffer) : 0; if (!buffer || total > XTC_MIDI_MAX_EVENTS) { (void)__atomic_fetch_add(&backend->midi_dropped, total ? total : 1U, __ATOMIC_RELAXED); xtc_midi_plan_panic(backend->midi, position); - } else for (i = 0; i < total; ++i) { - jack_midi_event_t raw; - xtc_result result; - if (jack_midi_event_get(&raw, buffer, i) != 0 || raw.time >= frames) { - (void)__atomic_fetch_add(&backend->midi_dropped, 1U, __ATOMIC_RELAXED); - xtc_midi_plan_panic(backend->midi, position); count = 0; break; + } else + for (i = 0; i < total; ++i) { + jack_midi_event_t raw; + xtc_result result; + if (jack_midi_event_get(&raw, buffer, i) != 0 || raw.time >= frames) { + (void)__atomic_fetch_add(&backend->midi_dropped, 1U, __ATOMIC_RELAXED); + xtc_midi_plan_panic(backend->midi, position); + count = 0; + break; + } + result = xtc_midi_decode(raw.buffer, raw.size, (unsigned int)raw.time, events + count); + if (result == XTC_ERR_UNSUPPORTED) + continue; + if (result != XTC_OK || (count && events[count].offset < events[count - 1].offset)) { + (void)__atomic_fetch_add(&backend->midi_dropped, 1U, __ATOMIC_RELAXED); + xtc_midi_plan_panic(backend->midi, position); + count = 0; + break; + } + ++count; } - result = xtc_midi_decode(raw.buffer, raw.size, (unsigned int)raw.time, events + count); - if (result == XTC_ERR_UNSUPPORTED) continue; - if (result != XTC_OK || (count && events[count].offset < events[count - 1].offset)) { - (void)__atomic_fetch_add(&backend->midi_dropped, 1U, __ATOMIC_RELAXED); - xtc_midi_plan_panic(backend->midi, position); count = 0; break; - } - ++count; - } return xtc_midi_plan_render_stereo(backend->midi, position, events, count, backend->instrument, - backend->output_right ? backend->instrument + backend->max_frames : NULL, (size_t)frames); + backend->output_right ? backend->instrument + backend->max_frames : NULL, + (size_t)frames); } + static int process(jack_nframes_t frames, void *context) { xtc_backend *backend = context; @@ -67,12 +77,15 @@ static int process(jack_nframes_t frames, void *context) float *right = backend->output_right ? jack_port_get_buffer(backend->output_right, frames) : NULL; xtc_result result = XTC_OK; if (output == NULL || (backend->output_right && !right)) { - if (output) memset(output, 0, (size_t)frames * sizeof(*output)); - if (right) memset(right, 0, (size_t)frames * sizeof(*right)); + if (output) + memset(output, 0, (size_t)frames * sizeof(*output)); + if (right) + memset(right, 0, (size_t)frames * sizeof(*right)); __atomic_store_n(&backend->peak_left, 0U, __ATOMIC_RELAXED); __atomic_store_n(&backend->peak_right, 0U, __ATOMIC_RELAXED); (void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE); - if (backend->recorder) xtc_recorder_push(backend->recorder, NULL, 1U); + if (backend->recorder) + xtc_recorder_push(backend->recorder, NULL, 1U); __atomic_store_n(&backend->process_result, (unsigned int)XTC_ERR_IO, __ATOMIC_RELAXED); return 0; } @@ -84,36 +97,46 @@ static int process(jack_nframes_t frames, void *context) } else { const float *input = jack_port_get_buffer(backend->input, frames); uint64_t position = __atomic_load_n(&backend->sample_position, __ATOMIC_RELAXED); - if (backend->recorder) xtc_recorder_push(backend->recorder, input, (size_t)frames); + if (backend->recorder) + xtc_recorder_push(backend->recorder, input, (size_t)frames); if (backend->audio) - result = xtc_audio_plan_render_stereo(backend->audio, position, backend->timeline, - right ? backend->timeline + backend->max_frames : NULL, (size_t)frames); - if (result == XTC_OK) result = render_midi(backend, position, frames); + result = + xtc_audio_plan_render_stereo(backend->audio, position, backend->timeline, + right ? backend->timeline + backend->max_frames : NULL, (size_t)frames); if (result == XTC_OK) - result = xtc_realtime_process_stereo(backend->engine, input, - backend->audio ? backend->timeline : NULL, - backend->audio && right ? backend->timeline + backend->max_frames : NULL, - backend->midi ? backend->instrument : NULL, - backend->midi && right ? backend->instrument + backend->max_frames : NULL, - output, right, (size_t)frames); + result = render_midi(backend, position, frames); + if (result == XTC_OK) + result = + xtc_realtime_process_stereo(backend->engine, input, backend->audio ? backend->timeline : NULL, + backend->audio && right ? backend->timeline + backend->max_frames : NULL, + backend->midi ? backend->instrument : NULL, + backend->midi && right ? backend->instrument + backend->max_frames : NULL, + output, right, (size_t)frames); } if (result == XTC_OK) { uint64_t position = __atomic_load_n(&backend->sample_position, __ATOMIC_RELAXED); - if (position > UINT64_MAX - frames) result = XTC_ERR_CAPACITY; - else __atomic_store_n(&backend->sample_position, position + frames, __ATOMIC_RELAXED); + if (position > UINT64_MAX - frames) + result = XTC_ERR_CAPACITY; + else + __atomic_store_n(&backend->sample_position, position + frames, __ATOMIC_RELAXED); } if (result != XTC_OK) { (void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE); - if (backend->recorder) xtc_recorder_push(backend->recorder, NULL, 1U); + if (backend->recorder) + xtc_recorder_push(backend->recorder, NULL, 1U); memset(output, 0, (size_t)frames * sizeof(*output)); - if (right) memset(right, 0, (size_t)frames * sizeof(*right)); + if (right) + memset(right, 0, (size_t)frames * sizeof(*right)); } { - size_t i; float left_peak = 0, right_peak = 0; + size_t i; + float left_peak = 0, right_peak = 0; for (i = 0; i < frames; ++i) { float l = fabsf(output[i]), r = right ? fabsf(right[i]) : l; - if (l > left_peak) left_peak = l; - if (r > right_peak) right_peak = r; + if (l > left_peak) + left_peak = l; + if (r > right_peak) + right_peak = r; } __atomic_store_n(&backend->peak_left, (unsigned int)(fminf(left_peak, 1000) * 1000000), __ATOMIC_RELAXED); __atomic_store_n(&backend->peak_right, (unsigned int)(fminf(right_peak, 1000) * 1000000), __ATOMIC_RELAXED); @@ -121,18 +144,21 @@ static int process(jack_nframes_t frames, void *context) __atomic_store_n(&backend->process_result, (unsigned int)result, __ATOMIC_RELAXED); return 0; /* Keep connected and silent on recoverable processing errors. */ } + static void shutdown_callback(void *context) { xtc_backend *backend = context; __atomic_store_n(&backend->lost, 1U, __ATOMIC_RELEASE); } + static void port_connection(jack_port_id_t source, jack_port_id_t destination, int connected, void *context) { xtc_backend *backend = context; if (!connected && (jack_port_by_id(backend->client, source) == backend->midi_input || - jack_port_by_id(backend->client, destination) == backend->midi_input)) + jack_port_by_id(backend->client, destination) == backend->midi_input)) (void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE); } + static int rate_callback(jack_nframes_t rate, void *context) { xtc_backend *backend = context; @@ -141,12 +167,14 @@ static int rate_callback(jack_nframes_t rate, void *context) __atomic_store_n(&backend->rate_changed, 1U, __ATOMIC_RELEASE); return 0; } + static int buffer_callback(jack_nframes_t frames, void *context) { xtc_backend *backend = context; __atomic_store_n(&backend->frames, (unsigned int)frames, __ATOMIC_RELAXED); return 0; } + static int xrun_callback(void *context) { xtc_backend *backend = context; @@ -160,36 +188,56 @@ xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_ xtc_backend *backend; xtc_result result; jack_status_t status = 0; - if (out_backend == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (out_backend == NULL) + return XTC_ERR_INVALID_ARGUMENT; *out_backend = NULL; - if (config == NULL || config->client_name == NULL || config->client_name[0] == '\0' || - config->max_frames == 0U || config->max_frames > 65536U || (config->channel_count != 1U && config->channel_count != 2U)) + if (config == NULL || config->client_name == NULL || config->client_name[0] == '\0' || config->max_frames == 0U || + config->max_frames > 65536U || (config->channel_count != 1U && config->channel_count != 2U)) + return XTC_ERR_INVALID_ARGUMENT; + if (!__atomic_always_lock_free(sizeof(uint64_t), 0)) + return XTC_ERR_UNSUPPORTED; + if (config->kind != XTC_BACKEND_JACK) + return XTC_ERR_UNSUPPORTED; + if (strlen(config->client_name) >= (size_t)jack_client_name_size()) return XTC_ERR_INVALID_ARGUMENT; - if (!__atomic_always_lock_free(sizeof(uint64_t), 0)) return XTC_ERR_UNSUPPORTED; - if (config->kind != XTC_BACKEND_JACK) return XTC_ERR_UNSUPPORTED; - if (strlen(config->client_name) >= (size_t)jack_client_name_size()) return XTC_ERR_INVALID_ARGUMENT; backend = calloc(1U, sizeof(*backend)); - if (backend == NULL) return XTC_ERR_OUT_OF_MEMORY; + if (backend == NULL) + return XTC_ERR_OUT_OF_MEMORY; backend->timeline = calloc(config->max_frames * config->channel_count, sizeof(float)); backend->instrument = calloc(config->max_frames * config->channel_count, sizeof(float)); if (!backend->timeline || !backend->instrument) { - free(backend->timeline); free(backend->instrument); free(backend); return XTC_ERR_OUT_OF_MEMORY; + free(backend->timeline); + free(backend->instrument); + free(backend); + return XTC_ERR_OUT_OF_MEMORY; } backend->max_frames = config->max_frames; backend->client = jack_client_open(config->client_name, JackNoStartServer, &status); - if (backend->client == NULL) { free(backend->instrument); free(backend->timeline); free(backend); return XTC_ERR_IO; } + if (backend->client == NULL) { + free(backend->instrument); + free(backend->timeline); + free(backend); + return XTC_ERR_IO; + } backend->expected_rate = (unsigned int)jack_get_sample_rate(backend->client); backend->rate = backend->expected_rate; backend->frames = (unsigned int)jack_get_buffer_size(backend->client); - result = backend->frames == 0U || backend->frames > config->max_frames ? XTC_ERR_CAPACITY : - xtc_realtime_create((double)backend->rate, config->max_frames, &backend->engine); - if (result != XTC_OK) goto fail; + result = backend->frames == 0U || backend->frames > config->max_frames + ? XTC_ERR_CAPACITY + : xtc_realtime_create((double)backend->rate, config->max_frames, &backend->engine); + if (result != XTC_OK) + goto fail; backend->input = jack_port_register(backend->client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0U); backend->output = jack_port_register(backend->client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0U); backend->midi_input = jack_port_register(backend->client, "midi_in", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0U); if (config->channel_count == 2U) - backend->output_right = jack_port_register(backend->client, "output_right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0U); - if ((config->channel_count == 2U && !backend->output_right) || backend->input == NULL || backend->output == NULL || backend->midi_input == NULL) { result = XTC_ERR_IO; goto fail; } + backend->output_right = + jack_port_register(backend->client, "output_right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0U); + if ((config->channel_count == 2U && !backend->output_right) || backend->input == NULL || backend->output == NULL || + backend->midi_input == NULL) { + result = XTC_ERR_IO; + goto fail; + } if (jack_set_port_connect_callback(backend->client, port_connection, backend) != 0 || jack_set_process_callback(backend->client, process, backend) != 0 || jack_set_sample_rate_callback(backend->client, rate_callback, backend) != 0 || @@ -220,9 +268,11 @@ fail: xtc_result xtc_backend_close(xtc_backend *backend) { - if (backend == NULL) return XTC_OK; + if (backend == NULL) + return XTC_OK; /* JACK removes this client and joins callbacks before returning success. */ - if (jack_client_close(backend->client) != 0) return XTC_ERR_IO; + if (jack_client_close(backend->client) != 0) + return XTC_ERR_IO; xtc_realtime_destroy(backend->engine); xtc_audio_plan_destroy(backend->audio); xtc_midi_plan_destroy(backend->midi); @@ -231,52 +281,73 @@ xtc_result xtc_backend_close(xtc_backend *backend) free(backend); return XTC_OK; } + xtc_result xtc_backend_start(xtc_backend *backend) { - if (backend == NULL) return XTC_ERR_INVALID_ARGUMENT; - if (!backend->ready) return XTC_ERR_INVALID_STATE; - if (__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) || - __atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE; - if (backend->running) return XTC_ERR_INVALID_STATE; + if (backend == NULL) + return XTC_ERR_INVALID_ARGUMENT; + if (!backend->ready) + return XTC_ERR_INVALID_STATE; + if (__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) || __atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE)) + return XTC_ERR_INVALID_STATE; + if (backend->running) + return XTC_ERR_INVALID_STATE; xtc_midi_plan_reset(backend->midi); backend->panic_seen = __atomic_load_n(&backend->panic_requested, __ATOMIC_ACQUIRE); - if (jack_activate(backend->client) != 0) return XTC_ERR_IO; + if (jack_activate(backend->client) != 0) + return XTC_ERR_IO; backend->running = 1; return XTC_OK; } + xtc_result xtc_backend_stop(xtc_backend *backend) { - if (backend == NULL) return XTC_ERR_INVALID_ARGUMENT; - if (!backend->ready) return XTC_ERR_INVALID_STATE; - if (!backend->running) { xtc_midi_plan_reset(backend->midi); return XTC_OK; } - if (jack_deactivate(backend->client) != 0) return XTC_ERR_IO; + if (backend == NULL) + return XTC_ERR_INVALID_ARGUMENT; + if (!backend->ready) + return XTC_ERR_INVALID_STATE; + if (!backend->running) { + xtc_midi_plan_reset(backend->midi); + return XTC_OK; + } + if (jack_deactivate(backend->client) != 0) + return XTC_ERR_IO; backend->running = 0; xtc_midi_plan_reset(backend->midi); xtc_realtime_collect(backend->engine); return XTC_OK; } + xtc_result xtc_backend_seek(xtc_backend *backend, uint64_t sample) { - if (backend == NULL || sample > UINT64_MAX - 65536U) return XTC_ERR_INVALID_ARGUMENT; - if (!backend->ready || backend->running || - __atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) || - __atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE; + if (backend == NULL || sample > UINT64_MAX - 65536U) + return XTC_ERR_INVALID_ARGUMENT; + if (!backend->ready || backend->running || __atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) || + __atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE)) + return XTC_ERR_INVALID_STATE; xtc_midi_plan_reset(backend->midi); __atomic_store_n(&backend->sample_position, sample, __ATOMIC_RELAXED); return XTC_OK; } + xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph) { - if (backend == NULL) return XTC_ERR_INVALID_ARGUMENT; - if (!backend->ready) return XTC_ERR_INVALID_STATE; - if (__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) || - __atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE; + if (backend == NULL) + return XTC_ERR_INVALID_ARGUMENT; + if (!backend->ready) + return XTC_ERR_INVALID_STATE; + if (__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) || __atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE)) + return XTC_ERR_INVALID_STATE; if (!backend->running) { xtc_realtime *replacement = NULL; xtc_result result = xtc_realtime_create((double)backend->expected_rate, backend->max_frames, &replacement); - if (result != XTC_OK) return result; + if (result != XTC_OK) + return result; result = xtc_realtime_submit(replacement, graph); - if (result != XTC_OK) { xtc_realtime_destroy(replacement); return result; } + if (result != XTC_OK) { + xtc_realtime_destroy(replacement); + return result; + } /* Inactive client, or successful deactivate: process callback quiescent. * Notifications never access engine. Preserve old engine until ready. */ xtc_realtime_destroy(backend->engine); @@ -286,60 +357,94 @@ xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph) } return xtc_realtime_submit(backend->engine, graph); } + xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *project) { xtc_audio_plan *candidate = NULL; xtc_midi_plan *midi = NULL; xtc_result result; - if (!backend || !project) return XTC_ERR_INVALID_ARGUMENT; - if (!backend->ready || backend->running) return XTC_ERR_INVALID_STATE; + if (!backend || !project) + return XTC_ERR_INVALID_ARGUMENT; + if (!backend->ready || backend->running) + return XTC_ERR_INVALID_STATE; result = xtc_audio_plan_create(project, backend->expected_rate, &candidate); - if (result == XTC_OK) result = xtc_midi_plan_create(project, backend->expected_rate, &midi); - if (result == XTC_OK) result = xtc_backend_submit(backend, xtc_project_graph(project)); - if (result != XTC_OK) { xtc_audio_plan_destroy(candidate); xtc_midi_plan_destroy(midi); return result; } - xtc_audio_plan_destroy(backend->audio); backend->audio = candidate; - xtc_midi_plan_destroy(backend->midi); backend->midi = midi; + if (result == XTC_OK) + result = xtc_midi_plan_create(project, backend->expected_rate, &midi); + if (result == XTC_OK) + result = xtc_backend_submit(backend, xtc_project_graph(project)); + if (result != XTC_OK) { + xtc_audio_plan_destroy(candidate); + xtc_midi_plan_destroy(midi); + return result; + } + xtc_audio_plan_destroy(backend->audio); + backend->audio = candidate; + xtc_midi_plan_destroy(backend->midi); + backend->midi = midi; return XTC_OK; } + const char *xtc_backend_asset_issue(const xtc_backend *backend) { const char *path = ""; - if (backend) xtc_audio_plan_issues(backend->audio, &path); + if (backend) + xtc_audio_plan_issues(backend->audio, &path); return path; } + xtc_result xtc_backend_set_recorder(xtc_backend *backend, xtc_recorder *recorder) { - if (!backend) return XTC_ERR_INVALID_ARGUMENT; - if (!backend->ready || backend->running) return XTC_ERR_INVALID_STATE; + if (!backend) + return XTC_ERR_INVALID_ARGUMENT; + if (!backend->ready || backend->running) + return XTC_ERR_INVALID_STATE; if (recorder && (__atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE) || - __atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE))) return XTC_ERR_INVALID_STATE; + __atomic_load_n(&backend->rate_changed, __ATOMIC_ACQUIRE))) + return XTC_ERR_INVALID_STATE; backend->recorder = recorder; return XTC_OK; } + xtc_result xtc_backend_panic(xtc_backend *backend) { - if (!backend) return XTC_ERR_INVALID_ARGUMENT; - if (!backend->ready) return XTC_ERR_INVALID_STATE; + if (!backend) + return XTC_ERR_INVALID_ARGUMENT; + if (!backend->ready) + return XTC_ERR_INVALID_STATE; (void)__atomic_fetch_add(&backend->panic_requested, 1U, __ATOMIC_RELEASE); - if (!backend->running) xtc_midi_plan_reset(backend->midi); + if (!backend->running) + xtc_midi_plan_reset(backend->midi); return XTC_OK; } + xtc_result xtc_backend_midi_port_name(const xtc_backend *backend, const char **out) { - if (!backend || !out) return XTC_ERR_INVALID_ARGUMENT; - if (!backend->ready) return XTC_ERR_INVALID_STATE; - *out = jack_port_name(backend->midi_input); return XTC_OK; + if (!backend || !out) + return XTC_ERR_INVALID_ARGUMENT; + if (!backend->ready) + return XTC_ERR_INVALID_STATE; + *out = jack_port_name(backend->midi_input); + return XTC_OK; } + void xtc_backend_collect(xtc_backend *backend) -{ if (backend != NULL) xtc_realtime_collect(backend->engine); } +{ + if (backend != NULL) + xtc_realtime_collect(backend->engine); +} + xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *out_status) { xtc_realtime_status engine_status; - if (backend == NULL || out_status == NULL) return XTC_ERR_INVALID_ARGUMENT; - if (!backend->ready) return XTC_ERR_INVALID_STATE; + if (backend == NULL || out_status == NULL) + return XTC_ERR_INVALID_ARGUMENT; + if (!backend->ready) + return XTC_ERR_INVALID_STATE; (void)xtc_realtime_get_status(backend->engine, &engine_status); - out_status->peak_left = backend->running ? (float)__atomic_load_n(&backend->peak_left, __ATOMIC_RELAXED) / 1000000.0f : 0; - out_status->peak_right = backend->running ? (float)__atomic_load_n(&backend->peak_right, __ATOMIC_RELAXED) / 1000000.0f : 0; + out_status->peak_left = + backend->running ? (float)__atomic_load_n(&backend->peak_left, __ATOMIC_RELAXED) / 1000000.0f : 0; + out_status->peak_right = + backend->running ? (float)__atomic_load_n(&backend->peak_right, __ATOMIC_RELAXED) / 1000000.0f : 0; out_status->midi_dropped = __atomic_load_n(&backend->midi_dropped, __ATOMIC_RELAXED); out_status->asset_issues = (unsigned int)xtc_audio_plan_issues(backend->audio, NULL); out_status->sample_rate = __atomic_load_n(&backend->rate, __ATOMIC_RELAXED); @@ -355,101 +460,205 @@ xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status out_status->process_result = (xtc_result)__atomic_load_n(&backend->process_result, __ATOMIC_RELAXED); return XTC_OK; } -xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction direction, - const char **out_name) + +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)) return XTC_ERR_INVALID_ARGUMENT; - if (!backend->ready || __atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE; + if (backend == NULL || out_name == NULL || (direction != XTC_PORT_INPUT && direction != XTC_PORT_OUTPUT)) + return XTC_ERR_INVALID_ARGUMENT; + if (!backend->ready || __atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE)) + return XTC_ERR_INVALID_STATE; *out_name = jack_port_name(direction == XTC_PORT_INPUT ? backend->input : backend->output); return *out_name == NULL ? XTC_ERR_IO : XTC_OK; } + xtc_result xtc_backend_output_name(const xtc_backend *backend, size_t channel, const char **out) { - if (!backend || !out || channel > 1) return XTC_ERR_INVALID_ARGUMENT; - if (!backend->ready || __atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE; - if (channel && !backend->output_right) return XTC_ERR_NOT_FOUND; + if (!backend || !out || channel > 1) + return XTC_ERR_INVALID_ARGUMENT; + if (!backend->ready || __atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE)) + return XTC_ERR_INVALID_STATE; + if (channel && !backend->output_right) + return XTC_ERR_NOT_FOUND; *out = jack_port_name(channel ? backend->output_right : backend->output); return *out ? XTC_OK : XTC_ERR_IO; } -xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction direction, - xtc_backend_port_visitor visitor, void *context) + +xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction direction, xtc_backend_port_visitor visitor, + void *context) { const char **ports; size_t i; - if (backend == NULL || visitor == NULL || - (direction != XTC_PORT_INPUT && direction != XTC_PORT_OUTPUT)) return XTC_ERR_INVALID_ARGUMENT; - if (!backend->ready || __atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE)) return XTC_ERR_INVALID_STATE; + if (backend == NULL || visitor == NULL || (direction != XTC_PORT_INPUT && direction != XTC_PORT_OUTPUT)) + return XTC_ERR_INVALID_ARGUMENT; + if (!backend->ready || __atomic_load_n(&backend->lost, __ATOMIC_ACQUIRE)) + return XTC_ERR_INVALID_STATE; ports = jack_get_ports(backend->client, NULL, JACK_DEFAULT_AUDIO_TYPE, - direction == XTC_PORT_INPUT ? JackPortIsInput : JackPortIsOutput); - if (ports == NULL) return XTC_OK; - for (i = 0U; ports[i] != NULL; ++i) if (visitor(ports[i], context) != 0) break; + direction == XTC_PORT_INPUT ? JackPortIsInput : JackPortIsOutput); + if (ports == NULL) + return XTC_OK; + for (i = 0U; ports[i] != NULL; ++i) + if (visitor(ports[i], context) != 0) + break; jack_free(ports); return XTC_OK; } -static xtc_result route(xtc_backend *backend, xtc_port_direction direction, - const char *remote, int disconnect) + +static xtc_result route(xtc_backend *backend, xtc_port_direction direction, const char *remote, int disconnect) { const char *own = NULL; const char *source, *dest; xtc_result result; int code; - if (remote == NULL || remote[0] == '\0') return XTC_ERR_INVALID_ARGUMENT; + if (remote == NULL || remote[0] == '\0') + return XTC_ERR_INVALID_ARGUMENT; result = xtc_backend_port_name(backend, direction, &own); - if (result != XTC_OK) return result; - if (!backend->running) return XTC_ERR_INVALID_STATE; + if (result != XTC_OK) + return result; + if (!backend->running) + return XTC_ERR_INVALID_STATE; source = direction == XTC_PORT_OUTPUT ? own : remote; dest = direction == XTC_PORT_OUTPUT ? remote : own; - code = disconnect ? jack_disconnect(backend->client, source, dest) : - jack_connect(backend->client, source, dest); + code = disconnect ? jack_disconnect(backend->client, source, dest) : jack_connect(backend->client, source, dest); return code == 0 || (!disconnect && code == EEXIST) ? XTC_OK : XTC_ERR_IO; } + xtc_result xtc_backend_connect(xtc_backend *backend, xtc_port_direction direction, const char *remote) -{ return route(backend, direction, remote, 0); } +{ + return route(backend, direction, remote, 0); +} + xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction direction, const char *remote) -{ return route(backend, direction, remote, 1); } +{ + return route(backend, direction, remote, 1); +} #else xtc_result xtc_backend_output_name(const xtc_backend *b, size_t c, const char **out) -{ (void)b; (void)c; (void)out; return XTC_ERR_UNSUPPORTED; } +{ + (void)b; + (void)c; + (void)out; + return XTC_ERR_UNSUPPORTED; +} + xtc_result xtc_backend_panic(xtc_backend *backend) -{ (void)backend; return XTC_ERR_UNSUPPORTED; } +{ + (void)backend; + return XTC_ERR_UNSUPPORTED; +} + xtc_result xtc_backend_midi_port_name(const xtc_backend *backend, const char **out) -{ (void)backend; (void)out; return XTC_ERR_UNSUPPORTED; } +{ + (void)backend; + (void)out; + return XTC_ERR_UNSUPPORTED; +} + xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *project) -{ (void)backend; (void)project; return XTC_ERR_UNSUPPORTED; } +{ + (void)backend; + (void)project; + return XTC_ERR_UNSUPPORTED; +} + const char *xtc_backend_asset_issue(const xtc_backend *backend) -{ (void)backend; return ""; } +{ + (void)backend; + return ""; +} + xtc_result xtc_backend_set_recorder(xtc_backend *backend, xtc_recorder *recorder) -{ (void)backend; (void)recorder; return XTC_ERR_UNSUPPORTED; } +{ + (void)backend; + (void)recorder; + return XTC_ERR_UNSUPPORTED; +} + xtc_result xtc_backend_seek(xtc_backend *backend, uint64_t sample) -{ (void)backend; (void)sample; return XTC_ERR_UNSUPPORTED; } +{ + (void)backend; + (void)sample; + return XTC_ERR_UNSUPPORTED; +} + /* Keep core/offline builds available on machines without JACK headers. */ xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_backend) { - if (out_backend == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (out_backend == NULL) + return XTC_ERR_INVALID_ARGUMENT; *out_backend = NULL; - if (config == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (config == NULL) + return XTC_ERR_INVALID_ARGUMENT; return XTC_ERR_UNSUPPORTED; } + xtc_result xtc_backend_close(xtc_backend *backend) -{ return backend == NULL ? XTC_OK : XTC_ERR_UNSUPPORTED; } +{ + return backend == NULL ? XTC_OK : XTC_ERR_UNSUPPORTED; +} + xtc_result xtc_backend_start(xtc_backend *backend) -{ (void)backend; return XTC_ERR_UNSUPPORTED; } +{ + (void)backend; + return XTC_ERR_UNSUPPORTED; +} + xtc_result xtc_backend_stop(xtc_backend *backend) -{ (void)backend; return XTC_ERR_UNSUPPORTED; } +{ + (void)backend; + return XTC_ERR_UNSUPPORTED; +} + xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph) -{ (void)backend; (void)graph; return XTC_ERR_UNSUPPORTED; } -void xtc_backend_collect(xtc_backend *backend) { (void)backend; } +{ + (void)backend; + (void)graph; + return XTC_ERR_UNSUPPORTED; +} + +void xtc_backend_collect(xtc_backend *backend) +{ + (void)backend; +} + xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *out_status) -{ (void)backend; (void)out_status; return XTC_ERR_UNSUPPORTED; } +{ + (void)backend; + (void)out_status; + return XTC_ERR_UNSUPPORTED; +} + xtc_result xtc_backend_port_name(const xtc_backend *backend, xtc_port_direction direction, const char **out_name) -{ (void)backend; (void)direction; (void)out_name; return XTC_ERR_UNSUPPORTED; } -xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction direction, - xtc_backend_port_visitor visitor, void *context) -{ (void)backend; (void)direction; (void)visitor; (void)context; return XTC_ERR_UNSUPPORTED; } +{ + (void)backend; + (void)direction; + (void)out_name; + return XTC_ERR_UNSUPPORTED; +} + +xtc_result xtc_backend_visit_ports(xtc_backend *backend, xtc_port_direction direction, xtc_backend_port_visitor visitor, + void *context) +{ + (void)backend; + (void)direction; + (void)visitor; + (void)context; + return XTC_ERR_UNSUPPORTED; +} + xtc_result xtc_backend_connect(xtc_backend *backend, xtc_port_direction direction, const char *remote) -{ (void)backend; (void)direction; (void)remote; return XTC_ERR_UNSUPPORTED; } +{ + (void)backend; + (void)direction; + (void)remote; + return XTC_ERR_UNSUPPORTED; +} + xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction direction, const char *remote) -{ (void)backend; (void)direction; (void)remote; return XTC_ERR_UNSUPPORTED; } +{ + (void)backend; + (void)direction; + (void)remote; + return XTC_ERR_UNSUPPORTED; +} #endif diff --git a/src/core/xtc.c b/src/core/xtc.c index 7af5c3a..5a4fa2f 100644 --- a/src/core/xtc.c +++ b/src/core/xtc.c @@ -9,17 +9,28 @@ struct xtc_app { const char *xtc_result_string(xtc_result result) { switch (result) { - case XTC_OK: return "success"; - case XTC_ERR_INVALID_ARGUMENT: return "invalid argument"; - case XTC_ERR_OUT_OF_MEMORY: return "out of memory"; - case XTC_ERR_NOT_FOUND: return "not found"; - case XTC_ERR_INVALID_STATE: return "invalid state"; - case XTC_ERR_IO: return "I/O error"; - case XTC_ERR_UNSUPPORTED: return "unsupported operation"; - case XTC_ERR_CAPACITY: return "capacity exceeded"; - case XTC_ERR_FORMAT: return "malformed project data"; - case XTC_ERR_GRAPH_CYCLE: return "graph contains a cycle"; - default: return "unknown error"; + case XTC_OK: + return "success"; + case XTC_ERR_INVALID_ARGUMENT: + return "invalid argument"; + case XTC_ERR_OUT_OF_MEMORY: + return "out of memory"; + case XTC_ERR_NOT_FOUND: + return "not found"; + case XTC_ERR_INVALID_STATE: + return "invalid state"; + case XTC_ERR_IO: + return "I/O error"; + case XTC_ERR_UNSUPPORTED: + return "unsupported operation"; + case XTC_ERR_CAPACITY: + return "capacity exceeded"; + case XTC_ERR_FORMAT: + return "malformed project data"; + case XTC_ERR_GRAPH_CYCLE: + return "graph contains a cycle"; + default: + return "unknown error"; } } diff --git a/src/dsp/gain.c b/src/dsp/gain.c index 08640da..c4b92b3 100644 --- a/src/dsp/gain.c +++ b/src/dsp/gain.c @@ -1,8 +1,7 @@ #include /* Kept as a separately testable DSP primitive. */ -void xtc_dsp_gain_process(const float *input, float *output, - size_t frame_count, float gain) +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 f1357e7..1dee018 100644 --- a/src/engine/offline_engine.c +++ b/src/engine/offline_engine.c @@ -20,9 +20,12 @@ struct xtc_plan { void xtc_plan_destroy(xtc_plan *plan) { - if (plan == NULL) return; + if (plan == NULL) + return; if (plan->native) { - size_t i; for (i = 0; i < plan->node_count * 2; ++i) xtc_native_destroy(plan->native[i]); + size_t i; + for (i = 0; i < plan->node_count * 2; ++i) + xtc_native_destroy(plan->native[i]); } free(plan->native); free(plan->nodes); @@ -36,27 +39,29 @@ void xtc_plan_destroy(xtc_plan *plan) free(plan); } -xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate, - size_t max_frames, xtc_plan **out_plan) +xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate, size_t max_frames, xtc_plan **out_plan) { xtc_plan *plan; xtc_result result; size_t node, edge, storage_nodes, cursor = 0U; int has_output = 0; - if (out_plan == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (out_plan == NULL) + return XTC_ERR_INVALID_ARGUMENT; *out_plan = NULL; - if (graph == NULL || !isfinite(sample_rate) || sample_rate <= 0.0 || - max_frames == 0U || max_frames > XTC_MAX_FRAMES) return XTC_ERR_INVALID_ARGUMENT; + if (graph == NULL || !isfinite(sample_rate) || sample_rate <= 0.0 || max_frames == 0U || + max_frames > XTC_MAX_FRAMES) + return XTC_ERR_INVALID_ARGUMENT; storage_nodes = graph->node_count == 0U ? 1U : graph->node_count; for (node = 0U; node < graph->node_count; ++node) { - if (graph->nodes[node].type == XTC_NODE_OSCILLATOR && - (double)graph->nodes[node].value > sample_rate * 0.5) + if (graph->nodes[node].type == XTC_NODE_OSCILLATOR && (double)graph->nodes[node].value > sample_rate * 0.5) return XTC_ERR_INVALID_ARGUMENT; - if (graph->nodes[node].type == XTC_NODE_OUTPUT) has_output = 1; + if (graph->nodes[node].type == XTC_NODE_OUTPUT) + has_output = 1; } /* Fixed graph/frame limits bound all allocation products below. */ plan = calloc(1U, sizeof(*plan)); - if (plan == NULL) return XTC_ERR_OUT_OF_MEMORY; + if (plan == NULL) + return XTC_ERR_OUT_OF_MEMORY; plan->node_count = graph->node_count; plan->max_frames = max_frames; plan->sample_rate = sample_rate; @@ -71,13 +76,16 @@ xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate, plan->peaks = calloc(storage_nodes, sizeof(*plan->peaks)); plan->phases = calloc(storage_nodes, sizeof(*plan->phases)); if (plan->native == NULL || plan->nodes == NULL || plan->order == NULL || plan->offsets == NULL || - plan->sources == NULL || plan->render == NULL || plan->buffers == NULL || - plan->peaks == NULL || plan->phases == NULL) { + plan->sources == NULL || plan->render == NULL || plan->buffers == NULL || plan->peaks == NULL || + plan->phases == NULL) { xtc_plan_destroy(plan); return XTC_ERR_OUT_OF_MEMORY; } result = xtc_graph_order(graph, plan->order); - if (result != XTC_OK) { xtc_plan_destroy(plan); return result; } + if (result != XTC_OK) { + xtc_plan_destroy(plan); + return result; + } memcpy(plan->nodes, graph->nodes, plan->node_count * sizeof(*plan->nodes)); for (node = 0U; node < plan->node_count; ++node) { int terminal = 1; @@ -85,18 +93,23 @@ xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate, for (edge = 0U; edge < graph->edge_count; ++edge) { if (graph->edges[edge].destination == node) plan->sources[cursor++] = graph->edges[edge].source; - if (graph->edges[edge].source == node) terminal = 0; + if (graph->edges[edge].source == node) + terminal = 0; } - plan->render[node] = (unsigned char)(has_output || graph->require_output_nodes ? - plan->nodes[node].type == XTC_NODE_OUTPUT : terminal); + plan->render[node] = + (unsigned char)(has_output || graph->require_output_nodes ? plan->nodes[node].type == XTC_NODE_OUTPUT + : terminal); } for (node = 0; node < plan->node_count; ++node) { if (plan->nodes[node].type == XTC_NODE_NATIVE) { size_t side; for (side = 0; side < 2; ++side) { - result = xtc_native_create(&plan->nodes[node].native, sample_rate, max_frames, - plan->nodes[node].value, &plan->native[side * plan->node_count + node]); - if (result != XTC_OK) { xtc_plan_destroy(plan); return result; } + result = xtc_native_create(&plan->nodes[node].native, sample_rate, max_frames, plan->nodes[node].value, + &plan->native[side * plan->node_count + node]); + if (result != XTC_OK) { + xtc_plan_destroy(plan); + return result; + } } } } @@ -107,8 +120,13 @@ xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate, xtc_result xtc_plan_reset(xtc_plan *plan) { - if (plan == NULL) return XTC_ERR_INVALID_ARGUMENT; - { size_t i; for (i = 0; i < plan->node_count * 2; ++i) xtc_native_reset(plan->native[i]); } + if (plan == NULL) + return XTC_ERR_INVALID_ARGUMENT; + { + size_t i; + for (i = 0; i < plan->node_count * 2; ++i) + xtc_native_reset(plan->native[i]); + } plan->position = 0; memset(plan->phases, 0, plan->node_count * sizeof(*plan->phases)); memset(plan->peaks, 0, plan->node_count * sizeof(*plan->peaks)); @@ -117,25 +135,32 @@ xtc_result xtc_plan_reset(xtc_plan *plan) xtc_result xtc_plan_meter_peak(const xtc_plan *plan, size_t node_id, float *out_peak) { - if (plan == NULL || out_peak == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (plan == NULL || out_peak == NULL) + return XTC_ERR_INVALID_ARGUMENT; size_t index; for (index = 0U; index < plan->node_count; ++index) - if (plan->nodes[index].id == node_id) break; - if (index == plan->node_count) return XTC_ERR_NOT_FOUND; + if (plan->nodes[index].id == node_id) + break; + if (index == plan->node_count) + return XTC_ERR_NOT_FOUND; node_id = index; - if (plan->nodes[node_id].type != XTC_NODE_METER) return XTC_ERR_INVALID_ARGUMENT; + if (plan->nodes[node_id].type != XTC_NODE_METER) + return XTC_ERR_INVALID_ARGUMENT; *out_peak = plan->peaks[node_id]; return XTC_OK; } -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) +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) { size_t index, frame, edge, channel; - if (plan == NULL || output == NULL || (right && right == output)) return XTC_ERR_INVALID_ARGUMENT; - if (frame_count > plan->max_frames || plan->position > UINT64_MAX - frame_count) return XTC_ERR_CAPACITY; - if (frame_count == 0U) return XTC_OK; + if (plan == NULL || output == NULL || (right && right == output)) + return XTC_ERR_INVALID_ARGUMENT; + if (frame_count > plan->max_frames || plan->position > UINT64_MAX - frame_count) + return XTC_ERR_CAPACITY; + if (frame_count == 0U) + return XTC_OK; /* All external input reads finish before output is written, allowing aliasing. * Nonfinite input/arithmetic becomes silence per sample, never persistent DSP state. */ for (channel = 0; channel < (right ? 2U : 1U); ++channel) { @@ -157,35 +182,48 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, } else if (spec.type == XTC_NODE_OSCILLATOR) { sample = (float)sin(XTC_TAU * phase); phase += (double)spec.value / plan->sample_rate; - if (phase >= 1.0) phase -= 1.0; + if (phase >= 1.0) + phase -= 1.0; } else { if (plan->offsets[node] == plan->offsets[node + 1U]) sample = input == NULL ? 0.0f : input[frame] * plan->input_gain; for (edge = plan->offsets[node]; edge < plan->offsets[node + 1U]; ++edge) - sample += plan->buffers[(channel * plan->node_count + plan->sources[edge]) * plan->max_frames + frame]; - if (spec.type == XTC_NODE_GAIN) sample *= spec.value; + sample += plan->buffers[(channel * plan->node_count + plan->sources[edge]) * plan->max_frames + + frame]; + if (spec.type == XTC_NODE_GAIN) + sample *= spec.value; } - if (!isfinite(sample)) sample = 0.0f; + if (!isfinite(sample)) + sample = 0.0f; buffer[frame] = sample; - if (fabsf(sample) > peak) peak = fabsf(sample); + if (fabsf(sample) > peak) + peak = fabsf(sample); } if (spec.type == XTC_NODE_NATIVE) { xtc_native_block block = {spec.native.kind == XTC_NATIVE_EFFECT ? buffer : NULL, - buffer, frame_count, plan->position, NULL, 0}; + buffer, + frame_count, + plan->position, + NULL, + 0}; xtc_result result = xtc_native_process(plan->native[channel * plan->node_count + node], &block); if (result != XTC_OK) { memset(output, 0, frame_count * sizeof(*output)); - if (right) memset(right, 0, frame_count * sizeof(*right)); + if (right) + memset(right, 0, frame_count * sizeof(*right)); return result; } } - if (!right || channel == 1) plan->phases[node] = phase; - if (spec.type == XTC_NODE_METER) plan->peaks[node] = channel && plan->peaks[node] > peak ? plan->peaks[node] : peak; + if (!right || channel == 1) + plan->phases[node] = phase; + if (spec.type == XTC_NODE_METER) + plan->peaks[node] = channel && plan->peaks[node] > peak ? plan->peaks[node] : peak; } for (frame = 0U; frame < frame_count; ++frame) { float sample = 0.0f; for (index = 0U; index < plan->node_count; ++index) - if (plan->render[index]) sample += plan->buffers[(channel * plan->node_count + index) * plan->max_frames + frame]; + if (plan->render[index]) + sample += plan->buffers[(channel * plan->node_count + index) * plan->max_frames + frame]; destination[frame] = isfinite(sample) ? sample : 0.0f; } } @@ -193,15 +231,16 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, return XTC_OK; } -xtc_result xtc_graph_process_offline(xtc_graph *graph, const float *input, - float *output, size_t frame_count, float input_gain) +xtc_result xtc_graph_process_offline(xtc_graph *graph, const float *input, float *output, size_t frame_count, + float input_gain) { xtc_plan *plan = NULL; xtc_result result; if (input == NULL || output == NULL || !isfinite(input_gain)) return XTC_ERR_INVALID_ARGUMENT; result = xtc_graph_compile(graph, 48000.0, frame_count == 0U ? 1U : frame_count, &plan); - if (result != XTC_OK) return result; + if (result != XTC_OK) + return result; plan->input_gain = input_gain; result = xtc_plan_process(plan, input, output, frame_count); xtc_plan_destroy(plan); @@ -209,10 +248,18 @@ xtc_result xtc_graph_process_offline(xtc_graph *graph, const float *input, } 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); } +{ + return xtc_plan_process_audio(plan, input, NULL, output, frames); +} -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); } +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); +} -xtc_result xtc_plan_process_sources(xtc_plan *p, const float *in, const float *audio, const float *midi, float *out, size_t frames) -{ return xtc_plan_process_stereo(p, in, audio, NULL, midi, NULL, out, NULL, frames); } +xtc_result xtc_plan_process_sources(xtc_plan *p, const float *in, const float *audio, const float *midi, float *out, + size_t frames) +{ + return xtc_plan_process_stereo(p, in, audio, NULL, midi, NULL, out, NULL, frames); +} diff --git a/src/engine/realtime.c b/src/engine/realtime.c index fdcbb0d..1fbbc75 100644 --- a/src/engine/realtime.c +++ b/src/engine/realtime.c @@ -14,18 +14,19 @@ struct xtc_realtime { unsigned int submitted_revision, pending_revision, active_revision; }; -xtc_result xtc_realtime_create(double sample_rate, size_t max_frames, - xtc_realtime **out_engine) +xtc_result xtc_realtime_create(double sample_rate, size_t max_frames, xtc_realtime **out_engine) { xtc_realtime *engine; - if (out_engine == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (out_engine == NULL) + return XTC_ERR_INVALID_ARGUMENT; *out_engine = NULL; - if (!isfinite(sample_rate) || sample_rate <= 0.0 || max_frames == 0U || - max_frames > 65536U) return XTC_ERR_INVALID_ARGUMENT; - if (!__atomic_always_lock_free(sizeof(xtc_plan *), 0) || - !__atomic_always_lock_free(sizeof(unsigned int), 0)) return XTC_ERR_UNSUPPORTED; + if (!isfinite(sample_rate) || sample_rate <= 0.0 || max_frames == 0U || max_frames > 65536U) + return XTC_ERR_INVALID_ARGUMENT; + if (!__atomic_always_lock_free(sizeof(xtc_plan *), 0) || !__atomic_always_lock_free(sizeof(unsigned int), 0)) + return XTC_ERR_UNSUPPORTED; engine = calloc(1U, sizeof(*engine)); - if (engine == NULL) return XTC_ERR_OUT_OF_MEMORY; + if (engine == NULL) + return XTC_ERR_OUT_OF_MEMORY; engine->sample_rate = sample_rate; engine->max_frames = max_frames; *out_engine = engine; @@ -35,14 +36,16 @@ xtc_result xtc_realtime_create(double sample_rate, size_t max_frames, void xtc_realtime_collect(xtc_realtime *engine) { xtc_plan *retired; - if (engine == NULL) return; + if (engine == NULL) + return; retired = __atomic_exchange_n(&engine->retired, NULL, __ATOMIC_ACQ_REL); xtc_plan_destroy(retired); } void xtc_realtime_destroy(xtc_realtime *engine) { - if (engine == NULL) return; + if (engine == NULL) + return; /* Lifecycle contract: no callback can still execute. */ xtc_plan_destroy(engine->active); xtc_plan_destroy(engine->pending); @@ -54,12 +57,14 @@ xtc_result xtc_realtime_submit(xtc_realtime *engine, const xtc_graph *graph) { xtc_plan *plan = NULL; xtc_result result; - if (engine == NULL || graph == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (engine == NULL || graph == NULL) + return XTC_ERR_INVALID_ARGUMENT; xtc_realtime_collect(engine); if (__atomic_load_n(&engine->pending, __ATOMIC_ACQUIRE) != NULL) return XTC_ERR_CAPACITY; result = xtc_graph_compile(graph, engine->sample_rate, engine->max_frames, &plan); - if (result != XTC_OK) return result; + if (result != XTC_OK) + return result; { unsigned int revision = __atomic_load_n(&engine->submitted_revision, __ATOMIC_RELAXED) + 1U; __atomic_store_n(&engine->pending_revision, revision, __ATOMIC_RELAXED); @@ -69,10 +74,10 @@ xtc_result xtc_realtime_submit(xtc_realtime *engine, const xtc_graph *graph) return XTC_OK; } -xtc_result xtc_realtime_get_status(const xtc_realtime *engine, - xtc_realtime_status *out_status) +xtc_result xtc_realtime_get_status(const xtc_realtime *engine, xtc_realtime_status *out_status) { - if (engine == NULL || out_status == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (engine == NULL || out_status == NULL) + return XTC_ERR_INVALID_ARGUMENT; out_status->processed_blocks = __atomic_load_n(&engine->blocks, __ATOMIC_RELAXED); out_status->submitted_revision = __atomic_load_n(&engine->submitted_revision, __ATOMIC_RELAXED); out_status->active_revision = __atomic_load_n(&engine->active_revision, __ATOMIC_RELAXED); @@ -80,14 +85,15 @@ xtc_result xtc_realtime_get_status(const xtc_realtime *engine, return XTC_OK; } -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) +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) { xtc_result result = XTC_OK; if (engine == NULL || output == NULL || (right && right == output) || frame_count > SIZE_MAX / sizeof(*output)) return XTC_ERR_INVALID_ARGUMENT; - if (frame_count == 0U) return XTC_OK; + if (frame_count == 0U) + return XTC_OK; if (frame_count > engine->max_frames) { result = XTC_ERR_CAPACITY; } else { @@ -108,27 +114,37 @@ xtc_result xtc_realtime_process_stereo(xtc_realtime *engine, const float *input, } } if (engine->active != NULL) - result = xtc_plan_process_stereo(engine->active, input, timeline, timeline_right, instrument, instrument_right, output, right, frame_count); + result = xtc_plan_process_stereo(engine->active, input, timeline, timeline_right, instrument, + instrument_right, output, right, frame_count); else { memset(output, 0, frame_count * sizeof(*output)); - if (right) memset(right, 0, frame_count * sizeof(*right)); + if (right) + memset(right, 0, frame_count * sizeof(*right)); } } if (result != XTC_OK) { memset(output, 0, frame_count * sizeof(*output)); - if (right) memset(right, 0, frame_count * sizeof(*right)); + if (right) + memset(right, 0, frame_count * sizeof(*right)); } __atomic_store_n(&engine->result, (unsigned int)result, __ATOMIC_RELAXED); - __atomic_store_n(&engine->blocks, - __atomic_load_n(&engine->blocks, __ATOMIC_RELAXED) + 1U, __ATOMIC_RELAXED); + __atomic_store_n(&engine->blocks, __atomic_load_n(&engine->blocks, __ATOMIC_RELAXED) + 1U, __ATOMIC_RELAXED); return result; } 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); } +{ + return xtc_realtime_process_audio(engine, input, NULL, output, frames); +} -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); } +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); +} -xtc_result xtc_realtime_process_sources(xtc_realtime *p, const float *in, const float *audio, const float *midi, float *out, size_t frames) -{ return xtc_realtime_process_stereo(p, in, audio, NULL, midi, NULL, out, NULL, frames); } +xtc_result xtc_realtime_process_sources(xtc_realtime *p, const float *in, const float *audio, const float *midi, + float *out, size_t frames) +{ + return xtc_realtime_process_stereo(p, in, audio, NULL, midi, NULL, out, NULL, frames); +} diff --git a/src/graph/graph.c b/src/graph/graph.c index 045f48d..c1f71bc 100644 --- a/src/graph/graph.c +++ b/src/graph/graph.c @@ -6,11 +6,14 @@ xtc_result xtc_graph_create(size_t max_nodes, xtc_graph **out_graph) { xtc_graph *graph; - if (out_graph == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (out_graph == NULL) + return XTC_ERR_INVALID_ARGUMENT; *out_graph = NULL; - if (max_nodes == 0U || max_nodes > XTC_MAX_NODES) return XTC_ERR_INVALID_ARGUMENT; + if (max_nodes == 0U || max_nodes > XTC_MAX_NODES) + return XTC_ERR_INVALID_ARGUMENT; graph = calloc(1U, sizeof(*graph)); - if (graph == NULL) return XTC_ERR_OUT_OF_MEMORY; + if (graph == NULL) + return XTC_ERR_OUT_OF_MEMORY; graph->nodes = calloc(max_nodes, sizeof(*graph->nodes)); graph->edges = calloc(max_nodes * max_nodes, sizeof(*graph->edges)); if (graph->nodes == NULL || graph->edges == NULL) { @@ -34,49 +37,71 @@ void xtc_graph_destroy(xtc_graph *graph) static size_t node_index(const xtc_graph *graph, size_t id) { size_t i; - for (i = 0U; i < graph->node_count; ++i) if (graph->nodes[i].id == id) return i; + for (i = 0U; i < graph->node_count; ++i) + if (graph->nodes[i].id == id) + return i; return SIZE_MAX; } -static xtc_result add_node(xtc_graph *graph, xtc_node_type type, float value, - size_t *out_node_id) +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)) return XTC_ERR_INVALID_ARGUMENT; - if (graph->node_count == graph->max_nodes || graph->next_id == SIZE_MAX) return XTC_ERR_CAPACITY; + if (graph->node_count == graph->max_nodes || graph->next_id == SIZE_MAX) + return XTC_ERR_CAPACITY; graph->nodes[graph->node_count].type = type; graph->nodes[graph->node_count].value = value; graph->nodes[graph->node_count++].id = graph->next_id; *out_node_id = graph->next_id++; return XTC_OK; } + 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); } +{ + return add_node(graph, XTC_NODE_GAIN, gain, out_node_id); +} + xtc_result xtc_graph_add_oscillator(xtc_graph *graph, float frequency, size_t *out_node_id) { - if (frequency < 0.0f) return XTC_ERR_INVALID_ARGUMENT; + if (frequency < 0.0f) + return XTC_ERR_INVALID_ARGUMENT; return add_node(graph, XTC_NODE_OSCILLATOR, frequency, out_node_id); } -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); } -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); } -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); } -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); } -xtc_result xtc_graph_get_port(const xtc_graph *graph, size_t node_id, - size_t port_id, xtc_port_info *out_port) +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); +} + +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); +} + +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); +} + +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); +} + +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; - if (graph == NULL || out_port == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (graph == NULL || out_port == NULL) + return XTC_ERR_INVALID_ARGUMENT; node_id = node_index(graph, node_id); - if (node_id == SIZE_MAX) return XTC_ERR_NOT_FOUND; + if (node_id == SIZE_MAX) + return XTC_ERR_NOT_FOUND; type = graph->nodes[node_id].type; - if (port_id > 1U || (port_id == 0U && (type == XTC_NODE_OSCILLATOR || type == XTC_NODE_AUDIO || type == XTC_NODE_INSTRUMENT || - (type == XTC_NODE_NATIVE && graph->nodes[node_id].native.kind != XTC_NATIVE_EFFECT))) || - (port_id == 1U && type == XTC_NODE_OUTPUT)) return XTC_ERR_NOT_FOUND; + if (port_id > 1U || + (port_id == 0U && (type == XTC_NODE_OSCILLATOR || type == XTC_NODE_AUDIO || type == XTC_NODE_INSTRUMENT || + (type == XTC_NODE_NATIVE && graph->nodes[node_id].native.kind != XTC_NATIVE_EFFECT))) || + (port_id == 1U && type == XTC_NODE_OUTPUT)) + return XTC_ERR_NOT_FOUND; out_port->direction = port_id == 0U ? XTC_PORT_INPUT : XTC_PORT_OUTPUT; out_port->signal_type = XTC_SIGNAL_AUDIO; out_port->channel_count = 1U; @@ -87,17 +112,20 @@ xtc_result xtc_graph_order(const xtc_graph *graph, size_t *order) { size_t indegree[XTC_MAX_NODES] = {0U}; size_t head = 0U, tail = 0U, i; - if (graph == NULL || order == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (graph == NULL || order == NULL) + return XTC_ERR_INVALID_ARGUMENT; for (i = 0U; i < graph->edge_count; ++i) ++indegree[graph->edges[i].destination]; for (i = 0U; i < graph->node_count; ++i) - if (indegree[i] == 0U) order[tail++] = i; + if (indegree[i] == 0U) + order[tail++] = i; while (head < tail) { size_t node = order[head++]; for (i = 0U; i < graph->edge_count; ++i) { if (graph->edges[i].source == node) { size_t dest = graph->edges[i].destination; - if (--indegree[dest] == 0U) order[tail++] = dest; + if (--indegree[dest] == 0U) + order[tail++] = dest; } } } @@ -110,16 +138,17 @@ xtc_result xtc_graph_validate(const xtc_graph *graph) return xtc_graph_order(graph, order); } -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) +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) { xtc_port_info source, dest; size_t i; xtc_result result = xtc_graph_get_port(graph, source_node, source_port, &source); - if (result != XTC_OK) return result; + if (result != XTC_OK) + return result; result = xtc_graph_get_port(graph, destination_node, destination_port, &dest); - if (result != XTC_OK) return result; + if (result != XTC_OK) + return result; if (source.direction != XTC_PORT_OUTPUT || dest.direction != XTC_PORT_INPUT || source.signal_type != dest.signal_type || source.channel_count != dest.channel_count) return XTC_ERR_INVALID_ARGUMENT; @@ -128,97 +157,123 @@ xtc_result xtc_graph_connect_ports(xtc_graph *graph, size_t source_node, for (i = 0U; i < graph->edge_count; ++i) if (graph->edges[i].source == source_node && graph->edges[i].destination == destination_node) return XTC_ERR_INVALID_STATE; - if (graph->edge_count == graph->max_nodes * graph->max_nodes) return XTC_ERR_CAPACITY; + if (graph->edge_count == graph->max_nodes * graph->max_nodes) + return XTC_ERR_CAPACITY; graph->edges[graph->edge_count].source = source_node; graph->edges[graph->edge_count++].destination = destination_node; result = xtc_graph_validate(graph); - if (result != XTC_OK) --graph->edge_count; + if (result != XTC_OK) + --graph->edge_count; return result; } + 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); } +{ + return xtc_graph_connect_ports(graph, source_node, 1U, destination_node, 0U); +} 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) return XTC_ERR_INVALID_ARGUMENT; + if (graph == NULL || out_nodes == NULL || out_connections == NULL) + return XTC_ERR_INVALID_ARGUMENT; *out_nodes = graph->node_count; *out_connections = graph->edge_count; return XTC_OK; } + xtc_result xtc_graph_get_node(const xtc_graph *graph, size_t index, xtc_node_info *out_node) { - if (graph == NULL || out_node == NULL) return XTC_ERR_INVALID_ARGUMENT; - if (index >= graph->node_count) return XTC_ERR_NOT_FOUND; + if (graph == NULL || out_node == NULL) + return XTC_ERR_INVALID_ARGUMENT; + if (index >= graph->node_count) + return XTC_ERR_NOT_FOUND; out_node->id = graph->nodes[index].id; out_node->type = graph->nodes[index].type; out_node->value = graph->nodes[index].value; return XTC_OK; } + xtc_result xtc_graph_get_connection(const xtc_graph *graph, size_t index, xtc_connection_info *out_connection) { - if (graph == NULL || out_connection == NULL) return XTC_ERR_INVALID_ARGUMENT; - if (index >= graph->edge_count) return XTC_ERR_NOT_FOUND; + if (graph == NULL || out_connection == NULL) + return XTC_ERR_INVALID_ARGUMENT; + if (index >= graph->edge_count) + return XTC_ERR_NOT_FOUND; out_connection->source_node = graph->nodes[graph->edges[index].source].id; out_connection->destination_node = graph->nodes[graph->edges[index].destination].id; out_connection->source_port = 1U; out_connection->destination_port = 0U; return XTC_OK; } + xtc_result xtc_graph_require_output_nodes(xtc_graph *graph, int required) { - if (graph == NULL || (required != 0 && required != 1)) return XTC_ERR_INVALID_ARGUMENT; + if (graph == NULL || (required != 0 && required != 1)) + return XTC_ERR_INVALID_ARGUMENT; graph->require_output_nodes = required; return XTC_OK; } + static xtc_result remove_node(xtc_graph *graph, size_t id) { size_t index = node_index(graph, id), i, count = 0U; - if (index == SIZE_MAX) return XTC_ERR_NOT_FOUND; + if (index == SIZE_MAX) + return XTC_ERR_NOT_FOUND; for (i = 0U; i < graph->edge_count; ++i) { xtc_edge edge = graph->edges[i]; - if (edge.source == index || edge.destination == index) continue; - if (edge.source > index) --edge.source; - if (edge.destination > index) --edge.destination; + if (edge.source == index || edge.destination == index) + continue; + if (edge.source > index) + --edge.source; + if (edge.destination > index) + --edge.destination; graph->edges[count++] = edge; } graph->edge_count = count; - memmove(graph->nodes + index, graph->nodes + index + 1U, - (graph->node_count - index - 1U) * sizeof(*graph->nodes)); + memmove(graph->nodes + index, graph->nodes + index + 1U, (graph->node_count - index - 1U) * sizeof(*graph->nodes)); --graph->node_count; return XTC_OK; } + static xtc_result disconnect_ports(xtc_graph *graph, const xtc_connection_info *connection) { size_t i; xtc_port_info port; xtc_result result = xtc_graph_get_port(graph, connection->source_node, connection->source_port, &port); - if (result != XTC_OK) return result; - if (port.direction != XTC_PORT_OUTPUT) return XTC_ERR_INVALID_ARGUMENT; + if (result != XTC_OK) + return result; + if (port.direction != XTC_PORT_OUTPUT) + return XTC_ERR_INVALID_ARGUMENT; result = xtc_graph_get_port(graph, connection->destination_node, connection->destination_port, &port); - if (result != XTC_OK) return result; - if (port.direction != XTC_PORT_INPUT) return XTC_ERR_INVALID_ARGUMENT; + if (result != XTC_OK) + return result; + if (port.direction != XTC_PORT_INPUT) + return XTC_ERR_INVALID_ARGUMENT; for (i = 0U; i < graph->edge_count; ++i) { if (graph->edges[i].source == node_index(graph, connection->source_node) && graph->edges[i].destination == node_index(graph, connection->destination_node)) { - memmove(graph->edges + i, graph->edges + i + 1U, - (graph->edge_count - i - 1U) * sizeof(*graph->edges)); + memmove(graph->edges + i, graph->edges + i + 1U, (graph->edge_count - i - 1U) * sizeof(*graph->edges)); --graph->edge_count; return XTC_OK; } } return XTC_ERR_NOT_FOUND; } -xtc_result xtc_graph_apply_edit(const xtc_graph *source, const xtc_graph_edit *edit, - xtc_graph **out_graph, size_t *out_added_id) + +xtc_result xtc_graph_apply_edit(const xtc_graph *source, const xtc_graph_edit *edit, xtc_graph **out_graph, + size_t *out_added_id) { xtc_graph *graph; xtc_result result; size_t added_id = 0U; - if (out_graph == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (out_graph == NULL) + return XTC_ERR_INVALID_ARGUMENT; *out_graph = NULL; - if (source == NULL || edit == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (source == NULL || edit == NULL) + return XTC_ERR_INVALID_ARGUMENT; result = xtc_graph_create(source->max_nodes, &graph); - if (result != XTC_OK) return result; + if (result != XTC_OK) + return result; graph->node_count = source->node_count; graph->edge_count = source->edge_count; graph->next_id = source->next_id; @@ -228,26 +283,50 @@ xtc_result xtc_graph_apply_edit(const xtc_graph *source, const xtc_graph_edit *e switch (edit->kind) { case XTC_EDIT_ADD_NODE: switch (edit->data.add.type) { - case XTC_NODE_GAIN: result = xtc_graph_add_gain(graph, edit->data.add.value, &added_id); break; - case XTC_NODE_OSCILLATOR: result = xtc_graph_add_oscillator(graph, edit->data.add.value, &added_id); break; - case XTC_NODE_METER: result = xtc_graph_add_meter(graph, &added_id); break; - case XTC_NODE_INSTRUMENT: result = xtc_graph_add_instrument(graph, &added_id); break; - case XTC_NODE_AUDIO: result = xtc_graph_add_audio(graph, &added_id); break; - case XTC_NODE_OUTPUT: result = xtc_graph_add_output(graph, &added_id); break; - default: result = XTC_ERR_INVALID_ARGUMENT; break; + case XTC_NODE_GAIN: + result = xtc_graph_add_gain(graph, edit->data.add.value, &added_id); + break; + case XTC_NODE_OSCILLATOR: + result = xtc_graph_add_oscillator(graph, edit->data.add.value, &added_id); + break; + case XTC_NODE_METER: + result = xtc_graph_add_meter(graph, &added_id); + break; + case XTC_NODE_INSTRUMENT: + result = xtc_graph_add_instrument(graph, &added_id); + break; + case XTC_NODE_AUDIO: + result = xtc_graph_add_audio(graph, &added_id); + break; + case XTC_NODE_OUTPUT: + result = xtc_graph_add_output(graph, &added_id); + break; + default: + result = XTC_ERR_INVALID_ARGUMENT; + break; } break; - case XTC_EDIT_REMOVE_NODE: result = remove_node(graph, edit->data.node_id); break; + case XTC_EDIT_REMOVE_NODE: + result = remove_node(graph, edit->data.node_id); + break; case XTC_EDIT_CONNECT: - result = xtc_graph_connect_ports(graph, edit->data.connection.source_node, - edit->data.connection.source_port, edit->data.connection.destination_node, - edit->data.connection.destination_port); + result = + xtc_graph_connect_ports(graph, edit->data.connection.source_node, edit->data.connection.source_port, + edit->data.connection.destination_node, edit->data.connection.destination_port); + break; + case XTC_EDIT_DISCONNECT: + result = disconnect_ports(graph, &edit->data.connection); + break; + default: + result = XTC_ERR_INVALID_ARGUMENT; break; - case XTC_EDIT_DISCONNECT: result = disconnect_ports(graph, &edit->data.connection); break; - default: result = XTC_ERR_INVALID_ARGUMENT; break; } - if (result != XTC_OK) { xtc_graph_destroy(graph); return result; } - if (out_added_id != NULL && edit->kind == XTC_EDIT_ADD_NODE) *out_added_id = added_id; + if (result != XTC_OK) { + xtc_graph_destroy(graph); + return result; + } + if (out_added_id != NULL && edit->kind == XTC_EDIT_ADD_NODE) + *out_added_id = added_id; *out_graph = graph; return XTC_OK; } @@ -255,18 +334,26 @@ xtc_result xtc_graph_apply_edit(const xtc_graph *source, const xtc_graph_edit *e 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); - if (result != XTC_OK) return result; - if (descriptor->kind == XTC_NATIVE_INSTRUMENT) return XTC_ERR_UNSUPPORTED; + if (result != XTC_OK) + return result; + if (descriptor->kind == XTC_NATIVE_INSTRUMENT) + return XTC_ERR_UNSUPPORTED; result = add_node(graph, XTC_NODE_NATIVE, value, out); - if (result == XTC_OK) graph->nodes[graph->node_count - 1].native = *descriptor; + if (result == XTC_OK) + graph->nodes[graph->node_count - 1].native = *descriptor; return result; } + xtc_result xtc_graph_get_native(const xtc_graph *graph, size_t id, xtc_native_descriptor *out) { size_t i; - if (!graph || !out) return XTC_ERR_INVALID_ARGUMENT; + if (!graph || !out) + return XTC_ERR_INVALID_ARGUMENT; i = node_index(graph, id); - if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND; - if (graph->nodes[i].type != XTC_NODE_NATIVE) return XTC_ERR_INVALID_ARGUMENT; - *out = graph->nodes[i].native; return XTC_OK; + if (i == SIZE_MAX) + return XTC_ERR_NOT_FOUND; + if (graph->nodes[i].type != XTC_NODE_NATIVE) + return XTC_ERR_INVALID_ARGUMENT; + *out = graph->nodes[i].native; + return XTC_OK; } diff --git a/src/graph/graph_internal.h b/src/graph/graph_internal.h index ab334f9..23ae3b8 100644 --- a/src/graph/graph_internal.h +++ b/src/graph/graph_internal.h @@ -1,16 +1,30 @@ #ifndef XTC_GRAPH_INTERNAL_H #define XTC_GRAPH_INTERNAL_H + #include "xtc/xtc_engine.h" #include "xtc/xtc_native.h" + #define XTC_MAX_NODES 256U #define XTC_MAX_FRAMES 65536U -typedef struct { xtc_node_type type; float value; size_t id; xtc_native_descriptor native; } xtc_node; -typedef struct { size_t source; size_t destination; } xtc_edge; + +typedef struct { + xtc_node_type type; + float value; + size_t id; + xtc_native_descriptor native; +} xtc_node; + +typedef struct { + size_t source; + size_t destination; +} xtc_edge; + struct xtc_graph { size_t max_nodes, node_count, edge_count, next_id; int require_output_nodes; xtc_node *nodes; xtc_edge *edges; }; + 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 658ce53..7c93ee1 100644 --- a/src/gui/graph_widget.cpp +++ b/src/gui/graph_widget.cpp @@ -8,42 +8,61 @@ namespace { constexpr int node_width = 150, node_height = 74; + const char *node_name(xtc_node_type type) { switch (type) { - case XTC_NODE_OSCILLATOR: return "Oscillator"; - case XTC_NODE_GAIN: return "Gain"; - case XTC_NODE_METER: return "Meter"; - case XTC_NODE_NATIVE: return "Native processor"; - case XTC_NODE_INSTRUMENT: return "Instrument"; - case XTC_NODE_AUDIO: return "Audio clips"; - case XTC_NODE_OUTPUT: return "Output"; + case XTC_NODE_OSCILLATOR: + return "Oscillator"; + case XTC_NODE_GAIN: + return "Gain"; + case XTC_NODE_METER: + return "Meter"; + case XTC_NODE_NATIVE: + return "Native processor"; + case XTC_NODE_INSTRUMENT: + return "Instrument"; + case XTC_NODE_AUDIO: + return "Audio clips"; + case XTC_NODE_OUTPUT: + return "Output"; } return "Unknown"; } +} // namespace + +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) +{ + set_graph(graph); } -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) -{ set_graph(graph); } void GraphWidget::set_graph(const xtc_graph *graph) { graph_ = graph; size_t nodes = 0U, edges = 0U; - if (xtc_graph_get_counts(graph_, &nodes, &edges) != XTC_OK) { count_ = 0U; redraw(); return; } + if (xtc_graph_get_counts(graph_, &nodes, &edges) != XTC_OK) { + count_ = 0U; + redraw(); + return; + } auto old = positions_; size_t old_count = count_; count_ = 0U; for (size_t i = 0U; i < nodes && i < positions_.size(); ++i) { xtc_node_info node{}; - if (xtc_graph_get_node(graph_, i, &node) != XTC_OK) continue; + if (xtc_graph_get_node(graph_, i, &node) != XTC_OK) + continue; Position position; position.id = node.id; bool found = false; for (size_t j = 0U; j < old_count; ++j) - if (old[j].id == node.id) { position = old[j]; found = true; break; } + if (old[j].id == node.id) { + position = old[j]; + found = true; + break; + } if (!found) { for (size_t slot = 0U; slot <= positions_.size(); ++slot) { position.left = 40 + static_cast(slot % 8U) * 210; @@ -51,62 +70,89 @@ void GraphWidget::set_graph(const xtc_graph *graph) bool occupied = false; for (size_t j = 0U; j < old_count; ++j) if (std::abs(old[j].left - position.left) < node_width + 12 && - std::abs(old[j].top - position.top) < node_height + 12) occupied = true; - if (!occupied) break; + std::abs(old[j].top - position.top) < node_height + 12) + occupied = true; + if (!occupied) + break; } } positions_[count_++] = position; } int left, top; - if (!node_position(selected_node_, left, top)) selected_node_ = SIZE_MAX; + if (!node_position(selected_node_, left, top)) + selected_node_ = SIZE_MAX; if (edge_selected_ && !select_connection(selected_edge_.source_node, selected_edge_.destination_node)) edge_selected_ = false; redraw(); } + bool GraphWidget::node_position(size_t id, int &left, int &top) const { - for (size_t i = 0U; i < count_; ++i) if (positions_[i].id == id) { - left = positions_[i].left; top = positions_[i].top; return true; - } + for (size_t i = 0U; i < count_; ++i) + if (positions_[i].id == id) { + left = positions_[i].left; + top = positions_[i].top; + return true; + } return false; } + bool GraphWidget::move_node(size_t id, int left, int top) { - for (size_t i = 0U; i < count_; ++i) if (positions_[i].id == id) { - positions_[i].left = std::max(12, std::min(left, w() - node_width - 12)); - positions_[i].top = std::max(12, std::min(top, h() - node_height - 12)); - redraw(); return true; - } + for (size_t i = 0U; i < count_; ++i) + if (positions_[i].id == id) { + positions_[i].left = std::max(12, std::min(left, w() - node_width - 12)); + positions_[i].top = std::max(12, std::min(top, h() - node_height - 12)); + redraw(); + return true; + } return false; } + bool GraphWidget::select_node(size_t id) { int left, top; - if (!node_position(id, left, top)) return false; - selected_node_ = id; edge_selected_ = false; redraw(); return true; + if (!node_position(id, left, top)) + return false; + selected_node_ = id; + edge_selected_ = false; + redraw(); + return true; } + bool GraphWidget::select_connection(size_t source, size_t destination) { size_t nodes, edges; - if (xtc_graph_get_counts(graph_, &nodes, &edges) != XTC_OK) return false; + if (xtc_graph_get_counts(graph_, &nodes, &edges) != XTC_OK) + return false; for (size_t i = 0U; i < edges; ++i) { xtc_connection_info edge{}; - if (xtc_graph_get_connection(graph_, i, &edge) == XTC_OK && - edge.source_node == source && edge.destination_node == destination) { - selected_edge_ = edge; edge_selected_ = true; selected_node_ = SIZE_MAX; redraw(); return true; + if (xtc_graph_get_connection(graph_, i, &edge) == XTC_OK && edge.source_node == source && + edge.destination_node == destination) { + selected_edge_ = edge; + edge_selected_ = true; + selected_node_ = SIZE_MAX; + redraw(); + return true; } } return false; } + void GraphWidget::feedback(xtc_result result, const char *success) { char text[256]; - if (result == XTC_OK) std::snprintf(text, sizeof(text), "%s", success); + if (result == XTC_OK) + std::snprintf(text, sizeof(text), "%s", success); else if (result == XTC_ERR_CAPACITY) - std::snprintf(text, sizeof(text), "Graph unchanged: capacity exceeded (node limit or pending edit). Retry after the next audio block."); - else std::snprintf(text, sizeof(text), "Graph unchanged: %s.", xtc_result_string(result)); + std::snprintf( + text, sizeof(text), + "Graph unchanged: capacity exceeded (node limit or pending edit). Retry after the next audio block."); + else + std::snprintf(text, sizeof(text), "Graph unchanged: %s.", xtc_result_string(result)); feedback_->copy_label(text); } + xtc_result GraphWidget::add_node(xtc_node_type type) { xtc_graph_edit edit{}; @@ -115,10 +161,12 @@ xtc_result GraphWidget::add_node(xtc_node_type type) edit.data.add.value = type == XTC_NODE_OSCILLATOR ? 440.0f : type == XTC_NODE_GAIN ? 0.1f : 0.0f; size_t added = SIZE_MAX; xtc_result result = handler_(context_, edit, &added); - if (result == XTC_OK) select_node(added); + if (result == XTC_OK) + select_node(added); feedback(result, "Node added. Drag its title to move; drag an output port to an input to connect."); return result; } + xtc_result GraphWidget::connect_ports(const xtc_connection_info &connection) { xtc_graph_edit edit{}; @@ -128,46 +176,63 @@ xtc_result GraphWidget::connect_ports(const xtc_connection_info &connection) feedback(result, "Connection added. Click a wire and press Delete to disconnect."); return result; } + xtc_result GraphWidget::delete_selected() { xtc_graph_edit edit{}; - if (edge_selected_) { edit.kind = XTC_EDIT_DISCONNECT; edit.data.connection = selected_edge_; } - else if (selected_node_ != SIZE_MAX) { edit.kind = XTC_EDIT_REMOVE_NODE; edit.data.node_id = selected_node_; } - else { feedback_->copy_label("Select a node or wire first."); return XTC_ERR_INVALID_STATE; } + if (edge_selected_) { + edit.kind = XTC_EDIT_DISCONNECT; + edit.data.connection = selected_edge_; + } else if (selected_node_ != SIZE_MAX) { + edit.kind = XTC_EDIT_REMOVE_NODE; + edit.data.node_id = selected_node_; + } else { + feedback_->copy_label("Select a node or wire first."); + return XTC_ERR_INVALID_STATE; + } xtc_result result = handler_(context_, edit, nullptr); feedback(result, "Selection deleted."); return result; } + bool GraphWidget::port_position(size_t id, size_t port, int &left, int &top) const { xtc_port_info info{}; - if (xtc_graph_get_port(graph_, id, port, &info) != XTC_OK || !node_position(id, left, top)) return false; + if (xtc_graph_get_port(graph_, id, port, &info) != XTC_OK || !node_position(id, left, top)) + return false; left += info.direction == XTC_PORT_OUTPUT ? node_width : 0; top += 48; return true; } + bool GraphWidget::hit_port(int left, int top, size_t &node, size_t &port) const { - for (size_t i = count_; i > 0U; --i) for (size_t p = 0U; p < 2U; ++p) { - int px, py; - if (port_position(positions_[i - 1U].id, p, px, py) && - std::abs(px - left) <= 9 && std::abs(py - top) <= 9) { - node = positions_[i - 1U].id; port = p; return true; + for (size_t i = count_; i > 0U; --i) + for (size_t p = 0U; p < 2U; ++p) { + int px, py; + if (port_position(positions_[i - 1U].id, p, px, py) && std::abs(px - left) <= 9 && + std::abs(py - top) <= 9) { + node = positions_[i - 1U].id; + port = p; + return true; + } } - } return false; } + void GraphWidget::draw() { fl_push_clip(x(), y(), w(), h()); - fl_color(fl_rgb_color(25, 28, 33)); fl_rectf(x(), y(), w(), h()); + fl_color(fl_rgb_color(25, 28, 33)); + fl_rectf(x(), y(), w(), h()); size_t nodes = 0U, edges = 0U; (void)xtc_graph_get_counts(graph_, &nodes, &edges); for (size_t i = 0U; i < edges; ++i) { xtc_connection_info edge{}; int sx, sy, dx, dy; - if (xtc_graph_get_connection(graph_, i, &edge) != XTC_OK || - !port_position(edge.source_node, 1U, sx, sy) || !port_position(edge.destination_node, 0U, dx, dy)) continue; + if (xtc_graph_get_connection(graph_, i, &edge) != XTC_OK || !port_position(edge.source_node, 1U, sx, sy) || + !port_position(edge.destination_node, 0U, dx, dy)) + continue; bool selected = edge_selected_ && selected_edge_.source_node == edge.source_node && selected_edge_.destination_node == edge.destination_node; fl_color(selected ? FL_YELLOW : fl_rgb_color(100, 200, 190)); @@ -177,49 +242,78 @@ void GraphWidget::draw() fl_line_style(0); for (size_t i = 0U; i < count_; ++i) { xtc_node_info node{}; - if (xtc_graph_get_node(graph_, i, &node) != XTC_OK) continue; + if (xtc_graph_get_node(graph_, i, &node) != XTC_OK) + continue; int left = x() + positions_[i].left, top = y() + positions_[i].top; - fl_color(fl_rgb_color(49, 54, 62)); fl_rectf(left, top, node_width, node_height); + fl_color(fl_rgb_color(49, 54, 62)); + fl_rectf(left, top, node_width, node_height); fl_color(selected_node_ == node.id ? FL_YELLOW : fl_rgb_color(115, 130, 145)); fl_rect(left, top, node_width, node_height); - fl_color(FL_WHITE); fl_font(FL_HELVETICA_BOLD, 12); + fl_color(FL_WHITE); + fl_font(FL_HELVETICA_BOLD, 12); char text[96]; std::snprintf(text, sizeof(text), "%s #%zu", node_name(node.type), node.id); fl_draw(text, left + 10, top + 20); fl_font(FL_HELVETICA, 12); - if (node.type == XTC_NODE_OSCILLATOR) std::snprintf(text, sizeof(text), "%.0f Hz", static_cast(node.value)); - else if (node.type == XTC_NODE_GAIN) std::snprintf(text, sizeof(text), "x %.2f", static_cast(node.value)); - else std::snprintf(text, sizeof(text), "%s", node.type == XTC_NODE_METER ? "Peak pass-through" : node.type == XTC_NODE_INSTRUMENT ? "MIDI sine source" : node.type == XTC_NODE_AUDIO ? "Timeline mix source" : "Mono audio output"); + if (node.type == XTC_NODE_OSCILLATOR) + std::snprintf(text, sizeof(text), "%.0f Hz", static_cast(node.value)); + else if (node.type == XTC_NODE_GAIN) + std::snprintf(text, sizeof(text), "x %.2f", static_cast(node.value)); + else + std::snprintf(text, sizeof(text), "%s", + node.type == XTC_NODE_METER ? "Peak pass-through" + : node.type == XTC_NODE_INSTRUMENT ? "MIDI sine source" + : node.type == XTC_NODE_AUDIO ? "Timeline mix source" + : "Mono audio output"); fl_draw(text, left + 10, top + 37); for (size_t port = 0U; port < 2U; ++port) { int px, py; - if (!port_position(node.id, port, px, py)) continue; - fl_color(fl_rgb_color(110, 220, 185)); fl_pie(x() + px - 5, y() + py - 5, 10, 10, 0, 360); - fl_color(FL_WHITE); fl_draw(port == 0U ? "in" : "out", left + (port == 0U ? 10 : 118), top + 62); + if (!port_position(node.id, port, px, py)) + continue; + fl_color(fl_rgb_color(110, 220, 185)); + fl_pie(x() + px - 5, y() + py - 5, 10, 10, 0, 360); + fl_color(FL_WHITE); + fl_draw(port == 0U ? "in" : "out", left + (port == 0U ? 10 : 118), top + 62); } } if (wiring_) { int px, py; if (port_position(wire_node_, wire_port_, px, py)) { - fl_color(FL_YELLOW); fl_line(x() + px, y() + py, x() + mouse_x_, y() + mouse_y_); + fl_color(FL_YELLOW); + fl_line(x() + px, y() + py, x() + mouse_x_, y() + mouse_y_); } } fl_pop_clip(); } + int GraphWidget::handle(int event) { const int left = Fl::event_x() - x(), top = Fl::event_y() - y(); switch (event) { - case FL_FOCUS: case FL_UNFOCUS: return 1; + case FL_FOCUS: + case FL_UNFOCUS: + return 1; case FL_PUSH: { - if (Fl::event_button() != FL_LEFT_MOUSE) return 0; - take_focus(); mouse_x_ = left; mouse_y_ = top; - dragging_ = false; wiring_ = false; - if (hit_port(left, top, wire_node_, wire_port_)) { wiring_ = true; redraw(); return 1; } + if (Fl::event_button() != FL_LEFT_MOUSE) + return 0; + take_focus(); + mouse_x_ = left; + mouse_y_ = top; + dragging_ = false; + wiring_ = false; + if (hit_port(left, top, wire_node_, wire_port_)) { + wiring_ = true; + redraw(); + return 1; + } for (size_t i = count_; i > 0U; --i) { const Position &pos = positions_[i - 1U]; if (left >= pos.left && left <= pos.left + node_width && top >= pos.top && top <= pos.top + node_height) { - select_node(pos.id); dragging_ = true; drag_x_ = left - pos.left; drag_y_ = top - pos.top; return 1; + select_node(pos.id); + dragging_ = true; + drag_x_ = left - pos.left; + drag_y_ = top - pos.top; + return 1; } } size_t nodes, edges; @@ -228,36 +322,55 @@ int GraphWidget::handle(int event) xtc_connection_info edge{}; int sx, sy, dx, dy; (void)xtc_graph_get_connection(graph_, i, &edge); - if (!port_position(edge.source_node, 1U, sx, sy) || !port_position(edge.destination_node, 0U, dx, dy)) continue; + if (!port_position(edge.source_node, 1U, sx, sy) || !port_position(edge.destination_node, 0U, dx, dy)) + continue; double vx = dx - sx, vy = dy - sy; double length = vx * vx + vy * vy; - double t = length == 0.0 ? 0.0 : std::max(0.0, std::min(1.0, ((left - sx) * vx + (top - sy) * vy) / length)); + double t = + length == 0.0 ? 0.0 : std::max(0.0, std::min(1.0, ((left - sx) * vx + (top - sy) * vy) / length)); if (std::hypot(left - sx - t * vx, top - sy - t * vy) <= 6.0) { - select_connection(edge.source_node, edge.destination_node); return 1; + select_connection(edge.source_node, edge.destination_node); + return 1; } } - selected_node_ = SIZE_MAX; edge_selected_ = false; redraw(); return 1; + selected_node_ = SIZE_MAX; + edge_selected_ = false; + redraw(); + return 1; } case FL_DRAG: - mouse_x_ = left; mouse_y_ = top; - if (dragging_) move_node(selected_node_, left - drag_x_, top - drag_y_); - if (wiring_) redraw(); + mouse_x_ = left; + mouse_y_ = top; + if (dragging_) + move_node(selected_node_, left - drag_x_, top - drag_y_); + if (wiring_) + redraw(); return 1; case FL_RELEASE: dragging_ = false; if (wiring_) { size_t node, port; wiring_ = false; - if (hit_port(left, top, node, port)) connect_ports({wire_node_, wire_port_, node, port}); - else feedback_->copy_label("Connection cancelled. Drag an output port to an input port."); + if (hit_port(left, top, node, port)) + connect_ports({wire_node_, wire_port_, node, port}); + else + feedback_->copy_label("Connection cancelled. Drag an output port to an input port."); redraw(); } return 1; case FL_KEYDOWN: - if (Fl::event_key() == FL_Delete || Fl::event_key() == FL_BackSpace) { delete_selected(); return 1; } - if (Fl::event_key() == FL_Escape) { wiring_ = dragging_ = false; redraw(); return 1; } + if (Fl::event_key() == FL_Delete || Fl::event_key() == FL_BackSpace) { + delete_selected(); + return 1; + } + if (Fl::event_key() == FL_Escape) { + wiring_ = dragging_ = false; + redraw(); + return 1; + } + break; + default: break; - default: break; } return Fl_Widget::handle(event); } diff --git a/src/gui/graph_widget.hpp b/src/gui/graph_widget.hpp index 84346eb..e40383e 100644 --- a/src/gui/graph_widget.hpp +++ b/src/gui/graph_widget.hpp @@ -3,17 +3,30 @@ #include #include #include "xtc/xtc_engine.h" + class Fl_Box; /* GUI positions/selection only. Domain snapshots and edits stay in the C API. */ class GraphWidget : public Fl_Widget { -public: + public: using EditHandler = xtc_result (*)(void *, const xtc_graph_edit &, size_t *); - GraphWidget(int x, int y, int width, int height, const xtc_graph *graph, - EditHandler handler, void *context, Fl_Box *feedback); - void reset_layout() { count_ = 0; selected_node_ = SIZE_MAX; edge_selected_ = dragging_ = wiring_ = false; } + GraphWidget(int x, int y, int width, int height, const xtc_graph *graph, EditHandler handler, void *context, + Fl_Box *feedback); + + void reset_layout() + { + count_ = 0; + selected_node_ = SIZE_MAX; + edge_selected_ = dragging_ = wiring_ = false; + } + void set_graph(const xtc_graph *graph); - const xtc_graph *graph() const { return graph_; } // Borrowed, GUI thread only. + + const xtc_graph *graph() const + { + return graph_; + } // Borrowed, GUI thread only. + xtc_result add_node(xtc_node_type type); xtc_result connect_ports(const xtc_connection_info &connection); xtc_result delete_selected(); @@ -22,10 +35,16 @@ public: bool move_node(size_t id, int left, int top); bool node_position(size_t id, int &left, int &top) const; int handle(int event) override; -protected: + + protected: void draw() override; -private: - struct Position { size_t id = SIZE_MAX; int left = 0, top = 0; }; + + private: + struct Position { + size_t id = SIZE_MAX; + int left = 0, top = 0; + }; + std::array positions_{}; size_t count_ = 0U; const xtc_graph *graph_; diff --git a/src/gui/main.cpp b/src/gui/main.cpp index a8c9f2a..7a987ad 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -9,13 +9,20 @@ int main(int argc, char **argv) { if (argc == 2 && !std::strcmp(argv[1], "--version")) { - std::printf("%s %s\n", XTC_PROJECT_NAME, XTC_VERSION_STRING); return EXIT_SUCCESS; + std::printf("%s %s\n", XTC_PROJECT_NAME, XTC_VERSION_STRING); + return EXIT_SUCCESS; } if (argc == 2 && !std::strcmp(argv[1], "--help")) { - std::puts("Usage: xtc [PROJECT] or xtc [FLTK options]\n--version and --help work without a display or audio server."); return EXIT_SUCCESS; + std::puts( + "Usage: xtc [PROJECT] or xtc [FLTK options]\n--version and --help work without a display or audio server."); + return EXIT_SUCCESS; } const char *project_path = nullptr; - if (argc == 2 && argv[1][0] != '-') { project_path = argv[1]; argc = 1; argv[1] = nullptr; } + if (argc == 2 && argv[1][0] != '-') { + project_path = argv[1]; + argc = 1; + argv[1] = nullptr; + } try { Fl::scheme("flat"); Fl::background(34, 36, 40); @@ -24,7 +31,10 @@ int main(int argc, char **argv) MainWindow window; if (project_path) { xtc_result result = window.open_project(project_path); - if (result != XTC_OK) { std::fprintf(stderr, "Cannot open project: %s\n", xtc_result_string(result)); return EXIT_FAILURE; } + if (result != XTC_OK) { + std::fprintf(stderr, "Cannot open project: %s\n", xtc_result_string(result)); + return EXIT_FAILURE; + } } window.show(argc, argv); return Fl::run(); diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index b718ed7..ea52eed 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -24,19 +24,20 @@ void set_label(Fl_Box *box, const char *text) if (box->label() == nullptr || std::strcmp(box->label(), text) != 0) box->copy_label(text); } -} +} // namespace MainWindow::MainWindow(bool prompt_unsaved) - : Fl_Double_Window(1000, 680, XTC_PROJECT_NAME " — modular audio workstation"), - prompt_unsaved_(prompt_unsaved) + : Fl_Double_Window(1000, 680, XTC_PROJECT_NAME " — modular audio workstation"), prompt_unsaved_(prompt_unsaved) { xtc_project *initial = nullptr; xtc_result result = xtc_project_create(&initial); - if (result != XTC_OK) throw std::runtime_error(xtc_result_string(result)); + if (result != XTC_OK) + throw std::runtime_error(xtc_result_string(result)); project_.reset(initial); xtc_history *history = nullptr; result = xtc_history_create(initial, &history); - if (result != XTC_OK) throw std::runtime_error(xtc_result_string(result)); + if (result != XTC_OK) + throw std::runtime_error(xtc_result_string(result)); history_.reset(history); begin(); auto *menu = new Fl_Menu_Bar(0, 0, 1000, 28); @@ -76,7 +77,8 @@ MainWindow::MainWindow(bool prompt_unsaved) indicator_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); indicator_->labelfont(FL_BOLD); auto *hint = new Fl_Box(12, 78, 976, 24, - "Edit signal flow in Modular Graph. No hardware ports are connected automatically; Import WAV clips or record JACK input; connect external ports explicitly."); + "Edit signal flow in Modular Graph. No hardware ports are connected automatically; Import " + "WAV clips or record JACK input; connect external ports explicitly."); hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); hint->labelsize(12); @@ -92,26 +94,60 @@ MainWindow::MainWindow(bool prompt_unsaved) auto *add_midi = new Fl_Button(560, 150, 110, 26, "Add MIDI clip"); auto *piano = new Fl_Button(678, 150, 100, 26, "Piano roll"); piano->callback(piano_callback, this); - auto *timeline_hint = new Fl_Box(20, 548, 956, 28, - "Import WAV (Ctrl+I) or Record. Drag body to move, edges to trim; ruler seeks while stopped."); - timeline_hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); timeline_hint->labelsize(12); + auto *timeline_hint = + new Fl_Box(20, 548, 956, 28, + "Import WAV (Ctrl+I) or Record. Drag body to move, edges to trim; ruler seeks while stopped."); + timeline_hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); + timeline_hint->labelsize(12); auto *timeline_scroll = new Fl_Scroll(20, 188, 956, 350); sequencer_ = new SequencerWidget(20, 188, project_.get(), seek, this); timeline_scroll->end(); sequencer_->before_edit = before_arrangement_edit; - sequencer_->callback([](Fl_Widget *, void *c) { static_cast(c)->mark_dirty(); }, this); - add_track->callback([](Fl_Widget *, void *c) { static_cast(c)->add_track(); }, sequencer_); - add_midi->callback([](Fl_Widget *, void *c) { static_cast(c)->add_midi_clip(); }, sequencer_); - add_region->callback([](Fl_Widget *, void *c) { static_cast(c)->add_region(); }, sequencer_); - delete_region->callback([](Fl_Widget *, void *c) { static_cast(c)->delete_selection(); }, sequencer_); - zoom_in->callback([](Fl_Widget *, void *c) { static_cast(c)->zoom(1); }, sequencer_); - zoom_out->callback([](Fl_Widget *, void *c) { static_cast(c)->zoom(-1); }, sequencer_); + sequencer_->callback( + [](Fl_Widget *, void *c) { + static_cast(c)->mark_dirty(); + }, + this); + add_track->callback( + [](Fl_Widget *, void *c) { + static_cast(c)->add_track(); + }, + sequencer_); + add_midi->callback( + [](Fl_Widget *, void *c) { + static_cast(c)->add_midi_clip(); + }, + sequencer_); + add_region->callback( + [](Fl_Widget *, void *c) { + static_cast(c)->add_region(); + }, + sequencer_); + delete_region->callback( + [](Fl_Widget *, void *c) { + static_cast(c)->delete_selection(); + }, + sequencer_); + zoom_in->callback( + [](Fl_Widget *, void *c) { + static_cast(c)->zoom(1); + }, + sequencer_); + zoom_out->callback( + [](Fl_Widget *, void *c) { + static_cast(c)->zoom(-1); + }, + sequencer_); sequencer->resizable(timeline_scroll); sequencer->end(); auto *mixer = new Fl_Group(8, 140, 984, 444, "Mixer"); mixer->begin(); mixer_ = new MixerWidget(20, 150, 956, 420, project_.get(), before_arrangement_edit, this); - mixer_->callback([](Fl_Widget *, void *c) { static_cast(c)->mark_dirty(); }, this); + mixer_->callback( + [](Fl_Widget *, void *c) { + static_cast(c)->mark_dirty(); + }, + this); mixer->resizable(mixer_); mixer->end(); auto *graph = new Fl_Group(8, 140, 984, 444, "Modular Graph"); @@ -119,39 +155,56 @@ MainWindow::MainWindow(bool prompt_unsaved) graph_state_ = new Fl_Box(20, 182, 956, 24, "Graph: offline"); graph_state_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); graph_state_->labelsize(12); - auto *feedback = new Fl_Box(20, 548, 956, 28, - "Add nodes. Drag output to input to connect; click a node or wire then Delete. Scroll to pan."); + auto *feedback = + new Fl_Box(20, 548, 956, 28, + "Add nodes. Drag output to input to connect; click a node or wire then Delete. Scroll to pan."); feedback->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP); feedback->labelsize(12); auto *scroll = new Fl_Scroll(20, 212, 956, 328); graph_widget_ = new GraphWidget(20, 212, 2000, 4000, xtc_project_graph(project_.get()), edit_graph, this, feedback); scroll->end(); auto *oscillator = new Fl_Button(20, 150, 130, 26, "Add oscillator"); - oscillator->callback([](Fl_Widget *, void *context) { - (void)static_cast(context)->add_node(XTC_NODE_OSCILLATOR); - }, graph_widget_); + oscillator->callback( + [](Fl_Widget *, void *context) { + (void)static_cast(context)->add_node(XTC_NODE_OSCILLATOR); + }, + graph_widget_); auto *gain = new Fl_Button(158, 150, 110, 26, "Add gain"); - gain->callback([](Fl_Widget *, void *context) { - (void)static_cast(context)->add_node(XTC_NODE_GAIN); - }, graph_widget_); + gain->callback( + [](Fl_Widget *, void *context) { + (void)static_cast(context)->add_node(XTC_NODE_GAIN); + }, + graph_widget_); auto *meter = new Fl_Button(276, 150, 110, 26, "Add meter"); - meter->callback([](Fl_Widget *, void *context) { - (void)static_cast(context)->add_node(XTC_NODE_METER); - }, graph_widget_); + meter->callback( + [](Fl_Widget *, void *context) { + (void)static_cast(context)->add_node(XTC_NODE_METER); + }, + graph_widget_); auto *output = new Fl_Button(394, 150, 110, 26, "Add output"); - output->callback([](Fl_Widget *, void *context) { - (void)static_cast(context)->add_node(XTC_NODE_OUTPUT); - }, graph_widget_); + output->callback( + [](Fl_Widget *, void *context) { + (void)static_cast(context)->add_node(XTC_NODE_OUTPUT); + }, + graph_widget_); auto *audio = new Fl_Button(650, 150, 130, 26, "Add audio source"); - audio->callback([](Fl_Widget *, void *context) { - (void)static_cast(context)->add_node(XTC_NODE_AUDIO); - }, graph_widget_); + audio->callback( + [](Fl_Widget *, void *context) { + (void)static_cast(context)->add_node(XTC_NODE_AUDIO); + }, + graph_widget_); auto *instrument = new Fl_Button(788, 150, 130, 26, "Add instrument"); - instrument->callback([](Fl_Widget *, void *c) { (void)static_cast(c)->add_node(XTC_NODE_INSTRUMENT); }, graph_widget_); + instrument->callback( + [](Fl_Widget *, void *c) { + (void)static_cast(c)->add_node(XTC_NODE_INSTRUMENT); + }, + graph_widget_); auto *remove = new Fl_Button(512, 150, 130, 26, "Delete selection"); - remove->callback([](Fl_Widget *, void *context) { - (void)static_cast(context)->delete_selected(); - }, graph_widget_); + remove->callback( + [](Fl_Widget *, void *context) { + (void)static_cast(context)->delete_selected(); + }, + graph_widget_); graph->resizable(scroll); graph->end(); tabs_->end(); @@ -183,7 +236,8 @@ MainWindow::~MainWindow() // covers exceptional event-loop exit. A failed close must retain its C // callback context until process exit, rather than free active audio state. if (backend_ != nullptr && xtc_backend_close(backend_) != XTC_OK) - std::fputs("Failed to close JACK during GUI destruction; backend and recording retained until process exit.\n", stderr); + std::fputs("Failed to close JACK during GUI destruction; backend and recording retained until process exit.\n", + stderr); else if (recorder_) { (void)xtc_recorder_finish(recorder_); xtc_recorder_destroy(recorder_); @@ -193,9 +247,10 @@ MainWindow::~MainWindow() void MainWindow::report_error(const char *operation, xtc_result result) { char text[384]; - const char *hint = std::strstr(operation, "Project") != nullptr ? "Check the project path, file format and write permissions." : - result == XTC_ERR_UNSUPPORTED ? "Rebuild with JACK development support." : - "Check the JACK-compatible server and use Reconnect to retry."; + const char *hint = std::strstr(operation, "Project") != nullptr + ? "Check the project path, file format and write permissions." + : result == XTC_ERR_UNSUPPORTED ? "Rebuild with JACK development support." + : "Check the JACK-compatible server and use Reconnect to retry."; std::snprintf(text, sizeof(text), "%s: %s. %s", operation, xtc_result_string(result), hint); set_label(error_, text); } @@ -203,23 +258,30 @@ void MainWindow::report_error(const char *operation, xtc_result result) xtc_result MainWindow::edit_graph(void *context, const xtc_graph_edit &edit, size_t *out_added_id) { auto *window = static_cast(context); - if (window->recorder_) return XTC_ERR_INVALID_STATE; + if (window->recorder_) + return XTC_ERR_INVALID_STATE; xtc_graph *candidate = nullptr; size_t added_id = SIZE_MAX; xtc_result result = xtc_graph_apply_edit(xtc_project_graph(window->project_.get()), &edit, &candidate, &added_id); - if (result != XTC_OK) return result; + if (result != XTC_OK) + return result; xtc_project *replacement = nullptr; result = xtc_project_clone(window->project_.get(), &replacement); - if (result == XTC_OK) result = xtc_project_set_graph(replacement, candidate); + if (result == XTC_OK) + result = xtc_project_set_graph(replacement, candidate); xtc_graph_destroy(candidate); if (result == XTC_OK && window->ready_) result = xtc_backend_submit(window->backend_, xtc_project_graph(replacement)); - if (result != XTC_OK) { xtc_project_destroy(replacement); return result; } + if (result != XTC_OK) { + xtc_project_destroy(replacement); + return result; + } window->project_.reset(replacement); window->graph_widget_->set_graph(xtc_project_graph(replacement)); window->sequencer_->set_project(replacement, false); window->mark_dirty(); - if (out_added_id != nullptr && edit.kind == XTC_EDIT_ADD_NODE) *out_added_id = added_id; + if (out_added_id != nullptr && edit.kind == XTC_EDIT_ADD_NODE) + *out_added_id = added_id; window->refresh_status(); return XTC_OK; } @@ -230,13 +292,15 @@ void MainWindow::open_backend() xtc_result result = xtc_backend_open(&config, &backend_); ready_ = result == XTC_OK; running_ = false; - if (result != XTC_OK) report_error("Open JACK failed", result); + if (result != XTC_OK) + report_error("Open JACK failed", result); else { result = xtc_backend_submit(backend_, xtc_project_graph(project_.get())); if (result != XTC_OK) { ready_ = false; // Handle is retained for close/reconnect; graph was not installed. report_error("Submit graph failed", result); - } else set_label(error_, ""); + } else + set_label(error_, ""); } refresh_status(); } @@ -247,15 +311,20 @@ void MainWindow::start_engine() report_error("Audio is disconnected; use Reconnect", XTC_ERR_INVALID_STATE); return; } - if (running_) return; + if (running_) + return; xtc_result result = xtc_backend_prepare_project(backend_, project_.get()); - if (result == XTC_OK) result = xtc_backend_start(backend_); - if (result != XTC_OK) report_error("Start engine failed", result); + if (result == XTC_OK) + result = xtc_backend_start(backend_); + if (result != XTC_OK) + report_error("Start engine failed", result); else { set_label(error_, ""); const char *issue = xtc_backend_asset_issue(backend_); if (issue[0]) { - char message[640]; std::snprintf(message, sizeof(message), "Missing/unreadable audio (silent): %s. Select clip and Relink to repair.", issue); + char message[640]; + std::snprintf(message, sizeof(message), + "Missing/unreadable audio (silent): %s. Select clip and Relink to repair.", issue); set_label(error_, message); } } @@ -264,19 +333,25 @@ void MainWindow::start_engine() void MainWindow::stop_engine() { - if (!ready_) return; + if (!ready_) + return; xtc_result result = xtc_backend_stop(backend_); - if (result != XTC_OK) report_error("Stop engine failed", result); + if (result != XTC_OK) + report_error("Stop engine failed", result); else { set_label(error_, ""); - if (recorder_) finish_recording(); + if (recorder_) + finish_recording(); } refresh_status(); } void MainWindow::reconnect_engine() { - if (recorder_) { report_error("Stop recording before reconnecting", XTC_ERR_INVALID_STATE); return; } + if (recorder_) { + report_error("Stop recording before reconnecting", XTC_ERR_INVALID_STATE); + return; + } xtc_result result = xtc_backend_close(backend_); if (result != XTC_OK) { report_error("Reconnect could not close JACK", result); @@ -290,7 +365,8 @@ void MainWindow::reconnect_engine() void MainWindow::refresh_status() { - if (mixer_) mixer_->peaks(0, 0); + if (mixer_) + mixer_->peaks(0, 0); bool can_play = false, can_record = false; if (!ready_) { set_label(indicator_, "Audio: disconnected"); @@ -314,36 +390,56 @@ void MainWindow::refresh_status() const bool fault = status.server_lost || status.sample_rate_changed || status.process_result != XTC_OK; can_play = !running_ && !fault; can_record = !fault && recorder_ == nullptr; - const char *state = status.server_lost ? "Audio: server lost — Reconnect required" : - status.sample_rate_changed ? "Audio: sample rate changed — Reconnect required" : - status.process_result != XTC_OK ? "Audio: processing error" : - recorder_ ? "Audio: recording JACK input — Stop to finish" : running_ ? "Audio: running — modular graph" : "Audio: ready / stopped"; + const char *state = status.server_lost ? "Audio: server lost — Reconnect required" + : status.sample_rate_changed ? "Audio: sample rate changed — Reconnect required" + : status.process_result != XTC_OK ? "Audio: processing error" + : recorder_ ? "Audio: recording JACK input — Stop to finish" + : running_ ? "Audio: running — modular graph" + : "Audio: ready / stopped"; set_label(indicator_, state); - indicator_->labelcolor(fault ? fl_rgb_color(255, 155, 120) : - running_ ? fl_rgb_color(125, 225, 160) : fl_rgb_color(215, 215, 215)); + indicator_->labelcolor(fault ? fl_rgb_color(255, 155, 120) + : running_ ? fl_rgb_color(125, 225, 160) + : fl_rgb_color(215, 215, 215)); mixer_->peaks(status.peak_left, status.peak_right); char text[256]; - std::snprintf(text, sizeof(text), "JACK | %u Hz | %u frames | stereo | %u blocks | %u xruns | %s | %u asset issues | %u MIDI drops", + std::snprintf( + text, sizeof(text), + "JACK | %u Hz | %u frames | stereo | %u blocks | %u xruns | %s | %u asset issues | %u MIDI drops", status.sample_rate, status.buffer_frames, status.processed_blocks, status.xruns, xtc_result_string(status.process_result), status.asset_issues, status.midi_dropped); set_label(status_, text); - if (fault) set_label(graph_state_, "Graph: engine unavailable — Reconnect required"); + if (fault) + set_label(graph_state_, "Graph: engine unavailable — Reconnect required"); else if (status.submitted_revision != status.active_revision) - set_label(graph_state_, running_ ? "Graph: queued — engine still processing previous graph" : - "Graph: prepared — starts on Play"); - else set_label(graph_state_, running_ ? "Graph: active in engine" : "Graph: stopped"); + set_label(graph_state_, running_ ? "Graph: queued — engine still processing previous graph" + : "Graph: prepared — starts on Play"); + else + set_label(graph_state_, running_ ? "Graph: active in engine" : "Graph: stopped"); } xtc_backend_collect(backend_); } - if (can_play) play_->activate(); else play_->deactivate(); - if (can_record) record_->activate(); else record_->deactivate(); - if (ready_ && (running_ || recorder_)) stop_->activate(); else stop_->deactivate(); + if (can_play) + play_->activate(); + else + play_->deactivate(); + if (can_record) + record_->activate(); + else + record_->deactivate(); + if (ready_ && (running_ || recorder_)) + stop_->activate(); + else + stop_->deactivate(); } bool MainWindow::close_window() { - if (recorder_) { report_error("Stop recording before closing", XTC_ERR_INVALID_STATE); return false; } - if (!discard_changes()) return false; + if (recorder_) { + report_error("Stop recording before closing", XTC_ERR_INVALID_STATE); + return false; + } + if (!discard_changes()) + return false; xtc_result result = xtc_backend_close(backend_); if (result != XTC_OK) { report_error("Close JACK failed; window kept open for retry", result); @@ -354,39 +450,57 @@ bool MainWindow::close_window() running_ = false; closing_ = true; Fl::remove_timeout(timer_callback, this); - if (piano_window_) piano_window_->hide(); + if (piano_window_) + piano_window_->hide(); hide(); return true; } int MainWindow::handle(int event) { - if (event == FL_SHORTCUT && Fl::event_key() == ' ' && - (Fl::event_state() & (FL_CTRL | FL_ALT | FL_META)) == 0) { - if (running_) stop_engine(); else start_engine(); + if (event == FL_SHORTCUT && Fl::event_key() == ' ' && (Fl::event_state() & (FL_CTRL | FL_ALT | FL_META)) == 0) { + if (running_) + stop_engine(); + else + start_engine(); return 1; } return Fl_Double_Window::handle(event); } + void MainWindow::play_callback(Fl_Widget *, void *context) -{ static_cast(context)->start_engine(); } +{ + static_cast(context)->start_engine(); +} + void MainWindow::stop_callback(Fl_Widget *, void *context) -{ static_cast(context)->stop_engine(); } +{ + static_cast(context)->stop_engine(); +} + void MainWindow::reconnect_callback(Fl_Widget *, void *context) -{ static_cast(context)->reconnect_engine(); } +{ + static_cast(context)->reconnect_engine(); +} + void MainWindow::close_callback(Fl_Widget *, void *context) -{ (void)static_cast(context)->close_window(); } +{ + (void)static_cast(context)->close_window(); +} + void MainWindow::view_callback(Fl_Widget *widget, void *view) { auto *window = static_cast(widget->window()); window->tabs_->value(static_cast(view)); window->tabs_->redraw(); } + 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); + XTC_PROJECT_NAME, XTC_VERSION_STRING); } + void MainWindow::timer_callback(void *context) { auto *window = static_cast(context); @@ -398,116 +512,166 @@ void MainWindow::timer_callback(void *context) window->stop_engine(); } window->refresh_status(); - if (!window->closing_) Fl::repeat_timeout(0.1, timer_callback, context); + if (!window->closing_) + Fl::repeat_timeout(0.1, timer_callback, context); } - void MainWindow::mark_dirty() { xtc_result history_result = xtc_history_record(history_.get(), project_.get()); - if (history_result != XTC_OK) report_error("Undo history could not retain edit", history_result); + if (history_result != XTC_OK) + report_error("Undo history could not retain edit", history_result); mixer_->set_project(project_.get()); graph_widget_->set_graph(xtc_project_graph(project_.get())); - if (piano_) piano_->set_project(project_.get(), piano_->clip()); - dirty_ = true; copy_label("xtc * — unsaved changes"); + if (piano_) + piano_->set_project(project_.get(), piano_->clip()); + dirty_ = true; + copy_label("xtc * — unsaved changes"); } + bool MainWindow::discard_changes() { return !dirty_ || !prompt_unsaved_ || - fl_choice("Discard unsaved project changes?", "Cancel", "Discard", nullptr) == 1; + fl_choice("Discard unsaved project changes?", "Cancel", "Discard", nullptr) == 1; } + xtc_result MainWindow::seek(void *context, uint64_t tick) { auto *window = static_cast(context); - if (!window->ready_) return XTC_OK; + if (!window->ready_) + return XTC_OK; xtc_backend_status status{}; xtc_result r = xtc_backend_get_status(window->backend_, &status); uint64_t sample = 0; - if (r == XTC_OK) r = xtc_project_tick_to_sample(window->project_.get(), tick, status.sample_rate, &sample); + if (r == XTC_OK) + r = xtc_project_tick_to_sample(window->project_.get(), tick, status.sample_rate, &sample); return r == XTC_OK ? xtc_backend_seek(window->backend_, sample) : r; } + xtc_result MainWindow::save_project(const char *path) { - if (recorder_) return XTC_ERR_INVALID_STATE; - if (path == nullptr) return XTC_ERR_INVALID_ARGUMENT; + if (recorder_) + return XTC_ERR_INVALID_STATE; + if (path == nullptr) + return XTC_ERR_INVALID_ARGUMENT; std::string saved_path(path); xtc_result r = xtc_project_save(project_.get(), path); - if (r == XTC_OK) { path_ = saved_path; dirty_ = false; copy_label(path_.c_str()); } + if (r == XTC_OK) { + path_ = saved_path; + dirty_ = false; + copy_label(path_.c_str()); + } return r; } + xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_history) { - if (recorder_) return XTC_ERR_INVALID_STATE; + if (recorder_) + return XTC_ERR_INVALID_STATE; std::unique_ptr new_history(nullptr, xtc_history_destroy); if (reset_history) { xtc_history *h = nullptr; xtc_result result = xtc_history_create(candidate, &h); - if (result != XTC_OK) return result; + if (result != XTC_OK) + return result; new_history.reset(h); } xtc_result r = XTC_OK; if (ready_) { r = xtc_backend_stop(backend_); - if (r == XTC_OK) r = xtc_backend_seek(backend_, 0); - if (r == XTC_OK) r = xtc_backend_prepare_project(backend_, candidate); + if (r == XTC_OK) + r = xtc_backend_seek(backend_, 0); + if (r == XTC_OK) + r = xtc_backend_prepare_project(backend_, candidate); } xtc_audio_plan *offline = nullptr; - if (r == XTC_OK && !ready_) r = xtc_audio_plan_create(candidate, 48000, &offline); + if (r == XTC_OK && !ready_) + r = xtc_audio_plan_create(candidate, 48000, &offline); xtc_midi_plan *offline_midi = nullptr; - if (r == XTC_OK && !ready_) r = xtc_midi_plan_create(candidate, 48000, &offline_midi); + if (r == XTC_OK && !ready_) + r = xtc_midi_plan_create(candidate, 48000, &offline_midi); xtc_midi_plan_destroy(offline_midi); - if (r != XTC_OK) { xtc_audio_plan_destroy(offline); return r; } + if (r != XTC_OK) { + xtc_audio_plan_destroy(offline); + return r; + } std::string issue = ready_ ? xtc_backend_asset_issue(backend_) : ""; if (offline) { const char *path = ""; - xtc_audio_plan_issues(offline, &path); issue = path; + xtc_audio_plan_issues(offline, &path); + issue = path; xtc_audio_plan_destroy(offline); } - if (piano_window_) piano_window_->hide(); - if (piano_) piano_->set_project(candidate, UINT64_MAX); + if (piano_window_) + piano_window_->hide(); + if (piano_) + piano_->set_project(candidate, UINT64_MAX); project_.reset(candidate); mixer_->set_project(candidate); - if (reset_history) history_.swap(new_history); + if (reset_history) + history_.swap(new_history); graph_widget_->reset_layout(); graph_widget_->set_graph(xtc_project_graph(candidate)); sequencer_->set_project(candidate); dirty_ = false; if (!issue.empty()) { - char text[640]; std::snprintf(text, sizeof(text), "Missing/unreadable audio (silent): %s. Select clip and Relink to repair.", issue.c_str()); + char text[640]; + std::snprintf(text, sizeof(text), "Missing/unreadable audio (silent): %s. Select clip and Relink to repair.", + issue.c_str()); set_label(error_, text); - } else set_label(error_, ""); + } else + set_label(error_, ""); refresh_status(); return XTC_OK; } + xtc_result MainWindow::open_project(const char *path) { - if (path == nullptr) return XTC_ERR_INVALID_ARGUMENT; + if (path == nullptr) + return XTC_ERR_INVALID_ARGUMENT; std::string loaded_path(path); xtc_project *candidate = nullptr; xtc_result r = xtc_project_create(&candidate); - if (r == XTC_OK) r = xtc_project_load(candidate, path); - if (r == XTC_OK) r = install_project(candidate); - if (r != XTC_OK) xtc_project_destroy(candidate); - else { path_ = loaded_path; copy_label(path_.c_str()); } + if (r == XTC_OK) + r = xtc_project_load(candidate, path); + if (r == XTC_OK) + r = install_project(candidate); + if (r != XTC_OK) + xtc_project_destroy(candidate); + else { + path_ = loaded_path; + copy_label(path_.c_str()); + } return r; } + void MainWindow::file_callback(Fl_Widget *widget, void *context) { auto *window = static_cast(context); - if (window->recorder_) { window->report_error("Stop recording before file operations", XTC_ERR_INVALID_STATE); return; } + if (window->recorder_) { + window->report_error("Stop recording before file operations", XTC_ERR_INVALID_STATE); + return; + } auto *menu = static_cast(widget); const char *action = menu->mvalue()->label(); xtc_result r = XTC_OK; if (std::strcmp(action, "New") == 0) { - if (!window->discard_changes()) return; + if (!window->discard_changes()) + return; xtc_project *candidate = nullptr; r = xtc_project_create(&candidate); - if (r == XTC_OK) r = window->install_project(candidate); - if (r != XTC_OK) xtc_project_destroy(candidate); - else { window->path_.clear(); window->copy_label("xtc — Untitled"); } + if (r == XTC_OK) + r = window->install_project(candidate); + if (r != XTC_OK) + xtc_project_destroy(candidate); + else { + window->path_.clear(); + window->copy_label("xtc — Untitled"); + } } else { bool open = std::strcmp(action, "Open") == 0; - if (open && !window->discard_changes()) return; + if (open && !window->discard_changes()) + return; if (!open && std::strcmp(action, "Save") == 0 && !window->path_.empty()) r = window->save_project(window->path_.c_str()); else { @@ -515,62 +679,90 @@ void MainWindow::file_callback(Fl_Widget *widget, void *context) chooser.title(open ? "Open project" : "Save project"); chooser.type(open ? Fl_Native_File_Chooser::BROWSE_FILE : Fl_Native_File_Chooser::BROWSE_SAVE_FILE); chooser.filter("xtc projects *.xtc"); - if (!open) chooser.options(Fl_Native_File_Chooser::SAVEAS_CONFIRM); + if (!open) + chooser.options(Fl_Native_File_Chooser::SAVEAS_CONFIRM); int result = chooser.show(); - if (result == 1) return; - if (result < 0) r = XTC_ERR_IO; - else r = open ? window->open_project(chooser.filename()) : window->save_project(chooser.filename()); + if (result == 1) + return; + if (result < 0) + r = XTC_ERR_IO; + else + r = open ? window->open_project(chooser.filename()) : window->save_project(chooser.filename()); } } - if (r != XTC_OK) window->report_error("Project operation failed", r); + if (r != XTC_OK) + window->report_error("Project operation failed", r); } - xtc_result MainWindow::before_arrangement_edit(void *context) { auto *window = static_cast(context); - if (window->recorder_) return XTC_ERR_INVALID_STATE; - if (!window->ready_) return XTC_OK; + if (window->recorder_) + return XTC_ERR_INVALID_STATE; + if (!window->ready_) + return XTC_OK; xtc_result r = xtc_backend_stop(window->backend_); window->refresh_status(); return r; } + xtc_result MainWindow::import_audio(const char *path) { xtc_result r = before_arrangement_edit(this); - if (r != XTC_OK) return r; + if (r != XTC_OK) + return r; xtc_project *candidate = nullptr; r = xtc_project_clone(project_.get(), &candidate); uint64_t track = sequencer_->selected_track(), clip = 0; - if (r == XTC_OK && track == UINT64_MAX) r = xtc_project_add_track(candidate, "Audio", &track); - if (r == XTC_OK) r = xtc_project_import_audio(candidate, track, path, sequencer_->position(), &clip); - if (r == XTC_OK && ready_) r = xtc_backend_prepare_project(backend_, candidate); - if (r != XTC_OK) { xtc_project_destroy(candidate); return r; } + if (r == XTC_OK && track == UINT64_MAX) + r = xtc_project_add_track(candidate, "Audio", &track); + if (r == XTC_OK) + r = xtc_project_import_audio(candidate, track, path, sequencer_->position(), &clip); + if (r == XTC_OK && ready_) + r = xtc_backend_prepare_project(backend_, candidate); + if (r != XTC_OK) { + xtc_project_destroy(candidate); + return r; + } project_.reset(candidate); sequencer_->set_project(candidate, false); graph_widget_->set_graph(xtc_project_graph(candidate)); - mark_dirty(); refresh_status(); return XTC_OK; + mark_dirty(); + refresh_status(); + return XTC_OK; } + xtc_result MainWindow::record_audio(const char *path) { - if (!ready_ || recorder_) return XTC_ERR_INVALID_STATE; + if (!ready_ || recorder_) + return XTC_ERR_INVALID_STATE; xtc_result r = xtc_backend_stop(backend_); xtc_backend_status status{}; - if (r == XTC_OK) r = xtc_backend_get_status(backend_, &status); - if (r == XTC_OK) r = xtc_project_sample_to_tick(project_.get(), status.sample_position, status.sample_rate, &recording_tick_); - if (r == XTC_OK) r = xtc_backend_prepare_project(backend_, project_.get()); - if (r == XTC_OK) r = xtc_recorder_create(path, status.sample_rate, static_cast(status.sample_rate) * 2, &recorder_); - if (r != XTC_OK) return r; + if (r == XTC_OK) + r = xtc_backend_get_status(backend_, &status); + if (r == XTC_OK) + r = xtc_project_sample_to_tick(project_.get(), status.sample_position, status.sample_rate, &recording_tick_); + if (r == XTC_OK) + r = xtc_backend_prepare_project(backend_, project_.get()); + if (r == XTC_OK) + r = xtc_recorder_create(path, status.sample_rate, static_cast(status.sample_rate) * 2, &recorder_); + if (r != XTC_OK) + return r; recording_path_ = path; recording_track_ = sequencer_->selected_track(); r = xtc_backend_set_recorder(backend_, recorder_); - if (r == XTC_OK) r = xtc_backend_start(backend_); + if (r == XTC_OK) + r = xtc_backend_start(backend_); if (r != XTC_OK) { (void)xtc_backend_set_recorder(backend_, nullptr); - xtc_recorder_destroy(recorder_); recorder_ = nullptr; - } else mark_dirty(); - refresh_status(); return r; + xtc_recorder_destroy(recorder_); + recorder_ = nullptr; + } else + mark_dirty(); + refresh_status(); + return r; } + void MainWindow::finish_recording() { // Called only after successful backend_stop: no producer can reference ring. @@ -578,28 +770,43 @@ void MainWindow::finish_recording() uint64_t frames = 0; (void)xtc_recorder_status(recorder_, &frames); xtc_result r = xtc_recorder_finish(recorder_); - xtc_recorder_destroy(recorder_); recorder_ = nullptr; + xtc_recorder_destroy(recorder_); + recorder_ = nullptr; if (r != XTC_OK) { - char text[768]; std::snprintf(text, sizeof(text), "Recording failed (%s). Partial take, if finalized: %s. It was not added to the project.", - xtc_result_string(r), recording_path_.c_str()); - set_label(error_, text); return; + char text[768]; + std::snprintf(text, sizeof(text), + "Recording failed (%s). Partial take, if finalized: %s. It was not added to the project.", + xtc_result_string(r), recording_path_.c_str()); + set_label(error_, text); + return; + } + if (!frames) { + set_label(error_, "Empty recording saved; no clip added."); + return; } - if (!frames) { set_label(error_, "Empty recording saved; no clip added."); return; } xtc_project *candidate = nullptr; r = xtc_project_clone(project_.get(), &candidate); uint64_t clip = 0, track = recording_track_; - if (r == XTC_OK && track == UINT64_MAX) r = xtc_project_add_track(candidate, "Recording", &track); - if (r == XTC_OK) r = xtc_project_import_audio(candidate, track, recording_path_.c_str(), recording_tick_, &clip); - if (r == XTC_OK) r = xtc_backend_prepare_project(backend_, candidate); + if (r == XTC_OK && track == UINT64_MAX) + r = xtc_project_add_track(candidate, "Recording", &track); + if (r == XTC_OK) + r = xtc_project_import_audio(candidate, track, recording_path_.c_str(), recording_tick_, &clip); + if (r == XTC_OK) + r = xtc_backend_prepare_project(backend_, candidate); if (r != XTC_OK) { xtc_project_destroy(candidate); - char text[768]; std::snprintf(text, sizeof(text), "Take saved to %s; adding clip failed: %s. Import the WAV to recover.", - recording_path_.c_str(), xtc_result_string(r)); - set_label(error_, text); return; + char text[768]; + std::snprintf(text, sizeof(text), "Take saved to %s; adding clip failed: %s. Import the WAV to recover.", + recording_path_.c_str(), xtc_result_string(r)); + set_label(error_, text); + return; } - project_.reset(candidate); sequencer_->set_project(candidate, false); - graph_widget_->set_graph(xtc_project_graph(candidate)); mark_dirty(); + project_.reset(candidate); + sequencer_->set_project(candidate, false); + graph_widget_->set_graph(xtc_project_graph(candidate)); + mark_dirty(); } + void MainWindow::audio_file_callback(Fl_Widget *widget, void *context) { auto *window = static_cast(context); @@ -607,24 +814,35 @@ void MainWindow::audio_file_callback(Fl_Widget *widget, void *context) const char *action = menu ? menu->mvalue()->label() : "Record WAV"; bool recording = std::strcmp(action, "Record WAV") == 0; bool relink = std::strcmp(action, "Relink selected audio") == 0; - if (window->recorder_) { window->report_error("Stop recording first", XTC_ERR_INVALID_STATE); return; } + if (window->recorder_) { + window->report_error("Stop recording first", XTC_ERR_INVALID_STATE); + return; + } Fl_Native_File_Chooser chooser; chooser.title(recording ? "Record JACK input to WAV" : relink ? "Relink selected clip" : "Import WAV"); chooser.type(recording ? Fl_Native_File_Chooser::BROWSE_SAVE_FILE : Fl_Native_File_Chooser::BROWSE_FILE); chooser.filter("WAV audio\t*.wav"); - if (recording) chooser.options(Fl_Native_File_Chooser::SAVEAS_CONFIRM); + if (recording) + chooser.options(Fl_Native_File_Chooser::SAVEAS_CONFIRM); int chosen = chooser.show(); - if (chosen == 1) return; + if (chosen == 1) + return; xtc_result r = chosen < 0 ? XTC_ERR_IO : XTC_OK; if (r == XTC_OK && relink) { r = before_arrangement_edit(window); - if (r == XTC_OK) r = xtc_project_set_clip_asset(window->project_.get(), window->sequencer_->selected_clip(), chooser.filename()); - if (r == XTC_OK) { window->mark_dirty(); window->sequencer_->redraw(); } - } else if (r == XTC_OK) r = recording ? window->record_audio(chooser.filename()) : window->import_audio(chooser.filename()); - if (r != XTC_OK) window->report_error("Project audio operation failed", r); + if (r == XTC_OK) + r = xtc_project_set_clip_asset(window->project_.get(), window->sequencer_->selected_clip(), + chooser.filename()); + if (r == XTC_OK) { + window->mark_dirty(); + window->sequencer_->redraw(); + } + } else if (r == XTC_OK) + r = recording ? window->record_audio(chooser.filename()) : window->import_audio(chooser.filename()); + if (r != XTC_OK) + window->report_error("Project audio operation failed", r); } - xtc_result MainWindow::open_piano_roll() { xtc_clip_info clip{}; @@ -634,47 +852,73 @@ xtc_result MainWindow::open_piano_roll() piano_window_ = std::make_unique(900, 620, "xtc — Piano roll"); piano_window_->begin(); auto *hint = new Fl_Box(12, 8, 876, 32, - "Source ticks: click draws; drag body moves/pitches, right edge resizes; right-click/Delete removes. Snap 1/16."); - hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); hint->labelsize(12); + "Source ticks: click draws; drag body moves/pitches, right edge resizes; " + "right-click/Delete removes. Snap 1/16."); + hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); + hint->labelsize(12); auto *scroll = new Fl_Scroll(12, 48, 876, 560); piano_ = new PianoRollWidget(12, 48, project_.get(), clip.id, before_arrangement_edit, this); - piano_->callback([](Fl_Widget *, void *c) { static_cast(c)->mark_dirty(); }, this); - scroll->end(); piano_window_->end(); piano_window_->resizable(scroll); + piano_->callback( + [](Fl_Widget *, void *c) { + static_cast(c)->mark_dirty(); + }, + this); + scroll->end(); + piano_window_->end(); + piano_window_->resizable(scroll); scroll->scroll_to(0, 60 * 16); - } else piano_->set_project(project_.get(), clip.id); - piano_window_->show(); return XTC_OK; + } else + piano_->set_project(project_.get(), clip.id); + piano_window_->show(); + return XTC_OK; } + void MainWindow::piano_callback(Fl_Widget *, void *context) { auto *window = static_cast(context); - if (window->recorder_) { window->report_error("Stop recording before editing MIDI", XTC_ERR_INVALID_STATE); return; } + if (window->recorder_) { + window->report_error("Stop recording before editing MIDI", XTC_ERR_INVALID_STATE); + return; + } xtc_result r = window->open_piano_roll(); - if (r != XTC_OK) set_label(window->error_, "Select a MIDI clip, then open Piano roll."); + if (r != XTC_OK) + set_label(window->error_, "Select a MIDI clip, then open Piano roll."); } + void MainWindow::panic_callback(Fl_Widget *, void *context) { auto *window = static_cast(context); - if (!window->ready_) return; + if (!window->ready_) + return; xtc_result r = xtc_backend_panic(window->backend_); - if (r != XTC_OK) window->report_error("Panic failed", r); + if (r != XTC_OK) + window->report_error("Panic failed", r); } xtc_result MainWindow::undo(bool redo) { - if (recorder_) return XTC_ERR_INVALID_STATE; + if (recorder_) + return XTC_ERR_INVALID_STATE; xtc_project *candidate = nullptr; xtc_result r = xtc_history_checkout(history_.get(), redo ? 1 : 0, &candidate); - if (r == XTC_OK) r = install_project(candidate, false); - if (r != XTC_OK) { xtc_project_destroy(candidate); return r; } + if (r == XTC_OK) + r = install_project(candidate, false); + if (r != XTC_OK) { + xtc_project_destroy(candidate); + return r; + } r = xtc_history_accept(history_.get(), redo ? 1 : 0); - dirty_ = true; copy_label("xtc * — unsaved changes"); + dirty_ = true; + copy_label("xtc * — unsaved changes"); return r; } + void MainWindow::history_callback(Fl_Widget *widget, void *context) { auto *window = static_cast(context); auto *menu = static_cast(widget); bool redo = std::strncmp(menu->mvalue()->label(), "Redo", 4) == 0; xtc_result r = window->undo(redo); - if (r != XTC_OK && r != XTC_ERR_NOT_FOUND) window->report_error("Undo/redo failed", r); + if (r != XTC_OK && r != XTC_ERR_NOT_FOUND) + window->report_error("Undo/redo failed", r); } diff --git a/src/gui/main_window.hpp b/src/gui/main_window.hpp index efa4d1e..1f7fbcd 100644 --- a/src/gui/main_window.hpp +++ b/src/gui/main_window.hpp @@ -17,7 +17,7 @@ class Fl_Button; class Fl_Tabs; class MainWindow : public Fl_Double_Window { -public: + public: explicit MainWindow(bool prompt_unsaved = true); ~MainWindow() override; MainWindow(const MainWindow &) = delete; @@ -26,14 +26,25 @@ public: int handle(int event) override; xtc_result undo(bool redo = false); - MixerWidget *mixer() const { return mixer_; } + + MixerWidget *mixer() const + { + return mixer_; + } + xtc_result open_piano_roll(); - PianoRollWidget *piano_roll() const { return piano_; } + + PianoRollWidget *piano_roll() const + { + return piano_; + } + xtc_result import_audio(const char *path); xtc_result record_audio(const char *path); xtc_result save_project(const char *path); xtc_result open_project(const char *path); -private: + + private: MixerWidget *mixer_ = nullptr; std::unique_ptr history_{nullptr, xtc_history_destroy}; static void history_callback(Fl_Widget *, void *); diff --git a/src/gui/mixer_widget.cpp b/src/gui/mixer_widget.cpp index b657c74..aaa1c15 100644 --- a/src/gui/mixer_widget.cpp +++ b/src/gui/mixer_widget.cpp @@ -13,16 +13,19 @@ #include #include #include -namespace -{ + +namespace { constexpr uint64_t lane_ticks = 15360; } -class AutomationLane : public Fl_Widget -{ + +class AutomationLane : public Fl_Widget { MixerWidget &mixer_; public: - AutomationLane(int x, int y, int w, int h, MixerWidget &m) : Fl_Widget(x, y, w, h), mixer_(m) {} + AutomationLane(int x, int y, int w, int h, MixerWidget &m) : Fl_Widget(x, y, w, h), mixer_(m) + { + } + void draw() override { fl_push_clip(x(), y(), w(), h()); @@ -36,8 +39,7 @@ class AutomationLane : public Fl_Widget if (i % 4 == 0) { char label[32]; std::snprintf(label, sizeof(label), "%llu", - static_cast(mixer_.lane_start() + - static_cast(i) * 960)); + static_cast(mixer_.lane_start() + static_cast(i) * 960)); fl_color(FL_WHITE); fl_draw(label, px + 3, y() + 14); } @@ -51,8 +53,9 @@ class AutomationLane : public Fl_Widget if (a.track_id == mixer_.selected_track() && a.parameter == mixer_.parameter()) points.push_back(a); } - std::sort(points.begin(), points.end(), - [](const auto &a, const auto &b) { return a.tick < b.tick; }); + std::sort(points.begin(), points.end(), [](const auto &a, const auto &b) { + return a.tick < b.tick; + }); float min = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 0.0f : -1.0f; float range = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 4.0f : 2.0f; auto py = [&](float v) { @@ -61,30 +64,31 @@ class AutomationLane : public Fl_Widget auto value = [&](uint64_t tick) { if (points.empty()) return min; - auto it = std::upper_bound(points.begin(), points.end(), tick, - [](uint64_t t, const auto &a) { return t < a.tick; }); + auto it = std::upper_bound(points.begin(), points.end(), tick, [](uint64_t t, const auto &a) { + return t < a.tick; + }); if (it == points.begin()) return it->value; if (it == points.end()) return points.back().value; const auto &a = *(it - 1); - return a.value + static_cast(static_cast(tick - a.tick) / - static_cast(it->tick - a.tick)) * - (it->value - a.value); + return a.value + + static_cast(static_cast(tick - a.tick) / static_cast(it->tick - a.tick)) * + (it->value - a.value); }; fl_color(fl_rgb_color(105, 200, 150)); if (!points.empty()) for (int px = 1; px < w(); ++px) { - uint64_t a = mixer_.lane_start() + static_cast(px - 1) * lane_ticks / - static_cast(w() - 1); - uint64_t b = mixer_.lane_start() + static_cast(px) * lane_ticks / - static_cast(w() - 1); + uint64_t a = + mixer_.lane_start() + static_cast(px - 1) * lane_ticks / static_cast(w() - 1); + uint64_t b = + mixer_.lane_start() + static_cast(px) * lane_ticks / static_cast(w() - 1); fl_line(x() + px - 1, py(value(a)), x() + px, py(value(b))); } for (const auto &a : points) if (a.tick >= mixer_.lane_start() && a.tick <= mixer_.lane_start() + lane_ticks) { - int px = x() + static_cast((a.tick - mixer_.lane_start()) * - static_cast(w() - 1) / lane_ticks); + int px = x() + + static_cast((a.tick - mixer_.lane_start()) * static_cast(w() - 1) / lane_ticks); fl_rectf(px - 4, py(a.value) - 4, 8, 8); } if (points.empty()) { @@ -95,34 +99,31 @@ class AutomationLane : public Fl_Widget } fl_pop_clip(); } + int handle(int event) override { if (event != FL_PUSH) return Fl_Widget::handle(event); if (Fl::event_button() != FL_LEFT_MOUSE && Fl::event_button() != FL_RIGHT_MOUSE) return 0; - uint64_t tick = mixer_.lane_start() + - static_cast(std::clamp(Fl::event_x() - x(), 0, w() - 1)) * - lane_ticks / static_cast(w() - 1); + uint64_t tick = mixer_.lane_start() + static_cast(std::clamp(Fl::event_x() - x(), 0, w() - 1)) * + lane_ticks / static_cast(w() - 1); tick = (tick + 120) / 240 * 240; tick = std::min(tick, XTC_PROJECT_MAX_TICK); float min = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 0.0f : -1.0f; float range = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 4.0f : 2.0f; - float value = - min + static_cast(std::clamp(y() + h() - 6 - Fl::event_y(), 0, h() - 28)) / - static_cast(h() - 28) * range; + float value = min + static_cast(std::clamp(y() + h() - 6 - Fl::event_y(), 0, h() - 28)) / + static_cast(h() - 28) * range; if (Fl::event_button() == FL_RIGHT_MOUSE) { size_t count = 0; xtc_project_automation_count(mixer_.project(), &count); - uint64_t distance = - static_cast(8) * lane_ticks / static_cast(w() - 1) + 1, + uint64_t distance = static_cast(8) * lane_ticks / static_cast(w() - 1) + 1, closest = UINT64_MAX; for (size_t i = 0; i < count; ++i) { xtc_automation_point a{}; xtc_project_get_automation(mixer_.project(), i, &a); uint64_t d = a.tick > tick ? a.tick - tick : tick - a.tick; - if (a.track_id == mixer_.selected_track() && a.parameter == mixer_.parameter() && - d < distance) { + if (a.track_id == mixer_.selected_track() && a.parameter == mixer_.parameter() && d < distance) { distance = d; closest = a.tick; } @@ -134,8 +135,8 @@ class AutomationLane : public Fl_Widget return 1; } }; -MixerWidget::MixerWidget(int x, int y, int w, int h, xtc_project *p, BeforeEdit before, - void *context) + +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) { begin(); @@ -180,23 +181,26 @@ MixerWidget::MixerWidget(int x, int y, int w, int h, xtc_project *p, BeforeEdit end(); set_project(p); } + 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; + return xtc_project_get_track(project_, static_cast(tracks_->value()), &t) == XTC_OK ? t.id : UINT64_MAX; } + xtc_automation_parameter MixerWidget::parameter() const { return parameter_->value() == 1 ? XTC_AUTOMATION_PAN : XTC_AUTOMATION_GAIN; } + uint64_t MixerWidget::lane_start() const { - if (!std::isfinite(start_->value())) return 0; + if (!std::isfinite(start_->value())) + return 0; return static_cast( std::clamp(start_->value(), 0.0, static_cast(XTC_PROJECT_MAX_TICK - lane_ticks))); } + void MixerWidget::set_project(xtc_project *p) { uint64_t selected = project_ == p ? selected_track() : UINT64_MAX; @@ -222,14 +226,14 @@ void MixerWidget::set_project(xtc_project *p) selection(); redraw(); } + void MixerWidget::selection() { xtc_track_info t{}; - bool valid = - xtc_project_get_track(project_, static_cast(tracks_->value()), &t) == XTC_OK; - for (Fl_Widget *w : {static_cast(gain_), static_cast(pan_), - static_cast(mute_), static_cast(solo_), - static_cast(lane_)}) { + bool valid = xtc_project_get_track(project_, static_cast(tracks_->value()), &t) == XTC_OK; + for (Fl_Widget *w : + {static_cast(gain_), static_cast(pan_), static_cast(mute_), + static_cast(solo_), static_cast(lane_)}) { if (valid) w->activate(); else @@ -241,6 +245,7 @@ void MixerWidget::selection() solo_->value(t.solo); lane_->redraw(); } + void MixerWidget::peaks(float left, float right) { left_->value(std::min(left, 1.0f)); @@ -248,6 +253,7 @@ void MixerWidget::peaks(float left, float right) left_->selection_color(left > 1 ? FL_RED : FL_GREEN); right_->selection_color(right > 1 ? FL_RED : FL_GREEN); } + xtc_result MixerWidget::changed(xtc_result r) { if (r == XTC_OK) @@ -257,6 +263,7 @@ xtc_result MixerWidget::changed(xtc_result r) selection(); return r; } + xtc_result MixerWidget::mix(float gain, float pan, int mute, int solo) { xtc_result r = before_ ? before_(context_) : XTC_OK; @@ -264,6 +271,7 @@ xtc_result MixerWidget::mix(float gain, float pan, int mute, int solo) r = xtc_project_set_mix(project_, selected_track(), gain, pan, mute, solo); return changed(r); } + xtc_result MixerWidget::point(uint64_t tick, float value, bool remove) { xtc_result r = before_ ? before_(context_) : XTC_OK; @@ -273,16 +281,19 @@ xtc_result MixerWidget::point(uint64_t tick, float value, bool remove) : xtc_project_set_automation(project_, &a); return changed(r); } + void MixerWidget::control(Fl_Widget *, void *c) { auto *m = static_cast(c); - m->mix(static_cast(m->gain_->value()), static_cast(m->pan_->value()), - m->mute_->value(), m->solo_->value()); + m->mix(static_cast(m->gain_->value()), static_cast(m->pan_->value()), m->mute_->value(), + m->solo_->value()); } + void MixerWidget::select(Fl_Widget *, void *c) { static_cast(c)->selection(); } + int MixerWidget::handle(int e) { if (e == FL_KEYDOWN && !(Fl::event_state() & (FL_CTRL | FL_ALT | FL_META))) { @@ -296,15 +307,14 @@ int MixerWidget::handle(int e) if (key == FL_Left || key == FL_Right) { float d = key == FL_Left ? -0.05f : 0.05f; bool pan = (Fl::event_state() & FL_SHIFT) != 0; - mix(pan ? t.gain : std::clamp(t.gain + d, 0.0f, 4.0f), - pan ? std::clamp(t.pan + d, -1.0f, 1.0f) : t.pan, t.mute, t.solo); + mix(pan ? t.gain : std::clamp(t.gain + d, 0.0f, 4.0f), pan ? std::clamp(t.pan + d, -1.0f, 1.0f) : t.pan, + t.mute, t.solo); return 1; } if (key == FL_Up || key == FL_Down) { size_t n = 0, c = 0; xtc_project_get_counts(project_, &n, &c); - tracks_->value(std::clamp(tracks_->value() + (key == FL_Up ? -1 : 1), 0, - static_cast(n) - 1)); + tracks_->value(std::clamp(tracks_->value() + (key == FL_Up ? -1 : 1), 0, static_cast(n) - 1)); selection(); return 1; } diff --git a/src/gui/mixer_widget.hpp b/src/gui/mixer_widget.hpp index 18fa1ac..e54c986 100644 --- a/src/gui/mixer_widget.hpp +++ b/src/gui/mixer_widget.hpp @@ -2,24 +2,27 @@ #define XTC_MIXER_WIDGET_HPP #include "xtc/xtc_mixer.h" #include + class Fl_Choice; class Fl_Value_Slider; class Fl_Check_Button; class Fl_Progress; class Fl_Value_Input; class AutomationLane; -class MixerWidget : public Fl_Group -{ + +class MixerWidget : public Fl_Group { public: using BeforeEdit = xtc_result (*)(void *); MixerWidget(int, int, int, int, xtc_project *, BeforeEdit, void *); void set_project(xtc_project *); void peaks(float left, float right); uint64_t selected_track() const; + xtc_project *project() const { return project_; } + xtc_automation_parameter parameter() const; uint64_t lane_start() const; xtc_result point(uint64_t tick, float value, bool remove = false); diff --git a/src/gui/piano_roll.cpp b/src/gui/piano_roll.cpp index 6b6abf2..8685e0f 100644 --- a/src/gui/piano_roll.cpp +++ b/src/gui/piano_roll.cpp @@ -4,55 +4,87 @@ #include #include #include -namespace { constexpr int key_width = 64, row_height = 16, scale = 96, header = 28; } -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), context_(context) -{ set_project(project, clip); } + +namespace { +constexpr int key_width = 64, row_height = 16, scale = 96, header = 28; +} + +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), + context_(context) +{ + set_project(project, clip); +} + void PianoRollWidget::set_project(xtc_project *project, uint64_t clip) { - if (project != project_ || clip != clip_) mode_ = 0; - project_ = project; clip_ = clip; + if (project != project_ || clip != clip_) + mode_ = 0; + project_ = project; + clip_ = clip; xtc_clip_info c{}; - int width = xtc_project_find_clip(project_, clip_, &c) == XTC_OK ? - static_cast(c.source_length_tick * scale / 960) + key_width + 20 : 900; + int width = xtc_project_find_clip(project_, clip_, &c) == XTC_OK + ? static_cast(c.source_length_tick * scale / 960) + key_width + 20 + : 900; size(std::max(900, width), header + 128 * row_height); redraw(); } + uint64_t PianoRollWidget::tick(int px) const -{ return static_cast(std::max(0, px - x() - key_width)) * 960U / scale / 240U * 240U; } +{ + return static_cast(std::max(0, px - x() - key_width)) * 960U / scale / 240U * 240U; +} + xtc_result PianoRollWidget::change(xtc_result result) { - if (result == XTC_OK) { do_callback(); redraw(); } - else fl_alert("Note unchanged: %s", xtc_result_string(result)); + if (result == XTC_OK) { + do_callback(); + redraw(); + } else + fl_alert("Note unchanged: %s", xtc_result_string(result)); return result; } -xtc_result PianoRollWidget::add_note(uint64_t start, uint64_t length, unsigned int pitch, - unsigned int velocity, unsigned int channel) + +xtc_result PianoRollWidget::add_note(uint64_t start, uint64_t length, unsigned int pitch, unsigned int velocity, + unsigned int channel) { xtc_result r = before_ ? before_(context_) : XTC_OK; uint64_t id; - if (r == XTC_OK) r = xtc_project_add_note(project_, clip_, start, length, pitch, velocity, channel, &id); - if (r == XTC_OK) selected_ = id; + if (r == XTC_OK) + r = xtc_project_add_note(project_, clip_, start, length, pitch, velocity, channel, &id); + if (r == XTC_OK) + selected_ = id; return change(r); } + xtc_result PianoRollWidget::delete_selected() { xtc_result r = before_ ? before_(context_) : XTC_OK; - if (r == XTC_OK) r = xtc_project_remove_note(project_, selected_); - if (r == XTC_OK) selected_ = UINT64_MAX; + if (r == XTC_OK) + r = xtc_project_remove_note(project_, selected_); + if (r == XTC_OK) + selected_ = UINT64_MAX; return change(r); } + void PianoRollWidget::draw() { fl_push_clip(x(), y(), w(), h()); - fl_color(fl_rgb_color(30, 33, 38)); fl_rectf(x(), y(), w(), h()); + fl_color(fl_rgb_color(30, 33, 38)); + fl_rectf(x(), y(), w(), h()); xtc_clip_info clip{}; - if (xtc_project_find_clip(project_, clip_, &clip) != XTC_OK || !clip.midi) { fl_pop_clip(); return; } - int cx, cy, cw, ch; fl_clip_box(x(), y(), w(), h(), cx, cy, cw, ch); + if (xtc_project_find_clip(project_, clip_, &clip) != XTC_OK || !clip.midi) { + fl_pop_clip(); + return; + } + int cx, cy, cw, ch; + fl_clip_box(x(), y(), w(), h(), cx, cy, cw, ch); fl_font(FL_HELVETICA, 11); for (int pitch = 0; pitch < 128; ++pitch) { int py = y() + header + (127 - pitch) * row_height; - if (py + row_height < cy || py > cy + ch) continue; + if (py + row_height < cy || py > cy + ch) + continue; int pc = pitch % 12; bool black = pc == 1 || pc == 3 || pc == 6 || pc == 8 || pc == 10; fl_color(black ? fl_rgb_color(29, 31, 35) : fl_rgb_color(43, 46, 52)); @@ -60,8 +92,10 @@ void PianoRollWidget::draw() fl_color(black ? fl_rgb_color(60, 63, 70) : fl_rgb_color(130, 135, 140)); fl_rectf(x(), py, key_width - 2, row_height - 1); if (!pc) { - char text[20]; std::snprintf(text, sizeof(text), "C%d", pitch / 12 - 1); - fl_color(FL_WHITE); fl_draw(text, x() + 6, py + 12); + char text[20]; + std::snprintf(text, sizeof(text), "C%d", pitch / 12 - 1); + fl_color(FL_WHITE); + fl_draw(text, x() + 6, py + 12); } } int first = std::max(0, (cx - x() - key_width) / (scale / 4)); @@ -71,68 +105,101 @@ void PianoRollWidget::draw() fl_color(step % 16 == 0 ? fl_rgb_color(95, 98, 105) : fl_rgb_color(52, 55, 62)); fl_line(px, cy, px, cy + ch); } - size_t count = 0; xtc_project_note_count(project_, &count); + size_t count = 0; + xtc_project_note_count(project_, &count); for (size_t i = 0; i < count; ++i) { - xtc_note_info n{}; xtc_project_get_note(project_, i, &n); - if (n.clip_id != clip_) continue; + xtc_note_info n{}; + xtc_project_get_note(project_, i, &n); + if (n.clip_id != clip_) + continue; int left = x() + key_width + static_cast(n.start_tick * scale / 960); int right = left + std::max(3, static_cast(n.length_tick * scale / 960)); int py = y() + header + (127 - static_cast(n.pitch)) * row_height; int a = std::max(cx, left), b = std::min(cx + cw, right); - if (a >= b || py + row_height < cy || py > cy + ch) continue; + if (a >= b || py + row_height < cy || py > cy + ch) + continue; fl_color(n.id == selected_ ? fl_rgb_color(230, 190, 80) : fl_rgb_color(90, 175, 130)); fl_rectf(a, py + 1, b - a, row_height - 3); } fl_pop_clip(); } + int PianoRollWidget::handle(int event) { if (event == FL_PUSH) { - take_focus(); mode_ = 0; - size_t count = 0; xtc_project_note_count(project_, &count); + take_focus(); + mode_ = 0; + size_t count = 0; + xtc_project_note_count(project_, &count); selected_ = UINT64_MAX; for (size_t i = count; i > 0; --i) { - xtc_note_info n{}; xtc_project_get_note(project_, i - 1, &n); + xtc_note_info n{}; + xtc_project_get_note(project_, i - 1, &n); int left = x() + key_width + static_cast(n.start_tick * scale / 960); int right = left + std::max(3, static_cast(n.length_tick * scale / 960)); int top = y() + header + (127 - static_cast(n.pitch)) * row_height; - if (n.clip_id == clip_ && Fl::event_x() >= left && Fl::event_x() < right && - Fl::event_y() >= top && Fl::event_y() < top + row_height) { - selected_ = n.id; original_ = n; drag_x_ = Fl::event_x(); drag_y_ = Fl::event_y(); - mode_ = right - Fl::event_x() <= 6 ? 2 : 1; break; + if (n.clip_id == clip_ && Fl::event_x() >= left && Fl::event_x() < right && Fl::event_y() >= top && + Fl::event_y() < top + row_height) { + selected_ = n.id; + original_ = n; + drag_x_ = Fl::event_x(); + drag_y_ = Fl::event_y(); + mode_ = right - Fl::event_x() <= 6 ? 2 : 1; + break; } } - if (selected_ != UINT64_MAX && Fl::event_button() == FL_RIGHT_MOUSE) { delete_selected(); mode_ = 0; } - else if (Fl::event_button() == FL_LEFT_MOUSE && selected_ == UINT64_MAX && - Fl::event_x() >= x() + key_width && Fl::event_y() >= y() + header) { + if (selected_ != UINT64_MAX && Fl::event_button() == FL_RIGHT_MOUSE) { + delete_selected(); + mode_ = 0; + } else if (Fl::event_button() == FL_LEFT_MOUSE && selected_ == UINT64_MAX && Fl::event_x() >= x() + key_width && + Fl::event_y() >= y() + header) { int pitch = 127 - (Fl::event_y() - y() - header) / row_height; - xtc_clip_info c{}; uint64_t start = tick(Fl::event_x()); - if (pitch >= 0 && pitch <= 127 && xtc_project_find_clip(project_, clip_, &c) == XTC_OK && start < c.source_length_tick) - add_note(start, std::min(960, c.source_length_tick - start), static_cast(pitch)); + xtc_clip_info c{}; + uint64_t start = tick(Fl::event_x()); + if (pitch >= 0 && pitch <= 127 && xtc_project_find_clip(project_, clip_, &c) == XTC_OK && + start < c.source_length_tick) + add_note(start, std::min(960, c.source_length_tick - start), + static_cast(pitch)); } - redraw(); return 1; + redraw(); + return 1; } - if (event == FL_DRAG) return mode_ != 0; + if (event == FL_DRAG) + return mode_ != 0; if (event == FL_RELEASE && mode_) { - int mode = mode_; mode_ = 0; + int mode = mode_; + mode_ = 0; int64_t delta = static_cast(Fl::event_x() - drag_x_) * 960 / scale / 240 * 240; - xtc_clip_info c{}; if (xtc_project_find_clip(project_, clip_, &c) != XTC_OK) return 1; - int64_t start = static_cast(original_.start_tick), length = static_cast(original_.length_tick); + xtc_clip_info c{}; + if (xtc_project_find_clip(project_, clip_, &c) != XTC_OK) + return 1; + int64_t start = static_cast(original_.start_tick), + length = static_cast(original_.length_tick); unsigned int pitch = original_.pitch; if (mode == 1) { start = std::clamp(start + delta, 0, static_cast(c.source_length_tick) - length); - pitch = static_cast(std::clamp(static_cast(pitch) - (Fl::event_y() - drag_y_) / row_height, 0, 127)); + pitch = static_cast( + std::clamp(static_cast(pitch) - (Fl::event_y() - drag_y_) / row_height, 0, 127)); } else { int64_t maximum = static_cast(c.source_length_tick) - start; length = std::clamp(length + delta, std::min(240, maximum), maximum); } xtc_result r = before_ ? before_(context_) : XTC_OK; - if (r == XTC_OK) r = xtc_project_edit_note(project_, selected_, static_cast(start), - static_cast(length), pitch, original_.velocity, original_.channel); - change(r); return 1; + if (r == XTC_OK) + r = xtc_project_edit_note(project_, selected_, static_cast(start), static_cast(length), + pitch, original_.velocity, original_.channel); + change(r); + return 1; } - if (event == FL_KEYDOWN && Fl::event_key() == FL_Delete) { delete_selected(); return 1; } - if (event == FL_KEYDOWN && Fl::event_key() == FL_Escape) { mode_ = 0; return 1; } - if (event == FL_FOCUS || event == FL_UNFOCUS) return 1; + if (event == FL_KEYDOWN && Fl::event_key() == FL_Delete) { + delete_selected(); + return 1; + } + if (event == FL_KEYDOWN && Fl::event_key() == FL_Escape) { + mode_ = 0; + return 1; + } + if (event == FL_FOCUS || event == FL_UNFOCUS) + return 1; return Fl_Widget::handle(event); } diff --git a/src/gui/piano_roll.hpp b/src/gui/piano_roll.hpp index bd072e0..f2f9e4a 100644 --- a/src/gui/piano_roll.hpp +++ b/src/gui/piano_roll.hpp @@ -2,19 +2,27 @@ #define XTC_PIANO_ROLL_HPP #include #include "xtc/xtc_midi.h" + class PianoRollWidget : public Fl_Widget { -public: + public: using BeforeEdit = xtc_result (*)(void *); PianoRollWidget(int x, int y, xtc_project *project, uint64_t clip, BeforeEdit before, void *context); void set_project(xtc_project *project, uint64_t clip); - uint64_t clip() const { return clip_; } - xtc_result add_note(uint64_t start, uint64_t length, unsigned int pitch, - unsigned int velocity = 100, unsigned int channel = 0); + + uint64_t clip() const + { + return clip_; + } + + xtc_result add_note(uint64_t start, uint64_t length, unsigned int pitch, unsigned int velocity = 100, + unsigned int channel = 0); xtc_result delete_selected(); int handle(int event) override; -protected: + + protected: void draw() override; -private: + + private: xtc_project *project_; uint64_t clip_, selected_ = UINT64_MAX; BeforeEdit before_; diff --git a/src/gui/sequencer_widget.cpp b/src/gui/sequencer_widget.cpp index e3d8284..2dfbca1 100644 --- a/src/gui/sequencer_widget.cpp +++ b/src/gui/sequencer_widget.cpp @@ -6,56 +6,101 @@ #include #include #include + 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(); } + : Fl_Widget(left, top, 6300, 3700), project_(project), seek_(seek), context_(context) +{ + extent(); +} + void SequencerWidget::set_project(xtc_project *project, bool reset) { project_ = project; - if (reset) { track_ = clip_ = UINT64_MAX; cursor_ = 0; mode_ = 0; } - extent(); redraw(); + if (reset) { + track_ = clip_ = UINT64_MAX; + cursor_ = 0; + mode_ = 0; + } + extent(); + redraw(); } + 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); + static_cast(scale_) / 240U * 240U); } + void SequencerWidget::extent() { size_t tracks, clips; xtc_project_get_counts(project_, &tracks, &clips); uint64_t end = 32U * 3840U; for (size_t i = 0; i < clips; ++i) { - xtc_clip_info c{}; xtc_project_get_clip(project_, i, &c); + xtc_clip_info c{}; + xtc_project_get_clip(project_, i, &c); end = std::max(end, c.start_tick + c.length_tick + 3840U); } size(static_cast(end * static_cast(scale_) / 960U + 140U), std::max(400, static_cast(tracks) * 56 + 100)); - if (parent()) parent()->redraw(); + if (parent()) + parent()->redraw(); } + void SequencerWidget::changed(xtc_result result) { - if (result != XTC_OK) fl_alert("%s", xtc_result_string(result)); - else { extent(); do_callback(); } + if (result != XTC_OK) + fl_alert("%s", xtc_result_string(result)); + else { + extent(); + do_callback(); + } redraw(); } + xtc_result SequencerWidget::add_track() { - if (before_edit) { xtc_result r = before_edit(context_); if (r != XTC_OK) { changed(r); return r; } } - uint64_t id; xtc_result r = xtc_project_add_track(project_, "Track", &id); - if (r == XTC_OK) { track_ = id; clip_ = UINT64_MAX; } changed(r); return r; + if (before_edit) { + xtc_result r = before_edit(context_); + if (r != XTC_OK) { + changed(r); + return r; + } + } + uint64_t id; + xtc_result r = xtc_project_add_track(project_, "Track", &id); + if (r == XTC_OK) { + track_ = id; + clip_ = UINT64_MAX; + } + changed(r); + return r; } + xtc_result SequencerWidget::add_region() { - if (before_edit) { xtc_result r = before_edit(context_); if (r != XTC_OK) { changed(r); return r; } } - size_t tracks, clips; xtc_project_get_counts(project_, &tracks, &clips); + if (before_edit) { + xtc_result r = before_edit(context_); + if (r != XTC_OK) { + changed(r); + return r; + } + } + size_t tracks, clips; + xtc_project_get_counts(project_, &tracks, &clips); if (track_ == UINT64_MAX && tracks) { - xtc_track_info t{}; xtc_project_get_track(project_, 0, &t); track_ = t.id; + xtc_track_info t{}; + xtc_project_get_track(project_, 0, &t); + track_ = t.id; } uint64_t id; xtc_result r = xtc_project_add_clip(project_, track_, "Region (silent)", cursor_, 3840U, &id); - if (r == XTC_OK) clip_ = id; - changed(r); return r; + if (r == XTC_OK) + clip_ = id; + changed(r); + return r; } + xtc_result SequencerWidget::add_midi_clip() { xtc_result r = before_edit ? before_edit(context_) : XTC_OK; @@ -63,96 +108,146 @@ xtc_result SequencerWidget::add_midi_clip() if (r == XTC_OK) { if (track_ == UINT64_MAX) { xtc_track_info t{}; - r = xtc_project_get_track(project_, 0, &t); track_ = r == XTC_OK ? t.id : UINT64_MAX; + r = xtc_project_get_track(project_, 0, &t); + track_ = r == XTC_OK ? t.id : UINT64_MAX; } - if (r == XTC_OK) r = xtc_project_add_midi_clip(project_, track_, cursor_, 3840, &id); - if (r == XTC_OK) clip_ = id; + if (r == XTC_OK) + r = xtc_project_add_midi_clip(project_, track_, cursor_, 3840, &id); + if (r == XTC_OK) + clip_ = id; } - changed(r); return r; + changed(r); + return r; } + xtc_result SequencerWidget::delete_selection() { - if (before_edit) { xtc_result r = before_edit(context_); if (r != XTC_OK) { changed(r); return r; } } - xtc_result r = clip_ != UINT64_MAX ? xtc_project_remove_clip(project_, clip_) : - xtc_project_remove_track(project_, track_); - if (r == XTC_OK) clip_ = track_ = UINT64_MAX; - changed(r); return r; + if (before_edit) { + xtc_result r = before_edit(context_); + if (r != XTC_OK) { + changed(r); + return r; + } + } + xtc_result r = + clip_ != UINT64_MAX ? xtc_project_remove_clip(project_, clip_) : xtc_project_remove_track(project_, track_); + if (r == XTC_OK) + clip_ = track_ = UINT64_MAX; + changed(r); + return r; } + void SequencerWidget::zoom(int direction) -{ scale_ = std::clamp(direction > 0 ? scale_ * 2 : scale_ / 2, 24, 384); extent(); redraw(); } +{ + scale_ = std::clamp(direction > 0 ? scale_ * 2 : scale_ / 2, 24, 384); + extent(); + redraw(); +} + void SequencerWidget::draw() { fl_push_clip(x(), y(), w(), h()); - fl_color(fl_rgb_color(35, 38, 43)); fl_rectf(x(), y(), w(), h()); - int cx, cy, cw, ch; fl_clip_box(x(), y(), w(), h(), cx, cy, cw, ch); + fl_color(fl_rgb_color(35, 38, 43)); + fl_rectf(x(), y(), w(), h()); + int cx, cy, cw, ch; + fl_clip_box(x(), y(), w(), h(), cx, cy, cw, ch); fl_font(FL_HELVETICA, 12); int first = std::max(0, (cx - x() - 120) / (scale_ * 4)); int last = (cx + cw - x() - 120) / (scale_ * 4) + 1; for (int bar = first; bar <= last; ++bar) { int px = x() + 120 + bar * scale_ * 4; - fl_color(fl_rgb_color(65, 68, 74)); fl_line(px, y(), px, y() + h()); - char label[32]; std::snprintf(label, sizeof(label), "%d", bar + 1); - fl_color(FL_WHITE); fl_draw(label, px + 4, y() + 19); + fl_color(fl_rgb_color(65, 68, 74)); + fl_line(px, y(), px, y() + h()); + char label[32]; + std::snprintf(label, sizeof(label), "%d", bar + 1); + fl_color(FL_WHITE); + fl_draw(label, px + 4, y() + 19); } char tempo[32]; std::snprintf(tempo, sizeof(tempo), "%.3g BPM | 4/4", xtc_project_tempo(project_) / 1000.0); - fl_color(FL_WHITE); fl_draw(tempo, x() + 4, y() + 19); - size_t tracks, clips; xtc_project_get_counts(project_, &tracks, &clips); + fl_color(FL_WHITE); + fl_draw(tempo, x() + 4, y() + 19); + size_t tracks, clips; + xtc_project_get_counts(project_, &tracks, &clips); for (size_t i = 0; i < tracks; ++i) { - xtc_track_info t{}; xtc_project_get_track(project_, i, &t); + xtc_track_info t{}; + xtc_project_get_track(project_, i, &t); int py = y() + 28 + static_cast(i) * 56; fl_color(t.id == track_ ? fl_rgb_color(70, 90, 110) : fl_rgb_color(48, 51, 57)); fl_rectf(x(), py, 118, 55); - fl_color(FL_WHITE); fl_draw(t.name, x() + 6, py + 32); + fl_color(FL_WHITE); + fl_draw(t.name, x() + 6, py + 32); for (size_t j = 0; j < clips; ++j) { - xtc_clip_info c{}; xtc_project_get_clip(project_, j, &c); - if (c.track_id != t.id) continue; + xtc_clip_info c{}; + xtc_project_get_clip(project_, j, &c); + if (c.track_id != t.id) + continue; int left = x() + 120 + static_cast(c.start_tick * static_cast(scale_) / 960U); int right = left + std::max(4, static_cast(c.length_tick * static_cast(scale_) / 960U)); int a = std::max(left, cx), b = std::min(right, cx + cw); - if (b <= a || py + 50 < cy || py > cy + ch) continue; + if (b <= a || py + 50 < cy || py > cy + ch) + continue; fl_color(c.id == clip_ ? fl_rgb_color(100, 170, 205) : fl_rgb_color(65, 120, 150)); fl_rectf(a, py + 5, b - a, 44); - fl_push_clip(a, py + 5, b - a, 44); fl_color(FL_WHITE); - fl_draw(c.midi ? "MIDI" : c.asset_path[0] ? "Audio" : c.name, std::max(a, left + 6), py + 31); fl_pop_clip(); + fl_push_clip(a, py + 5, b - a, 44); + fl_color(FL_WHITE); + fl_draw(c.midi ? "MIDI" : c.asset_path[0] ? "Audio" : c.name, std::max(a, left + 6), py + 31); + fl_pop_clip(); } } int playhead = x() + 120 + static_cast(cursor_ * static_cast(scale_) / 960U); if (playhead >= cx && playhead <= cx + cw) { - fl_color(FL_RED); fl_line(playhead, y(), playhead, y() + h()); + fl_color(FL_RED); + fl_line(playhead, y(), playhead, y() + h()); } fl_pop_clip(); } + int SequencerWidget::handle(int event) { if (event == FL_PUSH && Fl::event_button() == FL_LEFT_MOUSE) { - take_focus(); mode_ = 0; + take_focus(); + mode_ = 0; if (Fl::event_y() < y() + 28) { uint64_t target = tick(Fl::event_x()); xtc_result r = seek_(context_, target); - if (r == XTC_OK) cursor_ = target; - else fl_alert("Stop playback before seeking: %s", xtc_result_string(r)); - redraw(); return 1; + if (r == XTC_OK) + cursor_ = target; + else + fl_alert("Stop playback before seeking: %s", xtc_result_string(r)); + redraw(); + return 1; } - size_t tracks, clips; xtc_project_get_counts(project_, &tracks, &clips); + size_t tracks, clips; + xtc_project_get_counts(project_, &tracks, &clips); int row = (Fl::event_y() - y() - 28) / 56; - if (row < 0 || static_cast(row) >= tracks) return 1; - xtc_track_info t{}; xtc_project_get_track(project_, static_cast(row), &t); track_ = t.id; + if (row < 0 || static_cast(row) >= tracks) + return 1; + xtc_track_info t{}; + xtc_project_get_track(project_, static_cast(row), &t); + track_ = t.id; clip_ = UINT64_MAX; for (size_t j = clips; j > 0; --j) { - xtc_clip_info c{}; xtc_project_get_clip(project_, j - 1, &c); + xtc_clip_info c{}; + xtc_project_get_clip(project_, j - 1, &c); int left = x() + 120 + static_cast(c.start_tick * static_cast(scale_) / 960U); int right = left + static_cast(c.length_tick * static_cast(scale_) / 960U); if (c.track_id == track_ && Fl::event_x() >= left && Fl::event_x() < right) { - clip_ = c.id; original_ = c; drag_x_ = Fl::event_x(); - mode_ = Fl::event_x() - left < 7 ? 2 : right - Fl::event_x() < 7 ? 3 : 1; break; + clip_ = c.id; + original_ = c; + drag_x_ = Fl::event_x(); + mode_ = Fl::event_x() - left < 7 ? 2 : right - Fl::event_x() < 7 ? 3 : 1; + break; } } - redraw(); return 1; + redraw(); + return 1; } - if (event == FL_DRAG) return mode_ != 0; + if (event == FL_DRAG) + return mode_ != 0; if (event == FL_RELEASE && mode_) { - int mode = mode_; mode_ = 0; + int mode = mode_; + mode_ = 0; int64_t delta = static_cast(Fl::event_x() - drag_x_) * 960 / scale_ / 240 * 240; int64_t start = static_cast(original_.start_tick); int64_t length = static_cast(original_.length_tick); @@ -162,17 +257,36 @@ int SequencerWidget::handle(int event) start = std::max(0, start + delta); int row = (Fl::event_y() - y() - 28) / 56; xtc_track_info t{}; - if (row >= 0 && xtc_project_get_track(project_, static_cast(row), &t) == XTC_OK) target = t.id; - } else if (mode == 2) { start += delta; offset += delta; length -= delta; } - else length += delta; - if (start < 0 || offset < 0 || length <= 0) return 1; - if (before_edit) { xtc_result r = before_edit(context_); if (r != XTC_OK) { changed(r); return 1; } } + if (row >= 0 && xtc_project_get_track(project_, static_cast(row), &t) == XTC_OK) + target = t.id; + } else if (mode == 2) { + start += delta; + offset += delta; + length -= delta; + } else + length += delta; + if (start < 0 || offset < 0 || length <= 0) + return 1; + if (before_edit) { + xtc_result r = before_edit(context_); + if (r != XTC_OK) { + changed(r); + return 1; + } + } changed(xtc_project_edit_clip(project_, clip_, target, static_cast(start), - static_cast(length), static_cast(offset))); + static_cast(length), static_cast(offset))); return 1; } - if (event == FL_KEYDOWN && Fl::event_key() == FL_Escape) { mode_ = 0; return 1; } - if (event == FL_KEYDOWN && Fl::event_key() == FL_Delete) { delete_selection(); return 1; } - if (event == FL_FOCUS || event == FL_UNFOCUS) return 1; + if (event == FL_KEYDOWN && Fl::event_key() == FL_Escape) { + mode_ = 0; + return 1; + } + if (event == FL_KEYDOWN && Fl::event_key() == FL_Delete) { + delete_selection(); + return 1; + } + if (event == FL_FOCUS || event == FL_UNFOCUS) + return 1; return Fl_Widget::handle(event); } diff --git a/src/gui/sequencer_widget.hpp b/src/gui/sequencer_widget.hpp index 23eb961..ab1d2a8 100644 --- a/src/gui/sequencer_widget.hpp +++ b/src/gui/sequencer_widget.hpp @@ -2,27 +2,54 @@ #define XTC_SEQUENCER_WIDGET_HPP #include #include "xtc/xtc_project.h" + class SequencerWidget : public Fl_Widget { -public: + public: using BeforeEdit = xtc_result (*)(void *); BeforeEdit before_edit = nullptr; - uint64_t selected_track() const { return track_; } - uint64_t selected_clip() const { return clip_; } + + uint64_t selected_track() const + { + return track_; + } + + uint64_t selected_clip() const + { + return clip_; + } + using Seek = xtc_result (*)(void *, uint64_t); SequencerWidget(int x, int y, xtc_project *project, Seek seek, void *context); void set_project(xtc_project *project, bool reset = true); - uint64_t position() const { return cursor_; } - void position(uint64_t tick) { cursor_ = tick; redraw(); } + + uint64_t position() const + { + return cursor_; + } + + void position(uint64_t tick) + { + cursor_ = tick; + redraw(); + } + xtc_result add_track(); xtc_result add_region(); xtc_result add_midi_clip(); xtc_result delete_selection(); void zoom(int direction); - xtc_project *project() const { return project_; } + + xtc_project *project() const + { + return project_; + } + int handle(int event) override; -protected: + + protected: void draw() override; -private: + + private: xtc_project *project_; Seek seek_; void *context_; diff --git a/src/main.c b/src/main.c index f46fee3..b14c427 100644 --- a/src/main.c +++ b/src/main.c @@ -9,33 +9,42 @@ int main(int argc, char **argv) { if (argc == 2 && !strcmp(argv[1], "--version")) { - printf("%s %s\n", XTC_PROJECT_NAME, XTC_VERSION_STRING); return EXIT_SUCCESS; + printf("%s %s\n", XTC_PROJECT_NAME, XTC_VERSION_STRING); + return EXIT_SUCCESS; } if (argc == 2 && !strcmp(argv[1], "--help")) { - puts("Usage: xtc-check [--version | --help | --validate PROJECT]\nNo arguments checks core startup/shutdown. Validation checks metadata only."); return EXIT_SUCCESS; + puts("Usage: xtc-check [--version | --help | --validate PROJECT]\nNo arguments checks core startup/shutdown. " + "Validation checks metadata only."); + return EXIT_SUCCESS; } if (argc == 3 && !strcmp(argv[1], "--validate")) { xtc_project *project = NULL; xtc_result result = xtc_project_create(&project); - if (result == XTC_OK) result = xtc_project_load(project, argv[2]); + if (result == XTC_OK) + result = xtc_project_load(project, argv[2]); xtc_project_destroy(project); - if (result != XTC_OK) { fprintf(stderr, "Project validation failed: %s\n", xtc_result_string(result)); return EXIT_FAILURE; } - puts("Project metadata valid (external assets not loaded)."); return EXIT_SUCCESS; + if (result != XTC_OK) { + fprintf(stderr, "Project validation failed: %s\n", xtc_result_string(result)); + return EXIT_FAILURE; + } + puts("Project metadata valid (external assets not loaded)."); + return EXIT_SUCCESS; + } + if (argc != 1) { + fputs("Usage: xtc-check [--version | --help | --validate PROJECT]\n", stderr); + return EXIT_FAILURE; } - if (argc != 1) { fputs("Usage: xtc-check [--version | --help | --validate PROJECT]\n", stderr); return EXIT_FAILURE; } xtc_app *app = NULL; xtc_result result = xtc_app_create(&app); if (result != XTC_OK) { - (void)fprintf(stderr, "Application initialization failed: %s\n", - xtc_result_string(result)); + (void)fprintf(stderr, "Application initialization failed: %s\n", xtc_result_string(result)); return EXIT_FAILURE; } xtc_app_destroy(app); - if (printf("%s %s: core startup/shutdown OK\n", - XTC_PROJECT_NAME, XTC_VERSION_STRING) < 0) { + if (printf("%s %s: core startup/shutdown OK\n", XTC_PROJECT_NAME, XTC_VERSION_STRING) < 0) { return EXIT_FAILURE; } return EXIT_SUCCESS; diff --git a/src/midi/instrument.c b/src/midi/instrument.c index 824fbdb..0540e62 100644 --- a/src/midi/instrument.c +++ b/src/midi/instrument.c @@ -3,10 +3,26 @@ #include #include #include + #define TAU 6.28318530717958647692 -typedef struct { uint64_t start, end, age; unsigned int pitch, velocity, channel; size_t track; } span; -typedef struct { uint64_t time; int change; } endpoint; -typedef struct { unsigned int active, channel, pitch, velocity; uint64_t age; double phase; } voice; + +typedef struct { + uint64_t start, end, age; + unsigned int pitch, velocity, channel; + size_t track; +} span; + +typedef struct { + uint64_t time; + int change; +} endpoint; + +typedef struct { + unsigned int active, channel, pitch, velocity; + uint64_t age; + double phase; +} voice; + struct xtc_midi_plan { xtc_mix_snapshot mix; span notes[XTC_PROJECT_MAX_NOTES]; @@ -15,46 +31,72 @@ struct xtc_midi_plan { double step[128]; uint64_t cutoff[16], next_position, age; }; + xtc_result xtc_midi_decode(const unsigned char *bytes, size_t size, unsigned int offset, xtc_midi_event *out) { xtc_midi_event e; unsigned int command; - if (!bytes || !size || !out) return XTC_ERR_INVALID_ARGUMENT; + if (!bytes || !size || !out) + return XTC_ERR_INVALID_ARGUMENT; command = bytes[0] & 240U; - if (command != 128 && command != 144 && command != 176) return XTC_ERR_UNSUPPORTED; - if (size != 3 || bytes[1] > 127 || bytes[2] > 127) return XTC_ERR_INVALID_ARGUMENT; - if (command == 176 && bytes[1] != 120 && bytes[1] != 123) return XTC_ERR_UNSUPPORTED; - e.offset = offset; e.channel = bytes[0] & 15U; e.pitch = bytes[1]; e.velocity = bytes[2]; - e.type = command == 176 ? XTC_MIDI_ALL_OFF : - command == 128 || !e.velocity ? XTC_MIDI_NOTE_OFF : XTC_MIDI_NOTE_ON; - *out = e; return XTC_OK; + if (command != 128 && command != 144 && command != 176) + return XTC_ERR_UNSUPPORTED; + if (size != 3 || bytes[1] > 127 || bytes[2] > 127) + return XTC_ERR_INVALID_ARGUMENT; + if (command == 176 && bytes[1] != 120 && bytes[1] != 123) + return XTC_ERR_UNSUPPORTED; + e.offset = offset; + e.channel = bytes[0] & 15U; + e.pitch = bytes[1]; + e.velocity = bytes[2]; + e.type = command == 176 ? XTC_MIDI_ALL_OFF : command == 128 || !e.velocity ? XTC_MIDI_NOTE_OFF : XTC_MIDI_NOTE_ON; + *out = e; + return XTC_OK; } + static int compare_endpoint(const void *left, const void *right) { const endpoint *a = left, *b = right; - if (a->time != b->time) return a->time < b->time ? -1 : 1; + if (a->time != b->time) + return a->time < b->time ? -1 : 1; return a->change - b->change; /* End before start at the same sample. */ } + void xtc_midi_plan_reset(xtc_midi_plan *p) { - if (!p) return; - memset(p->voices, 0, sizeof(p->voices)); memset(p->cutoff, 0, sizeof(p->cutoff)); - p->next_position = UINT64_MAX; p->age = 0; + if (!p) + return; + memset(p->voices, 0, sizeof(p->voices)); + memset(p->cutoff, 0, sizeof(p->cutoff)); + p->next_position = UINT64_MAX; + p->age = 0; } + xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int rate, xtc_midi_plan **out) { xtc_midi_plan *p; endpoint *points; size_t i; int active = 0; - if (!out) return XTC_ERR_INVALID_ARGUMENT; + if (!out) + return XTC_ERR_INVALID_ARGUMENT; *out = NULL; - if (!project || !rate || rate > 768000) return XTC_ERR_INVALID_ARGUMENT; - p = calloc(1, sizeof(*p)); points = calloc(XTC_PROJECT_MAX_NOTES * 2U, sizeof(*points)); - if (!p || !points) { free(p); free(points); return XTC_ERR_OUT_OF_MEMORY; } + if (!project || !rate || rate > 768000) + return XTC_ERR_INVALID_ARGUMENT; + p = calloc(1, sizeof(*p)); + points = calloc(XTC_PROJECT_MAX_NOTES * 2U, sizeof(*points)); + if (!p || !points) { + free(p); + free(points); + return XTC_ERR_OUT_OF_MEMORY; + } { xtc_result r = xtc_mix_prepare(project, rate, &p->mix); - if (r != XTC_OK) { free(points); free(p); return r; } + if (r != XTC_OK) { + free(points); + free(p); + return r; + } } for (i = 0; i < 128; ++i) { double frequency = 440.0 * pow(2.0, ((double)i - 69.0) / 12.0); @@ -66,68 +108,116 @@ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int rate, x uint64_t begin, end; span *s = p->notes + p->count; xtc_result r = xtc_project_find_clip(project, n->clip_id, &c); - if (r != XTC_OK) { free(points); free(p); return XTC_ERR_INVALID_STATE; } + if (r != XTC_OK) { + free(points); + free(p); + return XTC_ERR_INVALID_STATE; + } begin = n->start_tick > c.offset_tick ? n->start_tick : c.offset_tick; - end = n->start_tick + n->length_tick < c.offset_tick + c.length_tick ? - n->start_tick + n->length_tick : c.offset_tick + c.length_tick; - if (end <= begin) continue; + end = n->start_tick + n->length_tick < c.offset_tick + c.length_tick ? n->start_tick + n->length_tick + : c.offset_tick + c.length_tick; + if (end <= begin) + continue; r = xtc_project_tick_to_sample(project, c.start_tick + begin - c.offset_tick, rate, &s->start); - if (r == XTC_OK) r = xtc_project_tick_to_sample(project, c.start_tick + end - c.offset_tick, rate, &s->end); - if (r == XTC_OK) r = xtc_project_tick_to_sample(project, begin - n->start_tick, rate, &s->age); - if (r != XTC_OK) { free(points); free(p); return r; } - if (s->end <= s->start) continue; + if (r == XTC_OK) + r = xtc_project_tick_to_sample(project, c.start_tick + end - c.offset_tick, rate, &s->end); + if (r == XTC_OK) + r = xtc_project_tick_to_sample(project, begin - n->start_tick, rate, &s->age); + if (r != XTC_OK) { + free(points); + free(p); + return r; + } + if (s->end <= s->start) + continue; s->track = xtc_mix_track(&p->mix, c.track_id); - s->pitch = n->pitch; s->velocity = n->velocity; s->channel = n->channel; - points[p->count * 2].time = s->start; points[p->count * 2].change = 1; - points[p->count * 2 + 1].time = s->end; points[p->count * 2 + 1].change = -1; + s->pitch = n->pitch; + s->velocity = n->velocity; + s->channel = n->channel; + points[p->count * 2].time = s->start; + points[p->count * 2].change = 1; + points[p->count * 2 + 1].time = s->end; + points[p->count * 2 + 1].change = -1; ++p->count; } qsort(points, p->count * 2, sizeof(*points), compare_endpoint); for (i = 0; i < p->count * 2; ++i) { active += points[i].change; - if (active > (int)XTC_MIDI_VOICES) { free(points); free(p); return XTC_ERR_CAPACITY; } + if (active > (int)XTC_MIDI_VOICES) { + free(points); + free(p); + return XTC_ERR_CAPACITY; + } } - free(points); xtc_midi_plan_reset(p); *out = p; return XTC_OK; + free(points); + xtc_midi_plan_reset(p); + *out = p; + return XTC_OK; } -void xtc_midi_plan_destroy(xtc_midi_plan *p) { free(p); } + +void xtc_midi_plan_destroy(xtc_midi_plan *p) +{ + free(p); +} + static void channel_off(xtc_midi_plan *p, unsigned int channel, uint64_t position) { size_t i; for (i = 0; i < XTC_MIDI_VOICES; ++i) - if (p->voices[i].channel == channel) p->voices[i].active = 0; + if (p->voices[i].channel == channel) + p->voices[i].active = 0; p->cutoff[channel] = position == UINT64_MAX ? position : position + 1; } + void xtc_midi_plan_panic(xtc_midi_plan *p, uint64_t position) { unsigned int c; - if (!p) return; - for (c = 0; c < 16; ++c) channel_off(p, c, position); + if (!p) + return; + for (c = 0; c < 16; ++c) + channel_off(p, c, position); p->next_position = position; } + static void event(xtc_midi_plan *p, const xtc_midi_event *e, uint64_t position) { size_t i, slot = SIZE_MAX; - if (e->type == XTC_MIDI_ALL_OFF) { channel_off(p, e->channel, position); return; } + if (e->type == XTC_MIDI_ALL_OFF) { + channel_off(p, e->channel, position); + return; + } for (i = 0; i < XTC_MIDI_VOICES; ++i) { if (p->voices[i].active && p->voices[i].channel == e->channel && p->voices[i].pitch == e->pitch) { - slot = i; break; + slot = i; + break; } } if (e->type == XTC_MIDI_NOTE_OFF) { - if (slot != SIZE_MAX) p->voices[slot].active = 0; + if (slot != SIZE_MAX) + p->voices[slot].active = 0; return; } if (slot == SIZE_MAX) { - for (i = 0; i < XTC_MIDI_VOICES; ++i) if (!p->voices[i].active) { slot = i; break; } + for (i = 0; i < XTC_MIDI_VOICES; ++i) + if (!p->voices[i].active) { + slot = i; + break; + } } if (slot == SIZE_MAX) { slot = 0; - for (i = 1; i < XTC_MIDI_VOICES; ++i) if (p->voices[i].age < p->voices[slot].age) slot = i; + for (i = 1; i < XTC_MIDI_VOICES; ++i) + if (p->voices[i].age < p->voices[slot].age) + slot = i; } - p->voices[slot].active = 1; p->voices[slot].channel = e->channel; - p->voices[slot].pitch = e->pitch; p->voices[slot].velocity = e->velocity; - p->voices[slot].phase = 0; p->voices[slot].age = p->age++; + p->voices[slot].active = 1; + p->voices[slot].channel = e->channel; + p->voices[slot].pitch = e->pitch; + p->voices[slot].velocity = e->velocity; + p->voices[slot].phase = 0; + p->voices[slot].age = p->age++; } + static void segment(xtc_midi_plan *p, uint64_t position, float *out, float *right, size_t frames) { size_t i, j; @@ -136,51 +226,67 @@ static void segment(xtc_midi_plan *p, uint64_t position, float *out, float *righ uint64_t begin = position > s->start ? position : s->start; uint64_t end = position + frames < s->end ? position + frames : s->end; double step = p->step[s->pitch]; - if (step == 0.0 || s->start < p->cutoff[s->channel] || end <= begin) continue; + if (step == 0.0 || s->start < p->cutoff[s->channel] || end <= begin) + continue; for (j = (size_t)(begin - position); j < (size_t)(end - position); ++j) { double phase = fmod((double)(position + j - s->start + s->age) * step, 1.0); float sample = (float)(sin(TAU * phase) * 0.1 * s->velocity / 127.0); out[j] += sample * xtc_mix_level(&p->mix, s->track, position + j, right ? 0U : 2U); - if (right) right[j] += sample * xtc_mix_level(&p->mix, s->track, position + j, 1); + if (right) + right[j] += sample * xtc_mix_level(&p->mix, s->track, position + j, 1); } } for (i = 0; i < XTC_MIDI_VOICES; ++i) { voice *v = p->voices + i; double step = p->step[v->pitch]; - if (!v->active || step == 0.0) continue; + if (!v->active || step == 0.0) + continue; for (j = 0; j < frames; ++j) { float sample = (float)(sin(TAU * v->phase) * 0.1 * v->velocity / 127.0); - out[j] += sample; if (right) right[j] += sample; - v->phase += step; if (v->phase >= 1) v->phase -= 1; + out[j] += sample; + if (right) + right[j] += sample; + v->phase += step; + if (v->phase >= 1) + v->phase -= 1; } } } -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) + +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) { size_t i, cursor = 0; - if (!p || !out || (right && right == out) || count > XTC_MIDI_MAX_EVENTS || (count && !events) || - frames > 65536U || position > UINT64_MAX - frames) return XTC_ERR_INVALID_ARGUMENT; + if (!p || !out || (right && right == out) || count > XTC_MIDI_MAX_EVENTS || (count && !events) || frames > 65536U || + position > UINT64_MAX - frames) + return XTC_ERR_INVALID_ARGUMENT; for (i = 0; i < count; ++i) { const xtc_midi_event *e = events + i; - if (e->offset >= frames || (i && e->offset < events[i - 1].offset) || - e->type < XTC_MIDI_NOTE_ON || e->type > XTC_MIDI_ALL_OFF || - e->pitch > 127 || e->velocity > 127 || e->channel > 15 || - (e->type == XTC_MIDI_NOTE_ON && !e->velocity)) return XTC_ERR_INVALID_ARGUMENT; + if (e->offset >= frames || (i && e->offset < events[i - 1].offset) || e->type < XTC_MIDI_NOTE_ON || + e->type > XTC_MIDI_ALL_OFF || e->pitch > 127 || e->velocity > 127 || e->channel > 15 || + (e->type == XTC_MIDI_NOTE_ON && !e->velocity)) + return XTC_ERR_INVALID_ARGUMENT; } - if (!frames) return XTC_OK; - if (p->next_position != position) xtc_midi_plan_reset(p); + if (!frames) + return XTC_OK; + if (p->next_position != position) + xtc_midi_plan_reset(p); memset(out, 0, frames * sizeof(*out)); - if (right) memset(right, 0, frames * sizeof(*right)); + if (right) + memset(right, 0, frames * sizeof(*right)); for (i = 0; i < count; ++i) { size_t next = events[i].offset; segment(p, position + cursor, out + cursor, right ? right + cursor : NULL, next - cursor); - event(p, events + i, position + next); cursor = next; + event(p, events + i, position + next); + cursor = next; } segment(p, position + cursor, out + cursor, right ? right + cursor : NULL, frames - cursor); p->next_position = position + frames; return XTC_OK; } -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) -{ return xtc_midi_plan_render_stereo(p, position, events, count, out, NULL, frames); } +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) +{ + return xtc_midi_plan_render_stereo(p, position, events, count, out, NULL, frames); +} diff --git a/src/midi/model.c b/src/midi/model.c index 2ffecd3..ec3cfa5 100644 --- a/src/midi/model.c +++ b/src/midi/model.c @@ -1,88 +1,137 @@ #include "../project/project_internal.h" #include "../graph/graph_internal.h" #include + 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; xtc_result r; size_t i, source = SIZE_MAX, output = SIZE_MAX; uint64_t id; - if (!p || !out) return XTC_ERR_INVALID_ARGUMENT; + if (!p || !out) + return XTC_ERR_INVALID_ARGUMENT; r = xtc_project_clone(p, &c); - if (r == XTC_OK) r = xtc_project_add_clip(c, track, "MIDI", start, length, &id); + if (r == XTC_OK) + r = xtc_project_add_clip(c, track, "MIDI", start, length, &id); if (r == XTC_OK) { c->clips[c->clip_count - 1].midi = 1; for (i = 0; i < c->graph->node_count; ++i) { xtc_node *n = c->graph->nodes + i; - if (n->type == XTC_NODE_INSTRUMENT) source = n->id; - if (n->type == XTC_NODE_OUTPUT) output = n->id; + if (n->type == XTC_NODE_INSTRUMENT) + source = n->id; + if (n->type == XTC_NODE_OUTPUT) + output = n->id; } if (source == SIZE_MAX) { r = xtc_graph_add_instrument(c->graph, &source); - if (r == XTC_OK && output == SIZE_MAX) r = xtc_graph_add_output(c->graph, &output); - if (r == XTC_OK) r = xtc_graph_connect(c->graph, source, output); + if (r == XTC_OK && output == SIZE_MAX) + r = xtc_graph_add_output(c->graph, &output); + if (r == XTC_OK) + r = xtc_graph_connect(c->graph, source, output); } } if (r == XTC_OK) { - xtc_graph *old = p->graph; *p = *c; c->graph = old; *out = id; + xtc_graph *old = p->graph; + *p = *c; + c->graph = old; + *out = id; } - xtc_project_destroy(c); return r; + xtc_project_destroy(c); + return r; } + xtc_result xtc_project_note_count(const xtc_project *p, size_t *out) { - if (!p || !out) return XTC_ERR_INVALID_ARGUMENT; - *out = p->note_count; return XTC_OK; -} -xtc_result xtc_project_get_note(const xtc_project *p, size_t i, xtc_note_info *out) -{ - if (!p || !out) return XTC_ERR_INVALID_ARGUMENT; - if (i >= p->note_count) return XTC_ERR_NOT_FOUND; - *out = p->notes[i]; return XTC_OK; -} -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) -{ - xtc_clip_info c; - if (pitch > 127 || !velocity || velocity > 127 || channel > 15 || !length) return XTC_ERR_INVALID_ARGUMENT; - if (xtc_project_find_clip(p, clip, &c) != XTC_OK) return XTC_ERR_NOT_FOUND; - if (!c.midi) return XTC_ERR_INVALID_STATE; - if (start >= c.source_length_tick || length > c.source_length_tick - start) return XTC_ERR_INVALID_ARGUMENT; + if (!p || !out) + return XTC_ERR_INVALID_ARGUMENT; + *out = p->note_count; return XTC_OK; } -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) + +xtc_result xtc_project_get_note(const xtc_project *p, size_t i, xtc_note_info *out) +{ + if (!p || !out) + return XTC_ERR_INVALID_ARGUMENT; + if (i >= p->note_count) + return XTC_ERR_NOT_FOUND; + *out = p->notes[i]; + return XTC_OK; +} + +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) +{ + xtc_clip_info c; + if (pitch > 127 || !velocity || velocity > 127 || channel > 15 || !length) + return XTC_ERR_INVALID_ARGUMENT; + if (xtc_project_find_clip(p, clip, &c) != XTC_OK) + return XTC_ERR_NOT_FOUND; + if (!c.midi) + return XTC_ERR_INVALID_STATE; + if (start >= c.source_length_tick || length > c.source_length_tick - start) + return XTC_ERR_INVALID_ARGUMENT; + return XTC_OK; +} + +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) { xtc_result r; xtc_note_info n; - if (!p || !out) return XTC_ERR_INVALID_ARGUMENT; + if (!p || !out) + return XTC_ERR_INVALID_ARGUMENT; r = valid_note(p, clip, start, length, pitch, velocity, channel); - if (r != XTC_OK) return r; - if (p->note_count == XTC_PROJECT_MAX_NOTES || p->next_note_id == UINT64_MAX) return XTC_ERR_CAPACITY; + if (r != XTC_OK) + return r; + if (p->note_count == XTC_PROJECT_MAX_NOTES || p->next_note_id == UINT64_MAX) + return XTC_ERR_CAPACITY; memset(&n, 0, sizeof(n)); - n.id = p->next_note_id++; n.clip_id = clip; n.start_tick = start; n.length_tick = length; - n.pitch = pitch; n.velocity = velocity; n.channel = channel; - p->notes[p->note_count++] = n; *out = n.id; return XTC_OK; + n.id = p->next_note_id++; + n.clip_id = clip; + n.start_tick = start; + n.length_tick = length; + n.pitch = pitch; + n.velocity = velocity; + n.channel = channel; + p->notes[p->note_count++] = n; + *out = n.id; + return XTC_OK; } -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) + +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) { size_t i; xtc_result r; - if (!p) return XTC_ERR_INVALID_ARGUMENT; - for (i = 0; i < p->note_count; ++i) if (p->notes[i].id == id) break; - if (i == p->note_count) return XTC_ERR_NOT_FOUND; + if (!p) + return XTC_ERR_INVALID_ARGUMENT; + for (i = 0; i < p->note_count; ++i) + if (p->notes[i].id == id) + break; + if (i == p->note_count) + return XTC_ERR_NOT_FOUND; r = valid_note(p, p->notes[i].clip_id, start, length, pitch, velocity, channel); - if (r != XTC_OK) return r; - p->notes[i].start_tick = start; p->notes[i].length_tick = length; - p->notes[i].pitch = pitch; p->notes[i].velocity = velocity; p->notes[i].channel = channel; + if (r != XTC_OK) + return r; + p->notes[i].start_tick = start; + p->notes[i].length_tick = length; + p->notes[i].pitch = pitch; + p->notes[i].velocity = velocity; + p->notes[i].channel = channel; return XTC_OK; } + xtc_result xtc_project_remove_note(xtc_project *p, uint64_t id) { size_t i; - if (!p) return XTC_ERR_INVALID_ARGUMENT; - for (i = 0; i < p->note_count; ++i) if (p->notes[i].id == id) break; - if (i == p->note_count) return XTC_ERR_NOT_FOUND; + if (!p) + return XTC_ERR_INVALID_ARGUMENT; + for (i = 0; i < p->note_count; ++i) + if (p->notes[i].id == id) + break; + if (i == p->note_count) + return XTC_ERR_NOT_FOUND; memmove(p->notes + i, p->notes + i + 1, (p->note_count - i - 1) * sizeof(*p->notes)); - --p->note_count; return XTC_OK; + --p->note_count; + return XTC_OK; } diff --git a/src/mixer/mix_internal.h b/src/mixer/mix_internal.h index 0b0f9df..104f6fd 100644 --- a/src/mixer/mix_internal.h +++ b/src/mixer/mix_internal.h @@ -1,18 +1,22 @@ #ifndef XTC_MIX_INTERNAL_H #define XTC_MIX_INTERNAL_H + #include "../project/project_internal.h" + typedef struct { uint64_t sample, tick; size_t track; unsigned int parameter; float value; } xtc_mix_point; + typedef struct { xtc_track_info tracks[XTC_PROJECT_MAX_TRACKS]; size_t count, offsets[XTC_PROJECT_MAX_TRACKS][2], lengths[XTC_PROJECT_MAX_TRACKS][2]; xtc_mix_point points[XTC_MAX_AUTOMATION_POINTS]; 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); /* side 0/1 stereo, 2 mono (gain only). */ diff --git a/src/mixer/model.c b/src/mixer/model.c index a73966f..41d5646 100644 --- a/src/mixer/model.c +++ b/src/mixer/model.c @@ -1,6 +1,7 @@ #include "../project/project_internal.h" #include #include + static size_t find_track(const xtc_project *p, uint64_t id) { size_t i; @@ -9,8 +10,8 @@ static size_t find_track(const xtc_project *p, uint64_t id) return i; return SIZE_MAX; } -xtc_result xtc_project_set_mix(xtc_project *p, uint64_t id, float gain, float pan, int mute, - int solo) + +xtc_result xtc_project_set_mix(xtc_project *p, uint64_t id, float gain, float pan, int mute, int solo) { size_t i; if (!p || !isfinite(gain) || gain < 0 || gain > 4 || !isfinite(pan) || pan < -1 || pan > 1 || @@ -25,6 +26,7 @@ xtc_result xtc_project_set_mix(xtc_project *p, uint64_t id, float gain, float pa p->tracks[i].solo = solo; return XTC_OK; } + xtc_result xtc_project_set_automation(xtc_project *p, const xtc_automation_point *a) { size_t i; @@ -36,8 +38,8 @@ xtc_result xtc_project_set_automation(xtc_project *p, const xtc_automation_point if (find_track(p, a->track_id) == SIZE_MAX) return XTC_ERR_NOT_FOUND; for (i = 0; i < p->automation_count; ++i) - if (p->automation[i].track_id == a->track_id && - p->automation[i].parameter == a->parameter && p->automation[i].tick == a->tick) + if (p->automation[i].track_id == a->track_id && p->automation[i].parameter == a->parameter && + p->automation[i].tick == a->tick) break; if (i == XTC_MAX_AUTOMATION_POINTS) return XTC_ERR_CAPACITY; @@ -46,23 +48,21 @@ xtc_result xtc_project_set_automation(xtc_project *p, const xtc_automation_point p->automation[i] = *a; return XTC_OK; } -xtc_result xtc_project_remove_automation(xtc_project *p, uint64_t id, - xtc_automation_parameter param, uint64_t tick) + +xtc_result xtc_project_remove_automation(xtc_project *p, uint64_t id, xtc_automation_parameter param, uint64_t tick) { size_t i; - if (!p || (param != XTC_AUTOMATION_GAIN && param != XTC_AUTOMATION_PAN) || - tick > XTC_PROJECT_MAX_TICK) + if (!p || (param != XTC_AUTOMATION_GAIN && param != XTC_AUTOMATION_PAN) || tick > XTC_PROJECT_MAX_TICK) return XTC_ERR_INVALID_ARGUMENT; for (i = 0; i < p->automation_count; ++i) - if (p->automation[i].track_id == id && p->automation[i].parameter == param && - p->automation[i].tick == tick) + if (p->automation[i].track_id == id && p->automation[i].parameter == param && p->automation[i].tick == tick) break; if (i == p->automation_count) return XTC_ERR_NOT_FOUND; - memmove(p->automation + i, p->automation + i + 1, - (--p->automation_count - i) * sizeof(*p->automation)); + memmove(p->automation + i, p->automation + i + 1, (--p->automation_count - i) * sizeof(*p->automation)); return XTC_OK; } + xtc_result xtc_project_automation_count(const xtc_project *p, size_t *n) { if (!p || !n) @@ -70,6 +70,7 @@ xtc_result xtc_project_automation_count(const xtc_project *p, size_t *n) *n = p->automation_count; return XTC_OK; } + 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 015490c..3e9a561 100644 --- a/src/mixer/render.c +++ b/src/mixer/render.c @@ -1,6 +1,7 @@ #include "mix_internal.h" #include #include + size_t xtc_mix_track(const xtc_mix_snapshot *m, uint64_t id) { size_t i; @@ -9,6 +10,7 @@ size_t xtc_mix_track(const xtc_mix_snapshot *m, uint64_t id) return i; return SIZE_MAX; } + static int compare(const void *left, const void *right) { const xtc_mix_point *a = left, *b = right; @@ -18,6 +20,7 @@ static int compare(const void *left, const void *right) return a->parameter < b->parameter ? -1 : 1; return a->tick < b->tick ? -1 : a->tick != b->tick; } + xtc_result xtc_mix_prepare(const xtc_project *p, unsigned int rate, xtc_mix_snapshot *m) { size_t i; @@ -51,6 +54,7 @@ xtc_result xtc_mix_prepare(const xtc_project *p, unsigned int rate, xtc_mix_snap } return XTC_OK; } + 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; @@ -72,6 +76,7 @@ static float value(const xtc_mix_snapshot *m, size_t track, unsigned int param, (double)(p[low].sample - p[low - 1].sample) * (p[low].value - p[low - 1].value)); } + 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 079d923..6f0cafd 100644 --- a/src/native/native.c +++ b/src/native/native.c @@ -1,12 +1,14 @@ #include "xtc/xtc_native.h" #include #include + struct xtc_native { xtc_native_descriptor descriptor; void *state; size_t max_frames; uint64_t next_position; }; + xtc_result xtc_native_validate(const xtc_native_descriptor *d) { size_t i; @@ -14,8 +16,8 @@ xtc_result xtc_native_validate(const xtc_native_descriptor *d) return XTC_ERR_INVALID_ARGUMENT; if (d->abi_version != XTC_NATIVE_ABI_VERSION || d->struct_size != sizeof(*d)) return XTC_ERR_UNSUPPORTED; - if (d->kind < XTC_NATIVE_EFFECT || d->kind > XTC_NATIVE_INSTRUMENT || !d->prepare || - !d->reset || !d->process || !d->destroy || !d->identifier[0]) + if (d->kind < XTC_NATIVE_EFFECT || d->kind > XTC_NATIVE_INSTRUMENT || !d->prepare || !d->reset || !d->process || + !d->destroy || !d->identifier[0]) return XTC_ERR_INVALID_ARGUMENT; for (i = 0; i < sizeof(d->identifier); ++i) { unsigned char c = (unsigned char)d->identifier[i]; @@ -26,8 +28,8 @@ xtc_result xtc_native_validate(const xtc_native_descriptor *d) } return XTC_ERR_INVALID_ARGUMENT; } -xtc_result xtc_native_create(const xtc_native_descriptor *d, double rate, size_t frames, - float value, xtc_native **out) + +xtc_result xtc_native_create(const xtc_native_descriptor *d, double rate, size_t frames, float value, xtc_native **out) { xtc_native *p; xtc_result r; @@ -37,8 +39,7 @@ xtc_result xtc_native_create(const xtc_native_descriptor *d, double rate, size_t r = xtc_native_validate(d); if (r != XTC_OK) return r; - if (!isfinite(rate) || rate <= 0 || rate > 768000 || !frames || frames > 65536 || - !isfinite(value)) + if (!isfinite(rate) || rate <= 0 || rate > 768000 || !frames || frames > 65536 || !isfinite(value)) return XTC_ERR_INVALID_ARGUMENT; p = calloc(1, sizeof(*p)); if (!p) @@ -55,6 +56,7 @@ xtc_result xtc_native_create(const xtc_native_descriptor *d, double rate, size_t *out = p; return XTC_OK; } + void xtc_native_destroy(xtc_native *p) { if (p) { @@ -62,6 +64,7 @@ void xtc_native_destroy(xtc_native *p) free(p); } } + void xtc_native_reset(xtc_native *p) { if (p) { @@ -69,21 +72,21 @@ void xtc_native_reset(xtc_native *p) p->next_position = UINT64_MAX; } } + xtc_result xtc_native_process(xtc_native *p, const xtc_native_block *b) { size_t i; xtc_result r; - if (!p || !b || !b->output || b->position > UINT64_MAX - b->frames || - b->event_count > XTC_MIDI_MAX_EVENTS || (b->event_count && !b->events) || - (b->event_count && p->descriptor.kind != XTC_NATIVE_INSTRUMENT)) + if (!p || !b || !b->output || b->position > UINT64_MAX - b->frames || b->event_count > XTC_MIDI_MAX_EVENTS || + (b->event_count && !b->events) || (b->event_count && p->descriptor.kind != XTC_NATIVE_INSTRUMENT)) return XTC_ERR_INVALID_ARGUMENT; if (b->frames > p->max_frames) return XTC_ERR_CAPACITY; for (i = 0; i < b->event_count; ++i) { const xtc_midi_event *e = b->events + i; - if (e->offset >= b->frames || (i && e->offset < b->events[i - 1].offset) || - e->type < XTC_MIDI_NOTE_ON || e->type > XTC_MIDI_ALL_OFF || e->channel > 15 || - e->pitch > 127 || e->velocity > 127 || (e->type == XTC_MIDI_NOTE_ON && !e->velocity)) + if (e->offset >= b->frames || (i && e->offset < b->events[i - 1].offset) || e->type < XTC_MIDI_NOTE_ON || + e->type > XTC_MIDI_ALL_OFF || e->channel > 15 || e->pitch > 127 || e->velocity > 127 || + (e->type == XTC_MIDI_NOTE_ON && !e->velocity)) return XTC_ERR_INVALID_ARGUMENT; } if (!b->frames) diff --git a/src/persistence/project_file.c b/src/persistence/project_file.c index 9b3cc6b..e55a261 100644 --- a/src/persistence/project_file.c +++ b/src/persistence/project_file.c @@ -11,7 +11,14 @@ #include #include -typedef struct { FILE *file; size_t bytes; char line[1536]; char *tokens[12]; size_t count; } reader; +typedef struct { + FILE *file; + size_t bytes; + char line[1536]; + char *tokens[12]; + size_t count; +} reader; + static xtc_result line(reader *input) { size_t used = 0U; @@ -19,266 +26,416 @@ static xtc_result line(reader *input) char *cursor; input->count = 0U; while ((byte = fgetc(input->file)) != EOF) { - if (++input->bytes > 4194304U) return XTC_ERR_FORMAT; - if (byte == '\n') break; - if (byte < 32 || byte > 126 || used == sizeof(input->line) - 1U) return XTC_ERR_FORMAT; + if (++input->bytes > 4194304U) + return XTC_ERR_FORMAT; + if (byte == '\n') + break; + if (byte < 32 || byte > 126 || used == sizeof(input->line) - 1U) + return XTC_ERR_FORMAT; input->line[used++] = (char)byte; } - if (byte == EOF) return ferror(input->file) ? XTC_ERR_IO : XTC_ERR_FORMAT; - input->line[used] = '\0'; cursor = input->line; + if (byte == EOF) + return ferror(input->file) ? XTC_ERR_IO : XTC_ERR_FORMAT; + input->line[used] = '\0'; + cursor = input->line; while (*cursor != '\0') { - while (*cursor == ' ') ++cursor; - if (*cursor == '\0') break; - if (input->count == 12U) return XTC_ERR_FORMAT; + while (*cursor == ' ') + ++cursor; + if (*cursor == '\0') + break; + if (input->count == 12U) + return XTC_ERR_FORMAT; input->tokens[input->count++] = cursor; - while (*cursor != '\0' && *cursor != ' ') ++cursor; - if (*cursor == ' ') *cursor++ = '\0'; + while (*cursor != '\0' && *cursor != ' ') + ++cursor; + if (*cursor == ' ') + *cursor++ = '\0'; } return input->count == 0U ? XTC_ERR_FORMAT : XTC_OK; } + static int number(const char *text, uint64_t *value) { uint64_t result = 0U; const unsigned char *cursor = (const unsigned char *)text; - if (*cursor == '\0') return 0; + if (*cursor == '\0') + return 0; while (*cursor != '\0') { unsigned int digit; - if (*cursor < '0' || *cursor > '9') return 0; + if (*cursor < '0' || *cursor > '9') + return 0; digit = (unsigned int)(*cursor++ - '0'); - if (result > (UINT64_MAX - digit) / 10U) return 0; + if (result > (UINT64_MAX - digit) / 10U) + return 0; result = result * 10U + digit; } - *value = result; return 1; + *value = result; + return 1; } + static int hex_digit(char byte) { - if (byte >= '0' && byte <= '9') return byte - '0'; - if (byte >= 'a' && byte <= 'f') return byte - 'a' + 10; - if (byte >= 'A' && byte <= 'F') return byte - 'A' + 10; + if (byte >= '0' && byte <= '9') + return byte - '0'; + if (byte >= 'a' && byte <= 'f') + return byte - 'a' + 10; + if (byte >= 'A' && byte <= 'F') + return byte - 'A' + 10; return -1; } + static int decode_name(const char *text, char *name) { size_t length = strlen(text), i; - if (length == 0U || length % 2U != 0U || length > 126U) return 0; + if (length == 0U || length % 2U != 0U || length > 126U) + return 0; for (i = 0U; i < length / 2U; ++i) { int high = hex_digit(text[i * 2U]), low = hex_digit(text[i * 2U + 1U]); - if (high < 0 || low < 0 || (high == 0 && low == 0)) return 0; + if (high < 0 || low < 0 || (high == 0 && low == 0)) + return 0; name[i] = (char)(high * 16 + low); } - name[length / 2U] = '\0'; return xtc_project_valid_name(name); + name[length / 2U] = '\0'; + return xtc_project_valid_name(name); } + static int decode_float(const char *text, float *value) { uint32_t bits = 0U; size_t i; - if (strlen(text) != 8U || (sizeof(*value) != sizeof(bits) || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128)) return 0; + if (strlen(text) != 8U || + (sizeof(*value) != sizeof(bits) || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128)) + return 0; for (i = 0U; i < 8U; ++i) { int digit = hex_digit(text[i]); - if (digit < 0) return 0; + if (digit < 0) + return 0; bits = (bits << 4U) | (uint32_t)digit; } - memcpy(value, &bits, sizeof(bits)); return isfinite(*value); + memcpy(value, &bits, sizeof(bits)); + return isfinite(*value); } -#define READ(tag, total) do { xtc_result read_result = line(input); if (read_result != XTC_OK) return read_result; \ - if (input->count != (total) || strcmp(input->tokens[0], (tag)) != 0) return XTC_ERR_FORMAT; } while (0) -#define NUM(index, variable) do { if (!number(input->tokens[(index)], &(variable))) return XTC_ERR_FORMAT; } while (0) + +#define READ(tag, total) \ + do { \ + xtc_result read_result = line(input); \ + if (read_result != XTC_OK) \ + return read_result; \ + if (input->count != (total) || strcmp(input->tokens[0], (tag)) != 0) \ + return XTC_ERR_FORMAT; \ + } while (0) + +#define NUM(index, variable) \ + do { \ + if (!number(input->tokens[(index)], &(variable))) \ + return XTC_ERR_FORMAT; \ + } while (0) + static xtc_result read_project(reader *input, xtc_project *project) { uint64_t version, value, next_id, node_count, edge_count, format_version; size_t i, j; xtc_graph *graph = project->graph; - READ("XTC_PROJECT", 2U); NUM(1U, version); - if (version < 1U || version > 4U) return XTC_ERR_UNSUPPORTED; + READ("XTC_PROJECT", 2U); + NUM(1U, version); + if (version < 1U || version > 4U) + return XTC_ERR_UNSUPPORTED; format_version = version; READ("APP", 4U); - for (i = 1U; i < 4U; ++i) { NUM(i, value); if (value > UINT_MAX) return XTC_ERR_FORMAT; } - READ("TEMPO", 2U); NUM(1U, value); - if (value < 20000U || value > 300000U) return XTC_ERR_FORMAT; + for (i = 1U; i < 4U; ++i) { + NUM(i, value); + if (value > UINT_MAX) + return XTC_ERR_FORMAT; + } + READ("TEMPO", 2U); + NUM(1U, value); + if (value < 20000U || value > 300000U) + return XTC_ERR_FORMAT; project->tempo = (unsigned int)value; - READ("IDS", 3U); NUM(1U, project->next_track_id); NUM(2U, project->next_clip_id); + READ("IDS", 3U); + NUM(1U, project->next_track_id); + NUM(2U, project->next_clip_id); READ("GRAPH", 5U); - NUM(1U, value); NUM(2U, next_id); NUM(3U, node_count); NUM(4U, edge_count); - if (value > 1U || next_id > SIZE_MAX || node_count > 64U || edge_count > node_count * node_count) return XTC_ERR_FORMAT; - graph->require_output_nodes = (int)value; graph->next_id = (size_t)next_id; + NUM(1U, value); + NUM(2U, next_id); + NUM(3U, node_count); + NUM(4U, edge_count); + if (value > 1U || next_id > SIZE_MAX || node_count > 64U || edge_count > node_count * node_count) + return XTC_ERR_FORMAT; + graph->require_output_nodes = (int)value; + graph->next_id = (size_t)next_id; for (i = 0U; i < (size_t)node_count; ++i) { xtc_node node = {0}; - READ("NODE", 4U); NUM(1U, value); NUM(2U, version); - if (value >= next_id || value >= SIZE_MAX || version < XTC_NODE_GAIN || version > (format_version == 1U ? XTC_NODE_OUTPUT : format_version == 2U ? XTC_NODE_AUDIO : XTC_NODE_INSTRUMENT) || - !decode_float(input->tokens[3], &node.value)) return XTC_ERR_FORMAT; - node.id = (size_t)value; node.type = (xtc_node_type)version; - if (node.type == XTC_NODE_OSCILLATOR && node.value < 0.0f) return XTC_ERR_FORMAT; - if ((node.type == XTC_NODE_METER || node.type == XTC_NODE_OUTPUT || node.type == XTC_NODE_AUDIO || node.type == XTC_NODE_INSTRUMENT) && node.value != 0.0f) return XTC_ERR_FORMAT; - for (j = 0U; j < i; ++j) if (graph->nodes[j].id == node.id) return XTC_ERR_FORMAT; + READ("NODE", 4U); + NUM(1U, value); + NUM(2U, version); + if (value >= next_id || value >= SIZE_MAX || version < XTC_NODE_GAIN || + version > (format_version == 1U ? XTC_NODE_OUTPUT + : format_version == 2U ? XTC_NODE_AUDIO + : XTC_NODE_INSTRUMENT) || + !decode_float(input->tokens[3], &node.value)) + return XTC_ERR_FORMAT; + node.id = (size_t)value; + node.type = (xtc_node_type)version; + if (node.type == XTC_NODE_OSCILLATOR && node.value < 0.0f) + return XTC_ERR_FORMAT; + if ((node.type == XTC_NODE_METER || node.type == XTC_NODE_OUTPUT || node.type == XTC_NODE_AUDIO || + node.type == XTC_NODE_INSTRUMENT) && + node.value != 0.0f) + return XTC_ERR_FORMAT; + for (j = 0U; j < i; ++j) + if (graph->nodes[j].id == node.id) + return XTC_ERR_FORMAT; graph->nodes[graph->node_count++] = node; } for (i = 0U; i < (size_t)edge_count; ++i) { uint64_t source, dest; - READ("EDGE", 3U); NUM(1U, source); NUM(2U, dest); - if (source >= SIZE_MAX || dest >= SIZE_MAX || - xtc_graph_connect(graph, (size_t)source, (size_t)dest) != XTC_OK) return XTC_ERR_FORMAT; + READ("EDGE", 3U); + NUM(1U, source); + NUM(2U, dest); + if (source >= SIZE_MAX || dest >= SIZE_MAX || xtc_graph_connect(graph, (size_t)source, (size_t)dest) != XTC_OK) + return XTC_ERR_FORMAT; } - READ("TRACKS", 2U); NUM(1U, value); - if (value > XTC_PROJECT_MAX_TRACKS) return XTC_ERR_FORMAT; + READ("TRACKS", 2U); + NUM(1U, value); + if (value > XTC_PROJECT_MAX_TRACKS) + return XTC_ERR_FORMAT; project->track_count = (size_t)value; for (i = 0U; i < project->track_count; ++i) { - READ("TRACK", 3U); NUM(1U, project->tracks[i].id); - if (!decode_name(input->tokens[2], project->tracks[i].name)) return XTC_ERR_FORMAT; + READ("TRACK", 3U); + NUM(1U, project->tracks[i].id); + if (!decode_name(input->tokens[2], project->tracks[i].name)) + return XTC_ERR_FORMAT; project->tracks[i].gain = 1; if (format_version >= 4U) { READ("MIX", 5U); if (!decode_float(input->tokens[1], &project->tracks[i].gain) || - !decode_float(input->tokens[2], &project->tracks[i].pan)) return XTC_ERR_FORMAT; - NUM(3U, value); if (value > 1) return XTC_ERR_FORMAT; project->tracks[i].mute = (int)value; - NUM(4U, value); if (value > 1) return XTC_ERR_FORMAT; project->tracks[i].solo = (int)value; + !decode_float(input->tokens[2], &project->tracks[i].pan)) + return XTC_ERR_FORMAT; + NUM(3U, value); + if (value > 1) + return XTC_ERR_FORMAT; + project->tracks[i].mute = (int)value; + NUM(4U, value); + if (value > 1) + return XTC_ERR_FORMAT; + project->tracks[i].solo = (int)value; } } - READ("CLIPS", 2U); NUM(1U, value); - if (value > XTC_PROJECT_MAX_CLIPS) return XTC_ERR_FORMAT; + READ("CLIPS", 2U); + NUM(1U, value); + if (value > XTC_PROJECT_MAX_CLIPS) + return XTC_ERR_FORMAT; project->clip_count = (size_t)value; for (i = 0U; i < project->clip_count; ++i) { xtc_clip_info *clip = project->clips + i; READ("CLIP", 8U); - NUM(1U, clip->id); NUM(2U, clip->track_id); NUM(3U, clip->start_tick); - NUM(4U, clip->length_tick); NUM(5U, clip->offset_tick); NUM(6U, clip->source_length_tick); - if (!decode_name(input->tokens[7], clip->name)) return XTC_ERR_FORMAT; + NUM(1U, clip->id); + NUM(2U, clip->track_id); + NUM(3U, clip->start_tick); + NUM(4U, clip->length_tick); + NUM(5U, clip->offset_tick); + NUM(6U, clip->source_length_tick); + if (!decode_name(input->tokens[7], clip->name)) + return XTC_ERR_FORMAT; if (format_version >= 2U) { size_t k, length; READ("ASSET", 2U); if (strcmp(input->tokens[1], "-")) { length = strlen(input->tokens[1]); - if (!length || length % 2 || length >= XTC_ASSET_PATH_BYTES * 2U) return XTC_ERR_FORMAT; + if (!length || length % 2 || length >= XTC_ASSET_PATH_BYTES * 2U) + return XTC_ERR_FORMAT; for (k = 0; k < length / 2; ++k) { int high = hex_digit(input->tokens[1][k * 2]), low = hex_digit(input->tokens[1][k * 2 + 1]); - if (high < 0 || low < 0 || (high == 0 && low == 0)) return XTC_ERR_FORMAT; + if (high < 0 || low < 0 || (high == 0 && low == 0)) + return XTC_ERR_FORMAT; clip->asset_path[k] = (char)(high * 16 + low); } clip->asset_path[length / 2] = 0; } } if (format_version >= 3U) { - READ("MIDI", 2U); NUM(1U, value); - if (value > 1) return XTC_ERR_FORMAT; + READ("MIDI", 2U); + NUM(1U, value); + if (value > 1) + return XTC_ERR_FORMAT; clip->midi = (int)value; } } if (format_version >= 3U) { - READ("NOTES", 3U); NUM(1U, project->next_note_id); NUM(2U, value); - if (value > XTC_PROJECT_MAX_NOTES) return XTC_ERR_FORMAT; + READ("NOTES", 3U); + NUM(1U, project->next_note_id); + NUM(2U, value); + if (value > XTC_PROJECT_MAX_NOTES) + return XTC_ERR_FORMAT; project->note_count = (size_t)value; for (i = 0; i < project->note_count; ++i) { xtc_note_info *n = project->notes + i; READ("NOTE", 8U); - NUM(1U, n->id); NUM(2U, n->clip_id); NUM(3U, n->start_tick); NUM(4U, n->length_tick); - NUM(5U, value); if (value > 127) return XTC_ERR_FORMAT; n->pitch = (unsigned int)value; - NUM(6U, value); if (!value || value > 127) return XTC_ERR_FORMAT; n->velocity = (unsigned int)value; - NUM(7U, value); if (value > 15) return XTC_ERR_FORMAT; n->channel = (unsigned int)value; + NUM(1U, n->id); + NUM(2U, n->clip_id); + NUM(3U, n->start_tick); + NUM(4U, n->length_tick); + NUM(5U, value); + if (value > 127) + return XTC_ERR_FORMAT; + n->pitch = (unsigned int)value; + NUM(6U, value); + if (!value || value > 127) + return XTC_ERR_FORMAT; + n->velocity = (unsigned int)value; + NUM(7U, value); + if (value > 15) + return XTC_ERR_FORMAT; + n->channel = (unsigned int)value; } } if (format_version >= 4U) { - READ("AUTOMATION", 2U); NUM(1U, value); - if (value > XTC_MAX_AUTOMATION_POINTS) return XTC_ERR_FORMAT; + READ("AUTOMATION", 2U); + NUM(1U, value); + if (value > XTC_MAX_AUTOMATION_POINTS) + return XTC_ERR_FORMAT; project->automation_count = (size_t)value; for (i = 0; i < project->automation_count; ++i) { xtc_automation_point *a = project->automation + i; - READ("POINT", 5U); NUM(1U, a->track_id); NUM(2U, value); - if (value > 1) return XTC_ERR_FORMAT; + READ("POINT", 5U); + NUM(1U, a->track_id); + NUM(2U, value); + if (value > 1) + return XTC_ERR_FORMAT; a->parameter = (xtc_automation_parameter)value; NUM(3U, a->tick); - if (!decode_float(input->tokens[4], &a->value)) return XTC_ERR_FORMAT; + if (!decode_float(input->tokens[4], &a->value)) + return XTC_ERR_FORMAT; } } READ("END", 1U); - if (fgetc(input->file) != EOF) return XTC_ERR_FORMAT; - if (ferror(input->file)) return XTC_ERR_IO; + if (fgetc(input->file) != EOF) + return XTC_ERR_FORMAT; + if (ferror(input->file)) + return XTC_ERR_IO; return xtc_project_validate(project); } + static int write_name(FILE *file, const char *name) { const unsigned char *cursor = (const unsigned char *)name; - while (*cursor != '\0') if (fprintf(file, "%02x", (unsigned int)*cursor++) < 0) return 0; + while (*cursor != '\0') + if (fprintf(file, "%02x", (unsigned int)*cursor++) < 0) + return 0; return fputc('\n', file) != EOF; } + static xtc_result write_project(FILE *file, const xtc_project *project) { const xtc_graph *graph = project->graph; size_t i; for (i = 0; i < graph->node_count; ++i) - if (graph->nodes[i].type == XTC_NODE_NATIVE) return XTC_ERR_UNSUPPORTED; - if (sizeof(float) != sizeof(uint32_t) || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128) return XTC_ERR_UNSUPPORTED; + if (graph->nodes[i].type == XTC_NODE_NATIVE) + return XTC_ERR_UNSUPPORTED; + if (sizeof(float) != sizeof(uint32_t) || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128) + return XTC_ERR_UNSUPPORTED; if (fprintf(file, "XTC_PROJECT 4\nAPP %d %d %d\nTEMPO %u\nIDS %" PRIu64 " %" PRIu64 "\nGRAPH %d %zu %zu %zu\n", - XTC_VERSION_MAJOR, XTC_VERSION_MINOR, XTC_VERSION_PATCH, project->tempo, - project->next_track_id, project->next_clip_id, graph->require_output_nodes, - graph->next_id, graph->node_count, graph->edge_count) < 0) return XTC_ERR_IO; + XTC_VERSION_MAJOR, XTC_VERSION_MINOR, XTC_VERSION_PATCH, project->tempo, project->next_track_id, + project->next_clip_id, graph->require_output_nodes, graph->next_id, graph->node_count, + graph->edge_count) < 0) + return XTC_ERR_IO; for (i = 0U; i < graph->node_count; ++i) { uint32_t bits; memcpy(&bits, &graph->nodes[i].value, sizeof(bits)); - if (fprintf(file, "NODE %zu %u %08" PRIx32 "\n", graph->nodes[i].id, - (unsigned int)graph->nodes[i].type, bits) < 0) return XTC_ERR_IO; + if (fprintf(file, "NODE %zu %u %08" PRIx32 "\n", graph->nodes[i].id, (unsigned int)graph->nodes[i].type, bits) < + 0) + return XTC_ERR_IO; } for (i = 0U; i < graph->edge_count; ++i) if (fprintf(file, "EDGE %zu %zu\n", graph->nodes[graph->edges[i].source].id, - graph->nodes[graph->edges[i].destination].id) < 0) return XTC_ERR_IO; - if (fprintf(file, "TRACKS %zu\n", project->track_count) < 0) return XTC_ERR_IO; + graph->nodes[graph->edges[i].destination].id) < 0) + return XTC_ERR_IO; + if (fprintf(file, "TRACKS %zu\n", project->track_count) < 0) + return XTC_ERR_IO; for (i = 0U; i < project->track_count; ++i) { uint32_t gain, pan; const xtc_track_info *t = project->tracks + i; - memcpy(&gain, &t->gain, sizeof(gain)); memcpy(&pan, &t->pan, sizeof(pan)); + memcpy(&gain, &t->gain, sizeof(gain)); + memcpy(&pan, &t->pan, sizeof(pan)); if (fprintf(file, "TRACK %" PRIu64 " ", t->id) < 0 || !write_name(file, t->name) || - fprintf(file, "MIX %08" PRIx32 " %08" PRIx32 " %d %d\n", gain, pan, t->mute, t->solo) < 0) return XTC_ERR_IO; + fprintf(file, "MIX %08" PRIx32 " %08" PRIx32 " %d %d\n", gain, pan, t->mute, t->solo) < 0) + return XTC_ERR_IO; } - if (fprintf(file, "CLIPS %zu\n", project->clip_count) < 0) return XTC_ERR_IO; + if (fprintf(file, "CLIPS %zu\n", project->clip_count) < 0) + return XTC_ERR_IO; for (i = 0U; i < project->clip_count; ++i) { const xtc_clip_info *clip = project->clips + i; - if (fprintf(file, "CLIP %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " ", - clip->id, clip->track_id, clip->start_tick, clip->length_tick, clip->offset_tick, clip->source_length_tick) < 0 || - !write_name(file, clip->name)) return XTC_ERR_IO; - if (fputs("ASSET ", file) == EOF) return XTC_ERR_IO; + if (fprintf(file, "CLIP %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " ", clip->id, + clip->track_id, clip->start_tick, clip->length_tick, clip->offset_tick, + clip->source_length_tick) < 0 || + !write_name(file, clip->name)) + return XTC_ERR_IO; + if (fputs("ASSET ", file) == EOF) + return XTC_ERR_IO; if (clip->asset_path[0]) { - if (!write_name(file, clip->asset_path)) return XTC_ERR_IO; - } else if (fputs("-\n", file) == EOF) return XTC_ERR_IO; - if (fprintf(file, "MIDI %d\n", clip->midi) < 0) return XTC_ERR_IO; + if (!write_name(file, clip->asset_path)) + return XTC_ERR_IO; + } else if (fputs("-\n", file) == EOF) + return XTC_ERR_IO; + if (fprintf(file, "MIDI %d\n", clip->midi) < 0) + return XTC_ERR_IO; } - if (fprintf(file, "NOTES %" PRIu64 " %zu\n", project->next_note_id, project->note_count) < 0) return XTC_ERR_IO; + if (fprintf(file, "NOTES %" PRIu64 " %zu\n", project->next_note_id, project->note_count) < 0) + return XTC_ERR_IO; for (i = 0; i < project->note_count; ++i) { const xtc_note_info *n = project->notes + i; - if (fprintf(file, "NOTE %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " %u %u %u\n", - n->id, n->clip_id, n->start_tick, n->length_tick, n->pitch, n->velocity, n->channel) < 0) return XTC_ERR_IO; + if (fprintf(file, "NOTE %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " %u %u %u\n", n->id, n->clip_id, + n->start_tick, n->length_tick, n->pitch, n->velocity, n->channel) < 0) + return XTC_ERR_IO; } - if (fprintf(file, "AUTOMATION %zu\n", project->automation_count) < 0) return XTC_ERR_IO; + if (fprintf(file, "AUTOMATION %zu\n", project->automation_count) < 0) + return XTC_ERR_IO; for (i = 0; i < project->automation_count; ++i) { - uint32_t bits; const xtc_automation_point *a = project->automation + i; + uint32_t bits; + const xtc_automation_point *a = project->automation + i; memcpy(&bits, &a->value, sizeof(bits)); - if (fprintf(file, "POINT %" PRIu64 " %u %" PRIu64 " %08" PRIx32 "\n", - a->track_id, (unsigned int)a->parameter, a->tick, bits) < 0) return XTC_ERR_IO; + if (fprintf(file, "POINT %" PRIu64 " %u %" PRIu64 " %08" PRIx32 "\n", a->track_id, (unsigned int)a->parameter, + a->tick, bits) < 0) + return XTC_ERR_IO; } return fputs("END\n", file) == EOF ? XTC_ERR_IO : XTC_OK; } + static size_t path_length(const char *path) { size_t size = 0U; - if (path == NULL) return 0U; - while (size < 4096U && path[size] != '\0') ++size; + if (path == NULL) + return 0U; + while (size < 4096U && path[size] != '\0') + ++size; return size == 4096U ? 0U : size; } + xtc_result xtc_project_load(xtc_project *project, const char *path) { xtc_project *candidate = NULL; reader input = {0}; xtc_result result; - if (project == NULL || path_length(path) == 0U) return XTC_ERR_INVALID_ARGUMENT; + if (project == NULL || path_length(path) == 0U) + return XTC_ERR_INVALID_ARGUMENT; input.file = fopen(path, "rb"); - if (input.file == NULL) return XTC_ERR_IO; + if (input.file == NULL) + return XTC_ERR_IO; result = xtc_project_create(&candidate); - if (result == XTC_OK) result = read_project(&input, candidate); - if (fclose(input.file) != 0 && result == XTC_OK) result = XTC_ERR_IO; + if (result == XTC_OK) + result = read_project(&input, candidate); + if (fclose(input.file) != 0 && result == XTC_OK) + result = XTC_ERR_IO; if (result == XTC_OK) { xtc_graph *old = project->graph; *project = *candidate; candidate->graph = old; } - xtc_project_destroy(candidate); return result; + xtc_project_destroy(candidate); + return result; } + xtc_result xtc_project_save(const xtc_project *project, const char *path) { size_t length = path_length(path); @@ -286,21 +443,39 @@ xtc_result xtc_project_save(const xtc_project *project, const char *path) FILE *file; int descriptor; xtc_result result; - if (project == NULL || length == 0U) return XTC_ERR_INVALID_ARGUMENT; + if (project == NULL || length == 0U) + return XTC_ERR_INVALID_ARGUMENT; result = xtc_project_validate(project); - if (result != XTC_OK) return result; + if (result != XTC_OK) + return result; temporary = malloc(length + 12U); - if (temporary == NULL) return XTC_ERR_OUT_OF_MEMORY; - memcpy(temporary, path, length); memcpy(temporary + length, ".tmp.XXXXXX", 12U); + if (temporary == NULL) + return XTC_ERR_OUT_OF_MEMORY; + memcpy(temporary, path, length); + memcpy(temporary + length, ".tmp.XXXXXX", 12U); descriptor = mkstemp(temporary); - if (descriptor < 0) { free(temporary); return XTC_ERR_IO; } + if (descriptor < 0) { + free(temporary); + return XTC_ERR_IO; + } file = fdopen(descriptor, "wb"); - if (file == NULL) { (void)close(descriptor); (void)unlink(temporary); free(temporary); return XTC_ERR_IO; } + if (file == NULL) { + (void)close(descriptor); + (void)unlink(temporary); + free(temporary); + return XTC_ERR_IO; + } result = write_project(file, project); - if (result == XTC_OK && fflush(file) != 0) result = XTC_ERR_IO; - if (result == XTC_OK && fsync(descriptor) != 0) result = XTC_ERR_IO; - if (fclose(file) != 0 && result == XTC_OK) result = XTC_ERR_IO; - if (result == XTC_OK && rename(temporary, path) != 0) result = XTC_ERR_IO; - if (result != XTC_OK) (void)unlink(temporary); - free(temporary); return result; + if (result == XTC_OK && fflush(file) != 0) + result = XTC_ERR_IO; + if (result == XTC_OK && fsync(descriptor) != 0) + result = XTC_ERR_IO; + if (fclose(file) != 0 && result == XTC_OK) + result = XTC_ERR_IO; + if (result == XTC_OK && rename(temporary, path) != 0) + result = XTC_ERR_IO; + if (result != XTC_OK) + (void)unlink(temporary); + free(temporary); + return result; } diff --git a/src/project/history.c b/src/project/history.c index 62fdc1f..5468a07 100644 --- a/src/project/history.c +++ b/src/project/history.c @@ -2,12 +2,14 @@ #include "project_internal.h" #include #include + struct xtc_history { xtc_project *states[XTC_HISTORY_EDITS + 1]; size_t count, cursor; uint64_t next_track, next_clip, next_note; size_t next_node; }; + static void remember_ids(xtc_history *h, const xtc_project *p) { if (p->next_track_id > h->next_track) @@ -19,6 +21,7 @@ static void remember_ids(xtc_history *h, const xtc_project *p) if (p->graph->next_id > h->next_node) h->next_node = p->graph->next_id; } + void xtc_history_destroy(xtc_history *h) { size_t i; @@ -28,6 +31,7 @@ void xtc_history_destroy(xtc_history *h) xtc_project_destroy(h->states[i]); free(h); } + xtc_result xtc_history_create(const xtc_project *p, xtc_history **out) { xtc_history *h; @@ -50,6 +54,7 @@ xtc_result xtc_history_create(const xtc_project *p, xtc_history **out) *out = h; return XTC_OK; } + xtc_result xtc_history_record(xtc_history *h, const xtc_project *p) { xtc_project *copy; @@ -73,10 +78,12 @@ xtc_result xtc_history_record(xtc_history *h, const xtc_project *p) h->cursor = h->count - 1; return XTC_OK; } + static int available(const xtc_history *h, int redo) { return redo ? h->cursor + 1 < h->count : h->cursor > 0; } + xtc_result xtc_history_checkout(const xtc_history *h, int redo, xtc_project **out) { if (!out) @@ -97,6 +104,7 @@ xtc_result xtc_history_checkout(const xtc_history *h, int redo, xtc_project **ou return r; } } + 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 c2b1fea..f571442 100644 --- a/src/project/project.c +++ b/src/project/project.c @@ -8,263 +8,389 @@ int xtc_project_valid_name(const char *name) { size_t length = 0U, i = 0U; - if (name == NULL) return 0; - while (length < XTC_PROJECT_NAME_BYTES && name[length] != '\0') ++length; - if (length == 0U || length == XTC_PROJECT_NAME_BYTES) return 0; + if (name == NULL) + return 0; + while (length < XTC_PROJECT_NAME_BYTES && name[length] != '\0') + ++length; + if (length == 0U || length == XTC_PROJECT_NAME_BYTES) + return 0; while (i < length) { unsigned char byte = (unsigned char)name[i++]; size_t extra, j; - if (byte < 32U || byte == 127U) return 0; - if (byte < 128U) continue; - if (byte >= 194U && byte <= 223U) extra = 1U; - else if (byte >= 224U && byte <= 239U) extra = 2U; - else if (byte >= 240U && byte <= 244U) extra = 3U; - else return 0; - if (extra > length - i) return 0; - if ((byte == 224U && (unsigned char)name[i] < 160U) || - (byte == 237U && (unsigned char)name[i] >= 160U) || - (byte == 240U && (unsigned char)name[i] < 144U) || - (byte == 244U && (unsigned char)name[i] >= 144U)) return 0; + if (byte < 32U || byte == 127U) + return 0; + if (byte < 128U) + continue; + if (byte >= 194U && byte <= 223U) + extra = 1U; + else if (byte >= 224U && byte <= 239U) + extra = 2U; + else if (byte >= 240U && byte <= 244U) + extra = 3U; + else + return 0; + if (extra > length - i) + return 0; + if ((byte == 224U && (unsigned char)name[i] < 160U) || (byte == 237U && (unsigned char)name[i] >= 160U) || + (byte == 240U && (unsigned char)name[i] < 144U) || (byte == 244U && (unsigned char)name[i] >= 144U)) + return 0; for (j = 0U; j < extra; ++j) { unsigned char tail = (unsigned char)name[i++]; - if (tail < 128U || tail > 191U) return 0; + if (tail < 128U || tail > 191U) + return 0; } } return 1; } + static size_t track_index(const xtc_project *project, uint64_t id) { size_t i; - for (i = 0U; i < project->track_count; ++i) if (project->tracks[i].id == id) return i; + for (i = 0U; i < project->track_count; ++i) + if (project->tracks[i].id == id) + return i; return SIZE_MAX; } + static size_t clip_index(const xtc_project *project, uint64_t id) { size_t i; - for (i = 0U; i < project->clip_count; ++i) if (project->clips[i].id == id) return i; + for (i = 0U; i < project->clip_count; ++i) + if (project->clips[i].id == id) + return i; return SIZE_MAX; } + 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; + return start <= XTC_PROJECT_MAX_TICK && length != 0U && length <= XTC_PROJECT_MAX_TICK - start && + source <= XTC_PROJECT_MAX_TICK && offset <= source && length <= source - offset; } + xtc_result xtc_project_create(xtc_project **out_project) { xtc_project *project; xtc_result result; - if (out_project == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (out_project == NULL) + return XTC_ERR_INVALID_ARGUMENT; *out_project = NULL; project = calloc(1U, sizeof(*project)); - if (project == NULL) return XTC_ERR_OUT_OF_MEMORY; + if (project == NULL) + return XTC_ERR_OUT_OF_MEMORY; result = xtc_graph_create(64U, &project->graph); - if (result != XTC_OK) { free(project); return result; } + if (result != XTC_OK) { + free(project); + return result; + } project->graph->require_output_nodes = 1; project->tempo = 120000U; *out_project = project; return XTC_OK; } + void xtc_project_destroy(xtc_project *project) -{ if (project != NULL) { xtc_graph_destroy(project->graph); free(project); } } +{ + if (project != NULL) { + xtc_graph_destroy(project->graph); + free(project); + } +} + const xtc_graph *xtc_project_graph(const xtc_project *project) -{ return project == NULL ? NULL : project->graph; } +{ + return project == NULL ? NULL : project->graph; +} + xtc_result xtc_project_set_graph(xtc_project *project, const xtc_graph *source) { xtc_graph *copy; xtc_result result; - if (project == NULL || source == NULL) return XTC_ERR_INVALID_ARGUMENT; - if (source->node_count > 64U || source->edge_count > 4096U) return XTC_ERR_CAPACITY; + if (project == NULL || source == NULL) + return XTC_ERR_INVALID_ARGUMENT; + if (source->node_count > 64U || source->edge_count > 4096U) + return XTC_ERR_CAPACITY; result = xtc_graph_validate(source); - if (result != XTC_OK) return result; + if (result != XTC_OK) + return result; result = xtc_graph_create(64U, ©); - if (result != XTC_OK) return result; - copy->node_count = source->node_count; copy->edge_count = source->edge_count; - copy->next_id = source->next_id; copy->require_output_nodes = source->require_output_nodes; + if (result != XTC_OK) + return result; + copy->node_count = source->node_count; + copy->edge_count = source->edge_count; + copy->next_id = source->next_id; + copy->require_output_nodes = source->require_output_nodes; memcpy(copy->nodes, source->nodes, source->node_count * sizeof(*copy->nodes)); memcpy(copy->edges, source->edges, source->edge_count * sizeof(*copy->edges)); - xtc_graph_destroy(project->graph); project->graph = copy; + xtc_graph_destroy(project->graph); + project->graph = copy; return XTC_OK; } + xtc_result xtc_project_clone(const xtc_project *source, xtc_project **out_project) { xtc_project *copy; xtc_result result; - if (out_project == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (out_project == NULL) + return XTC_ERR_INVALID_ARGUMENT; *out_project = NULL; - if (source == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (source == NULL) + return XTC_ERR_INVALID_ARGUMENT; copy = malloc(sizeof(*copy)); - if (copy == NULL) return XTC_ERR_OUT_OF_MEMORY; - *copy = *source; copy->graph = NULL; + if (copy == NULL) + return XTC_ERR_OUT_OF_MEMORY; + *copy = *source; + copy->graph = NULL; result = xtc_project_set_graph(copy, source->graph); - if (result != XTC_OK) { free(copy); return result; } + if (result != XTC_OK) { + free(copy); + return result; + } *out_project = copy; return XTC_OK; } + xtc_result xtc_project_get_counts(const xtc_project *project, size_t *tracks, size_t *clips) { - if (project == NULL || tracks == NULL || clips == NULL) return XTC_ERR_INVALID_ARGUMENT; - *tracks = project->track_count; *clips = project->clip_count; return XTC_OK; + if (project == NULL || tracks == NULL || clips == NULL) + return XTC_ERR_INVALID_ARGUMENT; + *tracks = project->track_count; + *clips = project->clip_count; + return XTC_OK; } + xtc_result xtc_project_get_track(const xtc_project *project, size_t index, xtc_track_info *track) { - if (project == NULL || track == NULL) return XTC_ERR_INVALID_ARGUMENT; - if (index >= project->track_count) return XTC_ERR_NOT_FOUND; - *track = project->tracks[index]; return XTC_OK; + if (project == NULL || track == NULL) + return XTC_ERR_INVALID_ARGUMENT; + if (index >= project->track_count) + return XTC_ERR_NOT_FOUND; + *track = project->tracks[index]; + return XTC_OK; } + xtc_result xtc_project_get_clip(const xtc_project *project, size_t index, xtc_clip_info *clip) { - if (project == NULL || clip == NULL) return XTC_ERR_INVALID_ARGUMENT; - if (index >= project->clip_count) return XTC_ERR_NOT_FOUND; - *clip = project->clips[index]; return XTC_OK; + if (project == NULL || clip == NULL) + return XTC_ERR_INVALID_ARGUMENT; + if (index >= project->clip_count) + return XTC_ERR_NOT_FOUND; + *clip = project->clips[index]; + return XTC_OK; } + xtc_result xtc_project_find_clip(const xtc_project *project, uint64_t id, xtc_clip_info *clip) { - if (project == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (project == NULL) + return XTC_ERR_INVALID_ARGUMENT; return xtc_project_get_clip(project, clip_index(project, id), clip); } + xtc_result xtc_project_add_track(xtc_project *project, const char *name, uint64_t *id) { xtc_track_info track = {0}; - if (project == NULL || id == NULL || !xtc_project_valid_name(name)) return XTC_ERR_INVALID_ARGUMENT; - if (project->track_count == XTC_PROJECT_MAX_TRACKS || project->next_track_id == UINT64_MAX) return XTC_ERR_CAPACITY; - track.id = project->next_track_id++; track.gain = 1.0f; + if (project == NULL || id == NULL || !xtc_project_valid_name(name)) + return XTC_ERR_INVALID_ARGUMENT; + if (project->track_count == XTC_PROJECT_MAX_TRACKS || project->next_track_id == UINT64_MAX) + return XTC_ERR_CAPACITY; + track.id = project->next_track_id++; + track.gain = 1.0f; memcpy(track.name, name, strlen(name) + 1U); - project->tracks[project->track_count++] = track; *id = track.id; return XTC_OK; + project->tracks[project->track_count++] = track; + *id = track.id; + return XTC_OK; } + xtc_result xtc_project_remove_track(xtc_project *project, uint64_t id) { size_t index, i, count = 0U; - if (project == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (project == NULL) + return XTC_ERR_INVALID_ARGUMENT; index = track_index(project, id); - if (index == SIZE_MAX) return XTC_ERR_NOT_FOUND; + if (index == SIZE_MAX) + return XTC_ERR_NOT_FOUND; for (i = 0U; i < project->clip_count; ++i) - if (project->clips[i].track_id != id) project->clips[count++] = project->clips[i]; + if (project->clips[i].track_id != id) + project->clips[count++] = project->clips[i]; project->clip_count = count; memmove(project->tracks + index, project->tracks + index + 1U, - (project->track_count - index - 1U) * sizeof(*project->tracks)); + (project->track_count - index - 1U) * sizeof(*project->tracks)); --project->track_count; count = 0; for (i = 0; i < project->note_count; ++i) - if (clip_index(project, project->notes[i].clip_id) != SIZE_MAX) project->notes[count++] = project->notes[i]; + if (clip_index(project, project->notes[i].clip_id) != SIZE_MAX) + project->notes[count++] = project->notes[i]; project->note_count = count; count = 0; for (i = 0; i < project->automation_count; ++i) - if (project->automation[i].track_id != id) project->automation[count++] = project->automation[i]; - project->automation_count = count; return XTC_OK; + if (project->automation[i].track_id != id) + project->automation[count++] = project->automation[i]; + project->automation_count = count; + return XTC_OK; } -xtc_result xtc_project_add_clip(xtc_project *project, uint64_t track, const char *name, - uint64_t start, uint64_t length, uint64_t *id) + +xtc_result xtc_project_add_clip(xtc_project *project, uint64_t track, const char *name, uint64_t start, uint64_t length, + uint64_t *id) { xtc_clip_info clip = {0}; if (project == NULL || id == NULL || !xtc_project_valid_name(name) || !valid_range(start, length, 0U, length)) return XTC_ERR_INVALID_ARGUMENT; - if (track_index(project, track) == SIZE_MAX) return XTC_ERR_NOT_FOUND; - if (project->clip_count == XTC_PROJECT_MAX_CLIPS || project->next_clip_id == UINT64_MAX) return XTC_ERR_CAPACITY; - clip.id = project->next_clip_id++; clip.track_id = track; - clip.start_tick = start; clip.length_tick = clip.source_length_tick = length; + if (track_index(project, track) == SIZE_MAX) + return XTC_ERR_NOT_FOUND; + if (project->clip_count == XTC_PROJECT_MAX_CLIPS || project->next_clip_id == UINT64_MAX) + return XTC_ERR_CAPACITY; + clip.id = project->next_clip_id++; + clip.track_id = track; + clip.start_tick = start; + clip.length_tick = clip.source_length_tick = length; memcpy(clip.name, name, strlen(name) + 1U); - project->clips[project->clip_count++] = clip; *id = clip.id; return XTC_OK; + project->clips[project->clip_count++] = clip; + *id = clip.id; + return XTC_OK; } + xtc_result xtc_project_remove_clip(xtc_project *project, uint64_t id) { size_t index; - if (project == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (project == NULL) + return XTC_ERR_INVALID_ARGUMENT; index = clip_index(project, id); - if (index == SIZE_MAX) return XTC_ERR_NOT_FOUND; + if (index == SIZE_MAX) + return XTC_ERR_NOT_FOUND; memmove(project->clips + index, project->clips + index + 1U, - (project->clip_count - index - 1U) * sizeof(*project->clips)); + (project->clip_count - index - 1U) * sizeof(*project->clips)); --project->clip_count; { size_t i, count = 0; for (i = 0; i < project->note_count; ++i) - if (project->notes[i].clip_id != id) project->notes[count++] = project->notes[i]; + if (project->notes[i].clip_id != id) + project->notes[count++] = project->notes[i]; project->note_count = count; } return XTC_OK; } -xtc_result xtc_project_edit_clip(xtc_project *project, uint64_t id, uint64_t track, - uint64_t start, uint64_t length, uint64_t offset) + +xtc_result xtc_project_edit_clip(xtc_project *project, uint64_t id, uint64_t track, uint64_t start, uint64_t length, + uint64_t offset) { size_t index; - if (project == NULL) return XTC_ERR_INVALID_ARGUMENT; + if (project == NULL) + return XTC_ERR_INVALID_ARGUMENT; index = clip_index(project, id); - if (index == SIZE_MAX || track_index(project, track) == SIZE_MAX) return XTC_ERR_NOT_FOUND; - if (!valid_range(start, length, offset, project->clips[index].source_length_tick)) return XTC_ERR_INVALID_ARGUMENT; - project->clips[index].track_id = track; project->clips[index].start_tick = start; - project->clips[index].length_tick = length; project->clips[index].offset_tick = offset; + if (index == SIZE_MAX || track_index(project, track) == SIZE_MAX) + return XTC_ERR_NOT_FOUND; + if (!valid_range(start, length, offset, project->clips[index].source_length_tick)) + return XTC_ERR_INVALID_ARGUMENT; + project->clips[index].track_id = track; + project->clips[index].start_tick = start; + project->clips[index].length_tick = length; + project->clips[index].offset_tick = offset; return XTC_OK; } -unsigned int xtc_project_tempo(const xtc_project *project) { return project == NULL ? 0U : project->tempo; } + +unsigned int xtc_project_tempo(const xtc_project *project) +{ + return project == NULL ? 0U : project->tempo; +} + xtc_result xtc_project_set_tempo(xtc_project *project, unsigned int tempo) { - if (project == NULL || tempo < 20000U || tempo > 300000U) return XTC_ERR_INVALID_ARGUMENT; - project->tempo = tempo; return XTC_OK; + if (project == NULL || tempo < 20000U || tempo > 300000U) + return XTC_ERR_INVALID_ARGUMENT; + project->tempo = tempo; + return XTC_OK; } + 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) return XTC_ERR_INVALID_ARGUMENT; - if (tick > XTC_PROJECT_MAX_TICK) return XTC_ERR_CAPACITY; + if (project == NULL || sample == NULL || rate == 0U || rate > 768000U) + return XTC_ERR_INVALID_ARGUMENT; + if (tick > XTC_PROJECT_MAX_TICK) + return XTC_ERR_CAPACITY; *sample = tick * rate * UINT64_C(60000) / ((uint64_t)project->tempo * XTC_TICKS_PER_QUARTER); return XTC_OK; } + 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; - if (project == NULL || tick == NULL || rate == 0U || rate > 768000U) return XTC_ERR_INVALID_ARGUMENT; + if (project == NULL || tick == NULL || rate == 0U || rate > 768000U) + return XTC_ERR_INVALID_ARGUMENT; scale = (uint64_t)project->tempo * XTC_TICKS_PER_QUARTER; divisor = (uint64_t)rate * UINT64_C(60000); whole = sample / divisor; - if (whole > XTC_PROJECT_MAX_TICK / scale) return XTC_ERR_CAPACITY; + if (whole > XTC_PROJECT_MAX_TICK / scale) + return XTC_ERR_CAPACITY; value = whole * scale + (sample % divisor) * scale / divisor; - if (value > XTC_PROJECT_MAX_TICK) return XTC_ERR_CAPACITY; - *tick = value; return XTC_OK; + if (value > XTC_PROJECT_MAX_TICK) + return XTC_ERR_CAPACITY; + *tick = value; + return XTC_OK; } + xtc_result xtc_project_validate(const xtc_project *project) { size_t i, j; - if (project->tempo < 20000U || project->tempo > 300000U || - project->track_count > XTC_PROJECT_MAX_TRACKS || project->clip_count > XTC_PROJECT_MAX_CLIPS || project->note_count > XTC_PROJECT_MAX_NOTES) return XTC_ERR_FORMAT; + if (project->tempo < 20000U || project->tempo > 300000U || project->track_count > XTC_PROJECT_MAX_TRACKS || + project->clip_count > XTC_PROJECT_MAX_CLIPS || project->note_count > XTC_PROJECT_MAX_NOTES) + return XTC_ERR_FORMAT; for (i = 0U; i < project->track_count; ++i) { - if (project->tracks[i].id >= project->next_track_id || !xtc_project_valid_name(project->tracks[i].name)) return XTC_ERR_FORMAT; - for (j = 0U; j < i; ++j) if (project->tracks[j].id == project->tracks[i].id) return XTC_ERR_FORMAT; + if (project->tracks[i].id >= project->next_track_id || !xtc_project_valid_name(project->tracks[i].name)) + return XTC_ERR_FORMAT; + for (j = 0U; j < i; ++j) + if (project->tracks[j].id == project->tracks[i].id) + return XTC_ERR_FORMAT; } for (i = 0U; i < project->clip_count; ++i) { const xtc_clip_info *clip = project->clips + i; size_t path_length = 0U; while (path_length < XTC_ASSET_PATH_BYTES && clip->asset_path[path_length]) { unsigned char byte = (unsigned char)clip->asset_path[path_length++]; - if (byte < 32U || byte == 127U) return XTC_ERR_FORMAT; + if (byte < 32U || byte == 127U) + return XTC_ERR_FORMAT; } - if (path_length == XTC_ASSET_PATH_BYTES || (path_length && clip->asset_path[0] != '/')) return XTC_ERR_FORMAT; - if ((clip->midi != 0 && clip->midi != 1) || (clip->midi && path_length)) return XTC_ERR_FORMAT; + if (path_length == XTC_ASSET_PATH_BYTES || (path_length && clip->asset_path[0] != '/')) + return XTC_ERR_FORMAT; + if ((clip->midi != 0 && clip->midi != 1) || (clip->midi && path_length)) + return XTC_ERR_FORMAT; if (clip->id >= project->next_clip_id || track_index(project, clip->track_id) == SIZE_MAX || - !xtc_project_valid_name(clip->name) || !valid_range(clip->start_tick, clip->length_tick, clip->offset_tick, clip->source_length_tick)) return XTC_ERR_FORMAT; - for (j = 0U; j < i; ++j) if (project->clips[j].id == clip->id) return XTC_ERR_FORMAT; + !xtc_project_valid_name(clip->name) || + !valid_range(clip->start_tick, clip->length_tick, clip->offset_tick, clip->source_length_tick)) + return XTC_ERR_FORMAT; + for (j = 0U; j < i; ++j) + if (project->clips[j].id == clip->id) + return XTC_ERR_FORMAT; } for (i = 0; i < project->note_count; ++i) { const xtc_note_info *n = project->notes + i; size_t c = clip_index(project, n->clip_id); - if (c == SIZE_MAX || !project->clips[c].midi || n->id >= project->next_note_id || - n->pitch > 127 || !n->velocity || n->velocity > 127 || n->channel > 15 || - !n->length_tick || n->start_tick >= project->clips[c].source_length_tick || - n->length_tick > project->clips[c].source_length_tick - n->start_tick) return XTC_ERR_FORMAT; - for (j = 0; j < i; ++j) if (project->notes[j].id == n->id) return XTC_ERR_FORMAT; + if (c == SIZE_MAX || !project->clips[c].midi || n->id >= project->next_note_id || n->pitch > 127 || + !n->velocity || n->velocity > 127 || n->channel > 15 || !n->length_tick || + n->start_tick >= project->clips[c].source_length_tick || + n->length_tick > project->clips[c].source_length_tick - n->start_tick) + return XTC_ERR_FORMAT; + for (j = 0; j < i; ++j) + if (project->notes[j].id == n->id) + return XTC_ERR_FORMAT; } - if (project->automation_count > XTC_MAX_AUTOMATION_POINTS) return XTC_ERR_FORMAT; + if (project->automation_count > XTC_MAX_AUTOMATION_POINTS) + return XTC_ERR_FORMAT; for (i = 0; i < project->track_count; ++i) { const xtc_track_info *t = project->tracks + i; if (!isfinite(t->gain) || t->gain < 0 || t->gain > 4 || !isfinite(t->pan) || t->pan < -1 || t->pan > 1 || - (t->mute != 0 && t->mute != 1) || (t->solo != 0 && t->solo != 1)) return XTC_ERR_FORMAT; + (t->mute != 0 && t->mute != 1) || (t->solo != 0 && t->solo != 1)) + return XTC_ERR_FORMAT; } for (i = 0; i < project->automation_count; ++i) { const xtc_automation_point *a = project->automation + i; - if (track_index(project, a->track_id) == SIZE_MAX || a->tick > XTC_PROJECT_MAX_TICK || - !isfinite(a->value) || (a->parameter != XTC_AUTOMATION_GAIN && a->parameter != XTC_AUTOMATION_PAN) || + if (track_index(project, a->track_id) == SIZE_MAX || a->tick > XTC_PROJECT_MAX_TICK || !isfinite(a->value) || + (a->parameter != XTC_AUTOMATION_GAIN && a->parameter != XTC_AUTOMATION_PAN) || a->value < (a->parameter == XTC_AUTOMATION_GAIN ? 0 : -1) || - a->value > (a->parameter == XTC_AUTOMATION_GAIN ? 4 : 1)) return XTC_ERR_FORMAT; - for (j = 0; j < i; ++j) if (project->automation[j].track_id == a->track_id && - project->automation[j].parameter == a->parameter && project->automation[j].tick == a->tick) return XTC_ERR_FORMAT; + a->value > (a->parameter == XTC_AUTOMATION_GAIN ? 4 : 1)) + return XTC_ERR_FORMAT; + for (j = 0; j < i; ++j) + if (project->automation[j].track_id == a->track_id && project->automation[j].parameter == a->parameter && + project->automation[j].tick == a->tick) + return XTC_ERR_FORMAT; } return XTC_OK; } @@ -272,14 +398,20 @@ xtc_result xtc_project_validate(const xtc_project *project) xtc_result xtc_project_set_clip_asset(xtc_project *project, uint64_t id, const char *path) { size_t index, length = 0U, i; - if (!project || !path) return XTC_ERR_INVALID_ARGUMENT; - while (length < XTC_ASSET_PATH_BYTES && path[length]) ++length; - if (length == XTC_ASSET_PATH_BYTES || (length && path[0] != '/')) return XTC_ERR_INVALID_ARGUMENT; + if (!project || !path) + return XTC_ERR_INVALID_ARGUMENT; + while (length < XTC_ASSET_PATH_BYTES && path[length]) + ++length; + if (length == XTC_ASSET_PATH_BYTES || (length && path[0] != '/')) + return XTC_ERR_INVALID_ARGUMENT; for (i = 0; i < length; ++i) - if ((unsigned char)path[i] < 32U || path[i] == 127) return XTC_ERR_INVALID_ARGUMENT; + if ((unsigned char)path[i] < 32U || path[i] == 127) + return XTC_ERR_INVALID_ARGUMENT; index = clip_index(project, id); - if (index == SIZE_MAX) return XTC_ERR_NOT_FOUND; - if (project->clips[index].midi) return XTC_ERR_INVALID_STATE; + if (index == SIZE_MAX) + return XTC_ERR_NOT_FOUND; + if (project->clips[index].midi) + return XTC_ERR_INVALID_STATE; memmove(project->clips[index].asset_path, path, length + 1U); return XTC_OK; } diff --git a/src/project/project_internal.h b/src/project/project_internal.h index c044df9..3a53804 100644 --- a/src/project/project_internal.h +++ b/src/project/project_internal.h @@ -1,8 +1,10 @@ #ifndef XTC_PROJECT_INTERNAL_H #define XTC_PROJECT_INTERNAL_H + #include "xtc/xtc_project.h" #include "xtc/xtc_midi.h" #include "xtc/xtc_mixer.h" + struct xtc_project { xtc_graph *graph; uint64_t next_track_id, next_clip_id; @@ -16,6 +18,7 @@ struct xtc_project { xtc_track_info tracks[XTC_PROJECT_MAX_TRACKS]; xtc_clip_info clips[XTC_PROJECT_MAX_CLIPS]; }; + int xtc_project_valid_name(const char *name); xtc_result xtc_project_validate(const xtc_project *project); #endif