Some code semantics refactoring.

This commit is contained in:
Johannes Findeisen 2026-10-27 01:37:21 +01:00
commit 0f04e056b2
42 changed files with 3374 additions and 1357 deletions

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -5,7 +5,12 @@
#include <math.h>
#include <stdlib.h>
#include <string.h>
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);
}

View file

@ -4,6 +4,7 @@
#include <float.h>
#include <string.h>
#include <unistd.h>
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);
}

View file

@ -3,35 +3,63 @@
#include <math.h>
#include <stdlib.h>
#include <string.h>
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;
}

View file

@ -1,8 +1,15 @@
#ifndef XTC_WAVE_INTERNAL_H
#define XTC_WAVE_INTERNAL_H
#include "xtc/xtc_audio.h"
#include <stdio.h>
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);

View file

@ -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

View file

@ -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";
}
}

View file

@ -1,8 +1,7 @@
#include <stddef.h>
/* 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;

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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

View file

@ -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<int>(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<double>(node.value));
else if (node.type == XTC_NODE_GAIN) std::snprintf(text, sizeof(text), "x %.2f", static_cast<double>(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<double>(node.value));
else if (node.type == XTC_NODE_GAIN)
std::snprintf(text, sizeof(text), "x %.2f", static_cast<double>(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);
}

View file

@ -3,17 +3,30 @@
#include <FL/Fl_Widget.H>
#include <array>
#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<Position, 256> positions_{};
size_t count_ = 0U;
const xtc_graph *graph_;

View file

@ -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();

View file

@ -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<MainWindow *>(c)->mark_dirty(); }, this);
add_track->callback([](Fl_Widget *, void *c) { static_cast<SequencerWidget *>(c)->add_track(); }, sequencer_);
add_midi->callback([](Fl_Widget *, void *c) { static_cast<SequencerWidget *>(c)->add_midi_clip(); }, sequencer_);
add_region->callback([](Fl_Widget *, void *c) { static_cast<SequencerWidget *>(c)->add_region(); }, sequencer_);
delete_region->callback([](Fl_Widget *, void *c) { static_cast<SequencerWidget *>(c)->delete_selection(); }, sequencer_);
zoom_in->callback([](Fl_Widget *, void *c) { static_cast<SequencerWidget *>(c)->zoom(1); }, sequencer_);
zoom_out->callback([](Fl_Widget *, void *c) { static_cast<SequencerWidget *>(c)->zoom(-1); }, sequencer_);
sequencer_->callback(
[](Fl_Widget *, void *c) {
static_cast<MainWindow *>(c)->mark_dirty();
},
this);
add_track->callback(
[](Fl_Widget *, void *c) {
static_cast<SequencerWidget *>(c)->add_track();
},
sequencer_);
add_midi->callback(
[](Fl_Widget *, void *c) {
static_cast<SequencerWidget *>(c)->add_midi_clip();
},
sequencer_);
add_region->callback(
[](Fl_Widget *, void *c) {
static_cast<SequencerWidget *>(c)->add_region();
},
sequencer_);
delete_region->callback(
[](Fl_Widget *, void *c) {
static_cast<SequencerWidget *>(c)->delete_selection();
},
sequencer_);
zoom_in->callback(
[](Fl_Widget *, void *c) {
static_cast<SequencerWidget *>(c)->zoom(1);
},
sequencer_);
zoom_out->callback(
[](Fl_Widget *, void *c) {
static_cast<SequencerWidget *>(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<MainWindow *>(c)->mark_dirty(); }, this);
mixer_->callback(
[](Fl_Widget *, void *c) {
static_cast<MainWindow *>(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<GraphWidget *>(context)->add_node(XTC_NODE_OSCILLATOR);
}, graph_widget_);
oscillator->callback(
[](Fl_Widget *, void *context) {
(void)static_cast<GraphWidget *>(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<GraphWidget *>(context)->add_node(XTC_NODE_GAIN);
}, graph_widget_);
gain->callback(
[](Fl_Widget *, void *context) {
(void)static_cast<GraphWidget *>(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<GraphWidget *>(context)->add_node(XTC_NODE_METER);
}, graph_widget_);
meter->callback(
[](Fl_Widget *, void *context) {
(void)static_cast<GraphWidget *>(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<GraphWidget *>(context)->add_node(XTC_NODE_OUTPUT);
}, graph_widget_);
output->callback(
[](Fl_Widget *, void *context) {
(void)static_cast<GraphWidget *>(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<GraphWidget *>(context)->add_node(XTC_NODE_AUDIO);
}, graph_widget_);
audio->callback(
[](Fl_Widget *, void *context) {
(void)static_cast<GraphWidget *>(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<GraphWidget *>(c)->add_node(XTC_NODE_INSTRUMENT); }, graph_widget_);
instrument->callback(
[](Fl_Widget *, void *c) {
(void)static_cast<GraphWidget *>(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<GraphWidget *>(context)->delete_selected();
}, graph_widget_);
remove->callback(
[](Fl_Widget *, void *context) {
(void)static_cast<GraphWidget *>(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<MainWindow *>(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<MainWindow *>(context)->start_engine(); }
{
static_cast<MainWindow *>(context)->start_engine();
}
void MainWindow::stop_callback(Fl_Widget *, void *context)
{ static_cast<MainWindow *>(context)->stop_engine(); }
{
static_cast<MainWindow *>(context)->stop_engine();
}
void MainWindow::reconnect_callback(Fl_Widget *, void *context)
{ static_cast<MainWindow *>(context)->reconnect_engine(); }
{
static_cast<MainWindow *>(context)->reconnect_engine();
}
void MainWindow::close_callback(Fl_Widget *, void *context)
{ (void)static_cast<MainWindow *>(context)->close_window(); }
{
(void)static_cast<MainWindow *>(context)->close_window();
}
void MainWindow::view_callback(Fl_Widget *widget, void *view)
{
auto *window = static_cast<MainWindow *>(widget->window());
window->tabs_->value(static_cast<Fl_Widget *>(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<MainWindow *>(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<MainWindow *>(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<xtc_history, decltype(&xtc_history_destroy)> 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<MainWindow *>(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<Fl_Menu_Bar *>(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<MainWindow *>(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<size_t>(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<size_t>(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<MainWindow *>(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<Fl_Double_Window>(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<MainWindow *>(c)->mark_dirty(); }, this);
scroll->end(); piano_window_->end(); piano_window_->resizable(scroll);
piano_->callback(
[](Fl_Widget *, void *c) {
static_cast<MainWindow *>(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<MainWindow *>(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<MainWindow *>(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<MainWindow *>(context);
auto *menu = static_cast<Fl_Menu_Bar *>(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);
}

View file

@ -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<xtc_history, decltype(&xtc_history_destroy)> history_{nullptr, xtc_history_destroy};
static void history_callback(Fl_Widget *, void *);

View file

@ -13,16 +13,19 @@
#include <cstdio>
#include <string>
#include <vector>
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<unsigned long long>(mixer_.lane_start() +
static_cast<uint64_t>(i) * 960));
static_cast<unsigned long long>(mixer_.lane_start() + static_cast<uint64_t>(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<float>(static_cast<double>(tick - a.tick) /
static_cast<double>(it->tick - a.tick)) *
(it->value - a.value);
return a.value +
static_cast<float>(static_cast<double>(tick - a.tick) / static_cast<double>(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<uint64_t>(px - 1) * lane_ticks /
static_cast<uint64_t>(w() - 1);
uint64_t b = mixer_.lane_start() + static_cast<uint64_t>(px) * lane_ticks /
static_cast<uint64_t>(w() - 1);
uint64_t a =
mixer_.lane_start() + static_cast<uint64_t>(px - 1) * lane_ticks / static_cast<uint64_t>(w() - 1);
uint64_t b =
mixer_.lane_start() + static_cast<uint64_t>(px) * lane_ticks / static_cast<uint64_t>(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<int>((a.tick - mixer_.lane_start()) *
static_cast<uint64_t>(w() - 1) / lane_ticks);
int px = x() +
static_cast<int>((a.tick - mixer_.lane_start()) * static_cast<uint64_t>(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<uint64_t>(std::clamp(Fl::event_x() - x(), 0, w() - 1)) *
lane_ticks / static_cast<uint64_t>(w() - 1);
uint64_t tick = mixer_.lane_start() + static_cast<uint64_t>(std::clamp(Fl::event_x() - x(), 0, w() - 1)) *
lane_ticks / static_cast<uint64_t>(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<float>(std::clamp(y() + h() - 6 - Fl::event_y(), 0, h() - 28)) /
static_cast<float>(h() - 28) * range;
float value = min + static_cast<float>(std::clamp(y() + h() - 6 - Fl::event_y(), 0, h() - 28)) /
static_cast<float>(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<uint64_t>(8) * lane_ticks / static_cast<uint64_t>(w() - 1) + 1,
uint64_t distance = static_cast<uint64_t>(8) * lane_ticks / static_cast<uint64_t>(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<size_t>(tracks_->value()), &t) == XTC_OK
? t.id
: UINT64_MAX;
return xtc_project_get_track(project_, static_cast<size_t>(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<uint64_t>(
std::clamp(start_->value(), 0.0, static_cast<double>(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<size_t>(tracks_->value()), &t) == XTC_OK;
for (Fl_Widget *w : {static_cast<Fl_Widget *>(gain_), static_cast<Fl_Widget *>(pan_),
static_cast<Fl_Widget *>(mute_), static_cast<Fl_Widget *>(solo_),
static_cast<Fl_Widget *>(lane_)}) {
bool valid = xtc_project_get_track(project_, static_cast<size_t>(tracks_->value()), &t) == XTC_OK;
for (Fl_Widget *w :
{static_cast<Fl_Widget *>(gain_), static_cast<Fl_Widget *>(pan_), static_cast<Fl_Widget *>(mute_),
static_cast<Fl_Widget *>(solo_), static_cast<Fl_Widget *>(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<MixerWidget *>(c);
m->mix(static_cast<float>(m->gain_->value()), static_cast<float>(m->pan_->value()),
m->mute_->value(), m->solo_->value());
m->mix(static_cast<float>(m->gain_->value()), static_cast<float>(m->pan_->value()), m->mute_->value(),
m->solo_->value());
}
void MixerWidget::select(Fl_Widget *, void *c)
{
static_cast<MixerWidget *>(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<int>(n) - 1));
tracks_->value(std::clamp(tracks_->value() + (key == FL_Up ? -1 : 1), 0, static_cast<int>(n) - 1));
selection();
return 1;
}

View file

@ -2,24 +2,27 @@
#define XTC_MIXER_WIDGET_HPP
#include "xtc/xtc_mixer.h"
#include <FL/Fl_Group.H>
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);

View file

@ -4,55 +4,87 @@
#include <FL/fl_ask.H>
#include <algorithm>
#include <cstdio>
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<int>(c.source_length_tick * scale / 960) + key_width + 20 : 900;
int width = xtc_project_find_clip(project_, clip_, &c) == XTC_OK
? static_cast<int>(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<uint64_t>(std::max(0, px - x() - key_width)) * 960U / scale / 240U * 240U; }
{
return static_cast<uint64_t>(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<int>(n.start_tick * scale / 960);
int right = left + std::max(3, static_cast<int>(n.length_tick * scale / 960));
int py = y() + header + (127 - static_cast<int>(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<int>(n.start_tick * scale / 960);
int right = left + std::max(3, static_cast<int>(n.length_tick * scale / 960));
int top = y() + header + (127 - static_cast<int>(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<uint64_t>(960, c.source_length_tick - start), static_cast<unsigned int>(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<uint64_t>(960, c.source_length_tick - start),
static_cast<unsigned int>(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<int64_t>(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<int64_t>(original_.start_tick), length = static_cast<int64_t>(original_.length_tick);
xtc_clip_info c{};
if (xtc_project_find_clip(project_, clip_, &c) != XTC_OK)
return 1;
int64_t start = static_cast<int64_t>(original_.start_tick),
length = static_cast<int64_t>(original_.length_tick);
unsigned int pitch = original_.pitch;
if (mode == 1) {
start = std::clamp<int64_t>(start + delta, 0, static_cast<int64_t>(c.source_length_tick) - length);
pitch = static_cast<unsigned int>(std::clamp(static_cast<int>(pitch) - (Fl::event_y() - drag_y_) / row_height, 0, 127));
pitch = static_cast<unsigned int>(
std::clamp(static_cast<int>(pitch) - (Fl::event_y() - drag_y_) / row_height, 0, 127));
} else {
int64_t maximum = static_cast<int64_t>(c.source_length_tick) - start;
length = std::clamp<int64_t>(length + delta, std::min<int64_t>(240, maximum), maximum);
}
xtc_result r = before_ ? before_(context_) : XTC_OK;
if (r == XTC_OK) r = xtc_project_edit_note(project_, selected_, static_cast<uint64_t>(start),
static_cast<uint64_t>(length), pitch, original_.velocity, original_.channel);
change(r); return 1;
if (r == XTC_OK)
r = xtc_project_edit_note(project_, selected_, static_cast<uint64_t>(start), static_cast<uint64_t>(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);
}

View file

@ -2,19 +2,27 @@
#define XTC_PIANO_ROLL_HPP
#include <FL/Fl_Widget.H>
#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_;

View file

@ -6,56 +6,101 @@
#include <FL/fl_ask.H>
#include <algorithm>
#include <cstdio>
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<uint64_t>(std::max(0, px - x() - 120)) * 960U /
static_cast<unsigned int>(scale_) / 240U * 240U);
static_cast<unsigned int>(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<int>(end * static_cast<unsigned int>(scale_) / 960U + 140U),
std::max(400, static_cast<int>(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<int>(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<int>(c.start_tick * static_cast<unsigned int>(scale_) / 960U);
int right = left + std::max(4, static_cast<int>(c.length_tick * static_cast<unsigned int>(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<int>(cursor_ * static_cast<unsigned int>(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<size_t>(row) >= tracks) return 1;
xtc_track_info t{}; xtc_project_get_track(project_, static_cast<size_t>(row), &t); track_ = t.id;
if (row < 0 || static_cast<size_t>(row) >= tracks)
return 1;
xtc_track_info t{};
xtc_project_get_track(project_, static_cast<size_t>(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<int>(c.start_tick * static_cast<unsigned int>(scale_) / 960U);
int right = left + static_cast<int>(c.length_tick * static_cast<unsigned int>(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<int64_t>(Fl::event_x() - drag_x_) * 960 / scale_ / 240 * 240;
int64_t start = static_cast<int64_t>(original_.start_tick);
int64_t length = static_cast<int64_t>(original_.length_tick);
@ -162,17 +257,36 @@ int SequencerWidget::handle(int event)
start = std::max<int64_t>(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<size_t>(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<size_t>(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<uint64_t>(start),
static_cast<uint64_t>(length), static_cast<uint64_t>(offset)));
static_cast<uint64_t>(length), static_cast<uint64_t>(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);
}

View file

@ -2,27 +2,54 @@
#define XTC_SEQUENCER_WIDGET_HPP
#include <FL/Fl_Widget.H>
#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_;

View file

@ -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;

View file

@ -3,10 +3,26 @@
#include <math.h>
#include <stdlib.h>
#include <string.h>
#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);
}

View file

@ -1,88 +1,137 @@
#include "../project/project_internal.h"
#include "../graph/graph_internal.h"
#include <string.h>
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;
}

View file

@ -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). */

View file

@ -1,6 +1,7 @@
#include "../project/project_internal.h"
#include <math.h>
#include <string.h>
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)

View file

@ -1,6 +1,7 @@
#include "mix_internal.h"
#include <stdlib.h>
#include <string.h>
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;

View file

@ -1,12 +1,14 @@
#include "xtc/xtc_native.h"
#include <math.h>
#include <stdlib.h>
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)

View file

@ -11,7 +11,14 @@
#include <string.h>
#include <unistd.h>
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;
}

View file

@ -2,12 +2,14 @@
#include "project_internal.h"
#include <stdlib.h>
#include <string.h>
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))

View file

@ -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, &copy);
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;
}

View file

@ -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