Some code semantics refactoring.
This commit is contained in:
parent
de403b37d7
commit
0f04e056b2
42 changed files with 3374 additions and 1357 deletions
259
docs/ROADMAP.md
259
docs/ROADMAP.md
|
|
@ -116,4 +116,263 @@ RECOVERY.md. Native graphs remain session-only, with unsupported saves preservin
|
|||
files. No stable binary plugin ABI, external host, system package, autosave or
|
||||
physical-device/endurance qualification is claimed.
|
||||
|
||||
## Phase 10 — Engine Correctness and Reliability
|
||||
|
||||
**Goal:** Verify that the existing implementation behaves correctly under realistic workloads before adding more features.
|
||||
|
||||
### Tasks
|
||||
|
||||
- Audit graph compilation, topological ordering, port compatibility, and cycle detection.
|
||||
- Test graph replacement while audio is running, including safe reclamation of old processing graphs.
|
||||
- Verify that audio-thread operations are bounded and non-blocking.
|
||||
- Test repeated initialization, start/stop cycles, device failures, shutdown, and partial initialization.
|
||||
- Run AddressSanitizer, UndefinedBehaviorSanitizer, leak checks, and static analysis where available.
|
||||
- Add regression tests for every bug discovered.
|
||||
- Document known limitations and thread ownership for shared resources.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- Reproducible tests cover engine lifecycle, graph changes, error handling, and DSP output.
|
||||
- No known critical real-time safety defects remain.
|
||||
- Resource ownership and cleanup behavior are documented.
|
||||
|
||||
## Phase 11 — Complete Minimal Music-Making Workflow
|
||||
|
||||
**Goal:** Ensure a user can create, save, reopen, and play a small musical project without manually editing project files.
|
||||
|
||||
### Tasks
|
||||
|
||||
- Verify project creation, track creation, clip arrangement, routing, and transport as one integrated workflow.
|
||||
- Ensure play, pause, stop, seek, and return-to-start behavior are consistent.
|
||||
- Save, close, reopen, and continue editing projects.
|
||||
- Provide useful defaults and actionable error messages.
|
||||
- Keep the sequencer as the default workspace, with the modular graph available when needed.
|
||||
- Test that GUI state and engine state remain synchronized.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- A user can create and reopen a small project and reproduce the same arrangement and routing.
|
||||
- Common operations do not require manual project-file edits.
|
||||
- Transport and GUI state remain consistent during normal playback and error conditions.
|
||||
|
||||
## Phase 12 — Audio Quality and Transport Correctness
|
||||
|
||||
**Goal:** Make timing and audio output predictable and musically reliable.
|
||||
|
||||
### Tasks
|
||||
|
||||
- Define precise behavior for tempo and meter changes.
|
||||
- Verify conversion between musical positions and sample positions without accumulated timing drift.
|
||||
- Test clips and events that cross processing-block boundaries.
|
||||
- Prevent clicks at clip boundaries and during playback start/stop where appropriate.
|
||||
- Implement gain ramps and parameter smoothing where abrupt changes could create artifacts.
|
||||
- Test silence, clipping, denormal values, channel layouts, and unusual supported buffer sizes.
|
||||
- Document latency reporting and identify whether latency compensation is required by the current architecture.
|
||||
- Add deterministic tests for sample-accurate event scheduling where supported.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- Automated tests verify timing conversions and expected sample output.
|
||||
- Tests cover events and clips at block boundaries and tempo changes.
|
||||
- Listening tests cover transitions, start/stop behavior, and representative projects.
|
||||
- Known limitations involving latency and synchronization are documented.
|
||||
|
||||
## Phase 13 — Recording and Recovery
|
||||
|
||||
**Goal:** Protect recorded material and handle recording failures predictably.
|
||||
|
||||
### Tasks
|
||||
|
||||
- Test long recordings and recording-buffer saturation.
|
||||
- Handle disk-full conditions, write errors, interrupted recording, and unavailable destinations.
|
||||
- Keep file I/O outside the real-time audio callback.
|
||||
- Define and implement recovery or clear reporting for incomplete recordings.
|
||||
- Make recording destinations and supported file formats explicit.
|
||||
- Protect existing files from accidental overwrites.
|
||||
- Test reopening projects when media files are missing, incomplete, or inaccessible.
|
||||
- Ensure shutdown and project closure do not silently discard pending recorded data.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- Recording failures are reported clearly and do not cause unsafe audio-thread behavior.
|
||||
- Valid recorded material is preserved whenever possible.
|
||||
- Incomplete recordings and missing media are detected and explained.
|
||||
- Recording and recovery behavior is covered by automated or repeatable integration tests.
|
||||
|
||||
## Phase 14 — Native Plugin, Instrument, and Effect Architecture
|
||||
|
||||
**Goal:** Provide a stable internal extension interface before hosting external plugins.
|
||||
|
||||
### Tasks
|
||||
|
||||
- Finalize the native C node API, including initialization, processing, reset, and destruction.
|
||||
- Define parameter metadata, defaults, valid ranges, and automation behavior.
|
||||
- Specify audio channel layouts and MIDI/event handling.
|
||||
- Define node state serialization and restoration requirements.
|
||||
- Establish rules for node creation, failure, reconfiguration, and destruction.
|
||||
- Ensure the GUI uses documented public APIs rather than internal engine structures.
|
||||
- Provide a small example instrument and effect implemented against the public interface.
|
||||
- Document ABI/API compatibility expectations for native extensions.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- A developer can implement a small native instrument or effect using documented interfaces.
|
||||
- Invalid parameters, unsupported layouts, and failed initialization are handled safely.
|
||||
- Node state can be saved and restored where required.
|
||||
- No C++ or FLTK types leak into the public C API.
|
||||
|
||||
## Phase 15 — External Plugin Hosting
|
||||
|
||||
**Goal:** Add external plugin support only after the internal node lifecycle is reliable.
|
||||
|
||||
### Tasks
|
||||
|
||||
- Evaluate candidate plugin formats for licensing, dependencies, maintenance, and Linux support.
|
||||
- Record the selected format and rationale in an architecture decision document.
|
||||
- Implement plugin discovery and loading outside the real-time callback.
|
||||
- Map plugin audio ports, parameters, MIDI/events, and channel layouts to the engine's node model.
|
||||
- Support plugin state save/load and clear reporting for missing or incompatible plugins.
|
||||
- Test plugins with different channel counts, parameter sets, and processing requirements.
|
||||
- Define how plugin failures are reported and how the engine recovers.
|
||||
- Document crash-isolation limitations; an in-process plugin must not be assumed safe merely because the host is safe.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- A small, documented set of compatible third-party plugins can be loaded, connected, saved, reopened, and played.
|
||||
- Missing or incompatible plugins do not silently corrupt the project.
|
||||
- Plugin loading and destruction do not occur in the real-time callback.
|
||||
- The chosen format's dependencies and licensing are documented.
|
||||
|
||||
## Phase 16 — Automation and Expressive Control
|
||||
|
||||
**Goal:** Make automation and MIDI control reliable, sample-timed, and usable.
|
||||
|
||||
### Tasks
|
||||
|
||||
- Support automation for common parameters such as volume, pan, instrument parameters, and effect parameters.
|
||||
- Define interpolation modes and behavior at clip boundaries.
|
||||
- Evaluate automation at the correct time and sample offsets for each processing block.
|
||||
- Smooth parameter changes where needed to avoid zipper noise or audio artifacts.
|
||||
- Add MIDI controller mapping where appropriate.
|
||||
- Handle note-off, panic, seek, stop, project teardown, and device disconnect correctly.
|
||||
- Define deterministic ordering for MIDI and automation events that occur at the same sample position.
|
||||
- Test overlapping notes, repeated note-ons, and interrupted playback.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- Automation and MIDI control reproduce consistent results across repeated playback and project reloads.
|
||||
- Held notes are released safely on stop, panic, seek, and relevant failure paths.
|
||||
- Event ordering and interpolation behavior are documented and tested.
|
||||
|
||||
## Phase 17 — Editing Workflow, Undo/Redo, and Usability
|
||||
|
||||
**Goal:** Make common editing tasks efficient and protect users from accidental changes.
|
||||
|
||||
### Tasks
|
||||
|
||||
- Make common operations undoable, including create, delete, move, trim, duplicate, and connect.
|
||||
- Define undo behavior for recording and destructive operations.
|
||||
- Add consistent keyboard shortcuts and context menus for frequently used actions.
|
||||
- Implement predictable snapping, grid settings, zoom, scrolling, and selection behavior.
|
||||
- Improve clip labels, track organization, and visual feedback.
|
||||
- Make destructive actions difficult to trigger accidentally.
|
||||
- Keep the GUI responsive while audio is running.
|
||||
- Ensure undo/redo updates project state and the active engine graph consistently.
|
||||
- Test keyboard focus and accessibility of essential controls where practical.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- Common editing workflows can be completed without unnecessary dialogs or manual workarounds.
|
||||
- Undo/redo restores the intended project state and corresponding engine state.
|
||||
- GUI operations do not block the real-time audio thread.
|
||||
- Interaction behavior is documented and covered by relevant tests.
|
||||
|
||||
## Phase 18 — Project Format, Compatibility, and Portability
|
||||
|
||||
**Goal:** Keep project files valid, recoverable, and maintainable over time.
|
||||
|
||||
### Tasks
|
||||
|
||||
- Version the project format and implement explicit migration logic when the format changes.
|
||||
- Validate project data before constructing engine objects from it.
|
||||
- Handle malformed files, unknown node types, missing assets, and unavailable plugins.
|
||||
- Define project-folder behavior and reliable media path handling.
|
||||
- Make saving robust against interrupted writes, for example by writing a temporary file and replacing the destination safely where supported.
|
||||
- Test save/load round trips and migration from every format version that is supported.
|
||||
- Keep the format documented and preferably human-inspectable.
|
||||
- Avoid silently discarding unknown data during load/save unless the format policy explicitly requires it.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- Supported older projects load correctly or produce clear, actionable compatibility errors.
|
||||
- Malformed project files do not crash the application or trigger unsafe allocations.
|
||||
- Missing media and plugins are reported clearly.
|
||||
- Save/load round trips and format migrations have automated tests.
|
||||
|
||||
## Phase 19 — Performance, Scalability, and Stress Testing
|
||||
|
||||
**Goal:** Measure performance and establish predictable behavior as projects grow.
|
||||
|
||||
### Tasks
|
||||
|
||||
- Create repeatable benchmarks for increasing node counts and audio channel counts.
|
||||
- Measure processing time at supported sample rates and block sizes.
|
||||
- Profile graph compilation separately from real-time audio processing.
|
||||
- Identify unnecessary buffer copies, allocations, and redundant processing.
|
||||
- Test large arrangements, many clips, automation events, and active meters.
|
||||
- Define bounded processing limits and meaningful overload diagnostics.
|
||||
- Test under CPU pressure and during frequent GUI redraws.
|
||||
- Optimize only after profiling identifies a meaningful bottleneck.
|
||||
- Record baseline benchmark results so regressions can be detected.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- Performance targets and test conditions are documented.
|
||||
- Repeatable benchmarks cover representative small and large projects.
|
||||
- Overload behavior is defined and does not cause unbounded work in the callback.
|
||||
- Significant performance regressions can be detected through repeatable tests.
|
||||
|
||||
## Phase 20 — Release Engineering, Packaging, and Maintenance
|
||||
|
||||
**Goal:** Make the application straightforward to build, install, use, diagnose, and maintain.
|
||||
|
||||
### Tasks
|
||||
|
||||
- Provide reproducible release builds and clean build instructions.
|
||||
- Add Makefile targets for building, testing, installing, and uninstalling.
|
||||
- Package the application and required resources for the initial supported Linux target.
|
||||
- Review licenses and attribution for direct and bundled dependencies.
|
||||
- Document supported audio backends, required system packages, and configuration.
|
||||
- Add useful crash and error diagnostics without compromising real-time processing.
|
||||
- Test fresh installations, upgrades, missing dependencies, and removal.
|
||||
- Publish a basic user manual, developer documentation, and a known-limitations list.
|
||||
- Define a release checklist and a policy for handling regressions and project-format compatibility.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- A person other than the primary developer can build or install the application using the documented steps.
|
||||
- The installed application can complete the supported core workflow.
|
||||
- Installation and removal are tested on the initial target environment.
|
||||
- Dependencies, licensing, limitations, and troubleshooting steps are documented.
|
||||
- A release can be reproduced from a clean checkout with recorded build instructions.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Execution Order
|
||||
|
||||
Reorder the phases when the audit reveals a blocking dependency. Do not mark a phase complete merely because code exists: its acceptance criteria must be demonstrated by builds, tests, or documented manual verification.
|
||||
|
||||
## Definition of Done for Each Phase
|
||||
|
||||
A phase is complete only when:
|
||||
|
||||
- The project builds with warnings enabled.
|
||||
- Relevant automated tests pass, and manual tests are documented where automation is impractical.
|
||||
- Error paths, resource ownership, and thread boundaries have been reviewed.
|
||||
- Real-time audio constraints remain satisfied.
|
||||
- Documentation is updated alongside API or behavior changes.
|
||||
- No unrelated functionality was silently removed.
|
||||
- Untested behavior and known limitations are reported honestly.
|
||||
- The phase's acceptance criteria are explicitly checked off.
|
||||
|
||||
Do not skip phases without documenting why and preserving acceptance criteria.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
163
src/audio/wave.c
163
src/audio/wave.c
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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_;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 *);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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_;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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_;
|
||||
|
|
|
|||
29
src/main.c
29
src/main.c
|
|
@ -9,33 +9,42 @@
|
|||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc == 2 && !strcmp(argv[1], "--version")) {
|
||||
printf("%s %s\n", XTC_PROJECT_NAME, XTC_VERSION_STRING); return EXIT_SUCCESS;
|
||||
printf("%s %s\n", XTC_PROJECT_NAME, XTC_VERSION_STRING);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
if (argc == 2 && !strcmp(argv[1], "--help")) {
|
||||
puts("Usage: xtc-check [--version | --help | --validate PROJECT]\nNo arguments checks core startup/shutdown. Validation checks metadata only."); return EXIT_SUCCESS;
|
||||
puts("Usage: xtc-check [--version | --help | --validate PROJECT]\nNo arguments checks core startup/shutdown. "
|
||||
"Validation checks metadata only.");
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
if (argc == 3 && !strcmp(argv[1], "--validate")) {
|
||||
xtc_project *project = NULL;
|
||||
xtc_result result = xtc_project_create(&project);
|
||||
if (result == XTC_OK) result = xtc_project_load(project, argv[2]);
|
||||
if (result == XTC_OK)
|
||||
result = xtc_project_load(project, argv[2]);
|
||||
xtc_project_destroy(project);
|
||||
if (result != XTC_OK) { fprintf(stderr, "Project validation failed: %s\n", xtc_result_string(result)); return EXIT_FAILURE; }
|
||||
puts("Project metadata valid (external assets not loaded)."); return EXIT_SUCCESS;
|
||||
if (result != XTC_OK) {
|
||||
fprintf(stderr, "Project validation failed: %s\n", xtc_result_string(result));
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
puts("Project metadata valid (external assets not loaded).");
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
if (argc != 1) {
|
||||
fputs("Usage: xtc-check [--version | --help | --validate PROJECT]\n", stderr);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (argc != 1) { fputs("Usage: xtc-check [--version | --help | --validate PROJECT]\n", stderr); return EXIT_FAILURE; }
|
||||
|
||||
xtc_app *app = NULL;
|
||||
xtc_result result = xtc_app_create(&app);
|
||||
|
||||
if (result != XTC_OK) {
|
||||
(void)fprintf(stderr, "Application initialization failed: %s\n",
|
||||
xtc_result_string(result));
|
||||
(void)fprintf(stderr, "Application initialization failed: %s\n", xtc_result_string(result));
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
xtc_app_destroy(app);
|
||||
if (printf("%s %s: core startup/shutdown OK\n",
|
||||
XTC_PROJECT_NAME, XTC_VERSION_STRING) < 0) {
|
||||
if (printf("%s %s: core startup/shutdown OK\n", XTC_PROJECT_NAME, XTC_VERSION_STRING) < 0) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
139
src/midi/model.c
139
src/midi/model.c
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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). */
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -8,263 +8,389 @@
|
|||
int xtc_project_valid_name(const char *name)
|
||||
{
|
||||
size_t length = 0U, i = 0U;
|
||||
if (name == NULL) return 0;
|
||||
while (length < XTC_PROJECT_NAME_BYTES && name[length] != '\0') ++length;
|
||||
if (length == 0U || length == XTC_PROJECT_NAME_BYTES) return 0;
|
||||
if (name == NULL)
|
||||
return 0;
|
||||
while (length < XTC_PROJECT_NAME_BYTES && name[length] != '\0')
|
||||
++length;
|
||||
if (length == 0U || length == XTC_PROJECT_NAME_BYTES)
|
||||
return 0;
|
||||
while (i < length) {
|
||||
unsigned char byte = (unsigned char)name[i++];
|
||||
size_t extra, j;
|
||||
if (byte < 32U || byte == 127U) return 0;
|
||||
if (byte < 128U) continue;
|
||||
if (byte >= 194U && byte <= 223U) extra = 1U;
|
||||
else if (byte >= 224U && byte <= 239U) extra = 2U;
|
||||
else if (byte >= 240U && byte <= 244U) extra = 3U;
|
||||
else return 0;
|
||||
if (extra > length - i) return 0;
|
||||
if ((byte == 224U && (unsigned char)name[i] < 160U) ||
|
||||
(byte == 237U && (unsigned char)name[i] >= 160U) ||
|
||||
(byte == 240U && (unsigned char)name[i] < 144U) ||
|
||||
(byte == 244U && (unsigned char)name[i] >= 144U)) return 0;
|
||||
if (byte < 32U || byte == 127U)
|
||||
return 0;
|
||||
if (byte < 128U)
|
||||
continue;
|
||||
if (byte >= 194U && byte <= 223U)
|
||||
extra = 1U;
|
||||
else if (byte >= 224U && byte <= 239U)
|
||||
extra = 2U;
|
||||
else if (byte >= 240U && byte <= 244U)
|
||||
extra = 3U;
|
||||
else
|
||||
return 0;
|
||||
if (extra > length - i)
|
||||
return 0;
|
||||
if ((byte == 224U && (unsigned char)name[i] < 160U) || (byte == 237U && (unsigned char)name[i] >= 160U) ||
|
||||
(byte == 240U && (unsigned char)name[i] < 144U) || (byte == 244U && (unsigned char)name[i] >= 144U))
|
||||
return 0;
|
||||
for (j = 0U; j < extra; ++j) {
|
||||
unsigned char tail = (unsigned char)name[i++];
|
||||
if (tail < 128U || tail > 191U) return 0;
|
||||
if (tail < 128U || tail > 191U)
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static size_t track_index(const xtc_project *project, uint64_t id)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0U; i < project->track_count; ++i) if (project->tracks[i].id == id) return i;
|
||||
for (i = 0U; i < project->track_count; ++i)
|
||||
if (project->tracks[i].id == id)
|
||||
return i;
|
||||
return SIZE_MAX;
|
||||
}
|
||||
|
||||
static size_t clip_index(const xtc_project *project, uint64_t id)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0U; i < project->clip_count; ++i) if (project->clips[i].id == id) return i;
|
||||
for (i = 0U; i < project->clip_count; ++i)
|
||||
if (project->clips[i].id == id)
|
||||
return i;
|
||||
return SIZE_MAX;
|
||||
}
|
||||
|
||||
static int valid_range(uint64_t start, uint64_t length, uint64_t offset, uint64_t source)
|
||||
{
|
||||
return start <= XTC_PROJECT_MAX_TICK && length != 0U &&
|
||||
length <= XTC_PROJECT_MAX_TICK - start && source <= XTC_PROJECT_MAX_TICK &&
|
||||
offset <= source && length <= source - offset;
|
||||
return start <= XTC_PROJECT_MAX_TICK && length != 0U && length <= XTC_PROJECT_MAX_TICK - start &&
|
||||
source <= XTC_PROJECT_MAX_TICK && offset <= source && length <= source - offset;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_create(xtc_project **out_project)
|
||||
{
|
||||
xtc_project *project;
|
||||
xtc_result result;
|
||||
if (out_project == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (out_project == NULL)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
*out_project = NULL;
|
||||
project = calloc(1U, sizeof(*project));
|
||||
if (project == NULL) return XTC_ERR_OUT_OF_MEMORY;
|
||||
if (project == NULL)
|
||||
return XTC_ERR_OUT_OF_MEMORY;
|
||||
result = xtc_graph_create(64U, &project->graph);
|
||||
if (result != XTC_OK) { free(project); return result; }
|
||||
if (result != XTC_OK) {
|
||||
free(project);
|
||||
return result;
|
||||
}
|
||||
project->graph->require_output_nodes = 1;
|
||||
project->tempo = 120000U;
|
||||
*out_project = project;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
void xtc_project_destroy(xtc_project *project)
|
||||
{ if (project != NULL) { xtc_graph_destroy(project->graph); free(project); } }
|
||||
{
|
||||
if (project != NULL) {
|
||||
xtc_graph_destroy(project->graph);
|
||||
free(project);
|
||||
}
|
||||
}
|
||||
|
||||
const xtc_graph *xtc_project_graph(const xtc_project *project)
|
||||
{ return project == NULL ? NULL : project->graph; }
|
||||
{
|
||||
return project == NULL ? NULL : project->graph;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_set_graph(xtc_project *project, const xtc_graph *source)
|
||||
{
|
||||
xtc_graph *copy;
|
||||
xtc_result result;
|
||||
if (project == NULL || source == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (source->node_count > 64U || source->edge_count > 4096U) return XTC_ERR_CAPACITY;
|
||||
if (project == NULL || source == NULL)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (source->node_count > 64U || source->edge_count > 4096U)
|
||||
return XTC_ERR_CAPACITY;
|
||||
result = xtc_graph_validate(source);
|
||||
if (result != XTC_OK) return result;
|
||||
if (result != XTC_OK)
|
||||
return result;
|
||||
result = xtc_graph_create(64U, ©);
|
||||
if (result != XTC_OK) return result;
|
||||
copy->node_count = source->node_count; copy->edge_count = source->edge_count;
|
||||
copy->next_id = source->next_id; copy->require_output_nodes = source->require_output_nodes;
|
||||
if (result != XTC_OK)
|
||||
return result;
|
||||
copy->node_count = source->node_count;
|
||||
copy->edge_count = source->edge_count;
|
||||
copy->next_id = source->next_id;
|
||||
copy->require_output_nodes = source->require_output_nodes;
|
||||
memcpy(copy->nodes, source->nodes, source->node_count * sizeof(*copy->nodes));
|
||||
memcpy(copy->edges, source->edges, source->edge_count * sizeof(*copy->edges));
|
||||
xtc_graph_destroy(project->graph); project->graph = copy;
|
||||
xtc_graph_destroy(project->graph);
|
||||
project->graph = copy;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_clone(const xtc_project *source, xtc_project **out_project)
|
||||
{
|
||||
xtc_project *copy;
|
||||
xtc_result result;
|
||||
if (out_project == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (out_project == NULL)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
*out_project = NULL;
|
||||
if (source == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (source == NULL)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
copy = malloc(sizeof(*copy));
|
||||
if (copy == NULL) return XTC_ERR_OUT_OF_MEMORY;
|
||||
*copy = *source; copy->graph = NULL;
|
||||
if (copy == NULL)
|
||||
return XTC_ERR_OUT_OF_MEMORY;
|
||||
*copy = *source;
|
||||
copy->graph = NULL;
|
||||
result = xtc_project_set_graph(copy, source->graph);
|
||||
if (result != XTC_OK) { free(copy); return result; }
|
||||
if (result != XTC_OK) {
|
||||
free(copy);
|
||||
return result;
|
||||
}
|
||||
*out_project = copy;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_get_counts(const xtc_project *project, size_t *tracks, size_t *clips)
|
||||
{
|
||||
if (project == NULL || tracks == NULL || clips == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
||||
*tracks = project->track_count; *clips = project->clip_count; return XTC_OK;
|
||||
if (project == NULL || tracks == NULL || clips == NULL)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
*tracks = project->track_count;
|
||||
*clips = project->clip_count;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_get_track(const xtc_project *project, size_t index, xtc_track_info *track)
|
||||
{
|
||||
if (project == NULL || track == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (index >= project->track_count) return XTC_ERR_NOT_FOUND;
|
||||
*track = project->tracks[index]; return XTC_OK;
|
||||
if (project == NULL || track == NULL)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (index >= project->track_count)
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
*track = project->tracks[index];
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_get_clip(const xtc_project *project, size_t index, xtc_clip_info *clip)
|
||||
{
|
||||
if (project == NULL || clip == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (index >= project->clip_count) return XTC_ERR_NOT_FOUND;
|
||||
*clip = project->clips[index]; return XTC_OK;
|
||||
if (project == NULL || clip == NULL)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (index >= project->clip_count)
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
*clip = project->clips[index];
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_find_clip(const xtc_project *project, uint64_t id, xtc_clip_info *clip)
|
||||
{
|
||||
if (project == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (project == NULL)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
return xtc_project_get_clip(project, clip_index(project, id), clip);
|
||||
}
|
||||
|
||||
xtc_result xtc_project_add_track(xtc_project *project, const char *name, uint64_t *id)
|
||||
{
|
||||
xtc_track_info track = {0};
|
||||
if (project == NULL || id == NULL || !xtc_project_valid_name(name)) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (project->track_count == XTC_PROJECT_MAX_TRACKS || project->next_track_id == UINT64_MAX) return XTC_ERR_CAPACITY;
|
||||
track.id = project->next_track_id++; track.gain = 1.0f;
|
||||
if (project == NULL || id == NULL || !xtc_project_valid_name(name))
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (project->track_count == XTC_PROJECT_MAX_TRACKS || project->next_track_id == UINT64_MAX)
|
||||
return XTC_ERR_CAPACITY;
|
||||
track.id = project->next_track_id++;
|
||||
track.gain = 1.0f;
|
||||
memcpy(track.name, name, strlen(name) + 1U);
|
||||
project->tracks[project->track_count++] = track; *id = track.id; return XTC_OK;
|
||||
project->tracks[project->track_count++] = track;
|
||||
*id = track.id;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_remove_track(xtc_project *project, uint64_t id)
|
||||
{
|
||||
size_t index, i, count = 0U;
|
||||
if (project == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (project == NULL)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
index = track_index(project, id);
|
||||
if (index == SIZE_MAX) return XTC_ERR_NOT_FOUND;
|
||||
if (index == SIZE_MAX)
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
for (i = 0U; i < project->clip_count; ++i)
|
||||
if (project->clips[i].track_id != id) project->clips[count++] = project->clips[i];
|
||||
if (project->clips[i].track_id != id)
|
||||
project->clips[count++] = project->clips[i];
|
||||
project->clip_count = count;
|
||||
memmove(project->tracks + index, project->tracks + index + 1U,
|
||||
(project->track_count - index - 1U) * sizeof(*project->tracks));
|
||||
(project->track_count - index - 1U) * sizeof(*project->tracks));
|
||||
--project->track_count;
|
||||
count = 0;
|
||||
for (i = 0; i < project->note_count; ++i)
|
||||
if (clip_index(project, project->notes[i].clip_id) != SIZE_MAX) project->notes[count++] = project->notes[i];
|
||||
if (clip_index(project, project->notes[i].clip_id) != SIZE_MAX)
|
||||
project->notes[count++] = project->notes[i];
|
||||
project->note_count = count;
|
||||
count = 0;
|
||||
for (i = 0; i < project->automation_count; ++i)
|
||||
if (project->automation[i].track_id != id) project->automation[count++] = project->automation[i];
|
||||
project->automation_count = count; return XTC_OK;
|
||||
if (project->automation[i].track_id != id)
|
||||
project->automation[count++] = project->automation[i];
|
||||
project->automation_count = count;
|
||||
return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_project_add_clip(xtc_project *project, uint64_t track, const char *name,
|
||||
uint64_t start, uint64_t length, uint64_t *id)
|
||||
|
||||
xtc_result xtc_project_add_clip(xtc_project *project, uint64_t track, const char *name, uint64_t start, uint64_t length,
|
||||
uint64_t *id)
|
||||
{
|
||||
xtc_clip_info clip = {0};
|
||||
if (project == NULL || id == NULL || !xtc_project_valid_name(name) || !valid_range(start, length, 0U, length))
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (track_index(project, track) == SIZE_MAX) return XTC_ERR_NOT_FOUND;
|
||||
if (project->clip_count == XTC_PROJECT_MAX_CLIPS || project->next_clip_id == UINT64_MAX) return XTC_ERR_CAPACITY;
|
||||
clip.id = project->next_clip_id++; clip.track_id = track;
|
||||
clip.start_tick = start; clip.length_tick = clip.source_length_tick = length;
|
||||
if (track_index(project, track) == SIZE_MAX)
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
if (project->clip_count == XTC_PROJECT_MAX_CLIPS || project->next_clip_id == UINT64_MAX)
|
||||
return XTC_ERR_CAPACITY;
|
||||
clip.id = project->next_clip_id++;
|
||||
clip.track_id = track;
|
||||
clip.start_tick = start;
|
||||
clip.length_tick = clip.source_length_tick = length;
|
||||
memcpy(clip.name, name, strlen(name) + 1U);
|
||||
project->clips[project->clip_count++] = clip; *id = clip.id; return XTC_OK;
|
||||
project->clips[project->clip_count++] = clip;
|
||||
*id = clip.id;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_remove_clip(xtc_project *project, uint64_t id)
|
||||
{
|
||||
size_t index;
|
||||
if (project == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (project == NULL)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
index = clip_index(project, id);
|
||||
if (index == SIZE_MAX) return XTC_ERR_NOT_FOUND;
|
||||
if (index == SIZE_MAX)
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
memmove(project->clips + index, project->clips + index + 1U,
|
||||
(project->clip_count - index - 1U) * sizeof(*project->clips));
|
||||
(project->clip_count - index - 1U) * sizeof(*project->clips));
|
||||
--project->clip_count;
|
||||
{
|
||||
size_t i, count = 0;
|
||||
for (i = 0; i < project->note_count; ++i)
|
||||
if (project->notes[i].clip_id != id) project->notes[count++] = project->notes[i];
|
||||
if (project->notes[i].clip_id != id)
|
||||
project->notes[count++] = project->notes[i];
|
||||
project->note_count = count;
|
||||
}
|
||||
return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_project_edit_clip(xtc_project *project, uint64_t id, uint64_t track,
|
||||
uint64_t start, uint64_t length, uint64_t offset)
|
||||
|
||||
xtc_result xtc_project_edit_clip(xtc_project *project, uint64_t id, uint64_t track, uint64_t start, uint64_t length,
|
||||
uint64_t offset)
|
||||
{
|
||||
size_t index;
|
||||
if (project == NULL) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (project == NULL)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
index = clip_index(project, id);
|
||||
if (index == SIZE_MAX || track_index(project, track) == SIZE_MAX) return XTC_ERR_NOT_FOUND;
|
||||
if (!valid_range(start, length, offset, project->clips[index].source_length_tick)) return XTC_ERR_INVALID_ARGUMENT;
|
||||
project->clips[index].track_id = track; project->clips[index].start_tick = start;
|
||||
project->clips[index].length_tick = length; project->clips[index].offset_tick = offset;
|
||||
if (index == SIZE_MAX || track_index(project, track) == SIZE_MAX)
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
if (!valid_range(start, length, offset, project->clips[index].source_length_tick))
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
project->clips[index].track_id = track;
|
||||
project->clips[index].start_tick = start;
|
||||
project->clips[index].length_tick = length;
|
||||
project->clips[index].offset_tick = offset;
|
||||
return XTC_OK;
|
||||
}
|
||||
unsigned int xtc_project_tempo(const xtc_project *project) { return project == NULL ? 0U : project->tempo; }
|
||||
|
||||
unsigned int xtc_project_tempo(const xtc_project *project)
|
||||
{
|
||||
return project == NULL ? 0U : project->tempo;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_set_tempo(xtc_project *project, unsigned int tempo)
|
||||
{
|
||||
if (project == NULL || tempo < 20000U || tempo > 300000U) return XTC_ERR_INVALID_ARGUMENT;
|
||||
project->tempo = tempo; return XTC_OK;
|
||||
if (project == NULL || tempo < 20000U || tempo > 300000U)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
project->tempo = tempo;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_tick_to_sample(const xtc_project *project, uint64_t tick, unsigned int rate, uint64_t *sample)
|
||||
{
|
||||
if (project == NULL || sample == NULL || rate == 0U || rate > 768000U) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (tick > XTC_PROJECT_MAX_TICK) return XTC_ERR_CAPACITY;
|
||||
if (project == NULL || sample == NULL || rate == 0U || rate > 768000U)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (tick > XTC_PROJECT_MAX_TICK)
|
||||
return XTC_ERR_CAPACITY;
|
||||
*sample = tick * rate * UINT64_C(60000) / ((uint64_t)project->tempo * XTC_TICKS_PER_QUARTER);
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_sample_to_tick(const xtc_project *project, uint64_t sample, unsigned int rate, uint64_t *tick)
|
||||
{
|
||||
uint64_t scale, value, divisor, whole;
|
||||
if (project == NULL || tick == NULL || rate == 0U || rate > 768000U) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (project == NULL || tick == NULL || rate == 0U || rate > 768000U)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
scale = (uint64_t)project->tempo * XTC_TICKS_PER_QUARTER;
|
||||
divisor = (uint64_t)rate * UINT64_C(60000);
|
||||
whole = sample / divisor;
|
||||
if (whole > XTC_PROJECT_MAX_TICK / scale) return XTC_ERR_CAPACITY;
|
||||
if (whole > XTC_PROJECT_MAX_TICK / scale)
|
||||
return XTC_ERR_CAPACITY;
|
||||
value = whole * scale + (sample % divisor) * scale / divisor;
|
||||
if (value > XTC_PROJECT_MAX_TICK) return XTC_ERR_CAPACITY;
|
||||
*tick = value; return XTC_OK;
|
||||
if (value > XTC_PROJECT_MAX_TICK)
|
||||
return XTC_ERR_CAPACITY;
|
||||
*tick = value;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_validate(const xtc_project *project)
|
||||
{
|
||||
size_t i, j;
|
||||
if (project->tempo < 20000U || project->tempo > 300000U ||
|
||||
project->track_count > XTC_PROJECT_MAX_TRACKS || project->clip_count > XTC_PROJECT_MAX_CLIPS || project->note_count > XTC_PROJECT_MAX_NOTES) return XTC_ERR_FORMAT;
|
||||
if (project->tempo < 20000U || project->tempo > 300000U || project->track_count > XTC_PROJECT_MAX_TRACKS ||
|
||||
project->clip_count > XTC_PROJECT_MAX_CLIPS || project->note_count > XTC_PROJECT_MAX_NOTES)
|
||||
return XTC_ERR_FORMAT;
|
||||
for (i = 0U; i < project->track_count; ++i) {
|
||||
if (project->tracks[i].id >= project->next_track_id || !xtc_project_valid_name(project->tracks[i].name)) return XTC_ERR_FORMAT;
|
||||
for (j = 0U; j < i; ++j) if (project->tracks[j].id == project->tracks[i].id) return XTC_ERR_FORMAT;
|
||||
if (project->tracks[i].id >= project->next_track_id || !xtc_project_valid_name(project->tracks[i].name))
|
||||
return XTC_ERR_FORMAT;
|
||||
for (j = 0U; j < i; ++j)
|
||||
if (project->tracks[j].id == project->tracks[i].id)
|
||||
return XTC_ERR_FORMAT;
|
||||
}
|
||||
for (i = 0U; i < project->clip_count; ++i) {
|
||||
const xtc_clip_info *clip = project->clips + i;
|
||||
size_t path_length = 0U;
|
||||
while (path_length < XTC_ASSET_PATH_BYTES && clip->asset_path[path_length]) {
|
||||
unsigned char byte = (unsigned char)clip->asset_path[path_length++];
|
||||
if (byte < 32U || byte == 127U) return XTC_ERR_FORMAT;
|
||||
if (byte < 32U || byte == 127U)
|
||||
return XTC_ERR_FORMAT;
|
||||
}
|
||||
if (path_length == XTC_ASSET_PATH_BYTES || (path_length && clip->asset_path[0] != '/')) return XTC_ERR_FORMAT;
|
||||
if ((clip->midi != 0 && clip->midi != 1) || (clip->midi && path_length)) return XTC_ERR_FORMAT;
|
||||
if (path_length == XTC_ASSET_PATH_BYTES || (path_length && clip->asset_path[0] != '/'))
|
||||
return XTC_ERR_FORMAT;
|
||||
if ((clip->midi != 0 && clip->midi != 1) || (clip->midi && path_length))
|
||||
return XTC_ERR_FORMAT;
|
||||
if (clip->id >= project->next_clip_id || track_index(project, clip->track_id) == SIZE_MAX ||
|
||||
!xtc_project_valid_name(clip->name) || !valid_range(clip->start_tick, clip->length_tick, clip->offset_tick, clip->source_length_tick)) return XTC_ERR_FORMAT;
|
||||
for (j = 0U; j < i; ++j) if (project->clips[j].id == clip->id) return XTC_ERR_FORMAT;
|
||||
!xtc_project_valid_name(clip->name) ||
|
||||
!valid_range(clip->start_tick, clip->length_tick, clip->offset_tick, clip->source_length_tick))
|
||||
return XTC_ERR_FORMAT;
|
||||
for (j = 0U; j < i; ++j)
|
||||
if (project->clips[j].id == clip->id)
|
||||
return XTC_ERR_FORMAT;
|
||||
}
|
||||
for (i = 0; i < project->note_count; ++i) {
|
||||
const xtc_note_info *n = project->notes + i;
|
||||
size_t c = clip_index(project, n->clip_id);
|
||||
if (c == SIZE_MAX || !project->clips[c].midi || n->id >= project->next_note_id ||
|
||||
n->pitch > 127 || !n->velocity || n->velocity > 127 || n->channel > 15 ||
|
||||
!n->length_tick || n->start_tick >= project->clips[c].source_length_tick ||
|
||||
n->length_tick > project->clips[c].source_length_tick - n->start_tick) return XTC_ERR_FORMAT;
|
||||
for (j = 0; j < i; ++j) if (project->notes[j].id == n->id) return XTC_ERR_FORMAT;
|
||||
if (c == SIZE_MAX || !project->clips[c].midi || n->id >= project->next_note_id || n->pitch > 127 ||
|
||||
!n->velocity || n->velocity > 127 || n->channel > 15 || !n->length_tick ||
|
||||
n->start_tick >= project->clips[c].source_length_tick ||
|
||||
n->length_tick > project->clips[c].source_length_tick - n->start_tick)
|
||||
return XTC_ERR_FORMAT;
|
||||
for (j = 0; j < i; ++j)
|
||||
if (project->notes[j].id == n->id)
|
||||
return XTC_ERR_FORMAT;
|
||||
}
|
||||
if (project->automation_count > XTC_MAX_AUTOMATION_POINTS) return XTC_ERR_FORMAT;
|
||||
if (project->automation_count > XTC_MAX_AUTOMATION_POINTS)
|
||||
return XTC_ERR_FORMAT;
|
||||
for (i = 0; i < project->track_count; ++i) {
|
||||
const xtc_track_info *t = project->tracks + i;
|
||||
if (!isfinite(t->gain) || t->gain < 0 || t->gain > 4 || !isfinite(t->pan) || t->pan < -1 || t->pan > 1 ||
|
||||
(t->mute != 0 && t->mute != 1) || (t->solo != 0 && t->solo != 1)) return XTC_ERR_FORMAT;
|
||||
(t->mute != 0 && t->mute != 1) || (t->solo != 0 && t->solo != 1))
|
||||
return XTC_ERR_FORMAT;
|
||||
}
|
||||
for (i = 0; i < project->automation_count; ++i) {
|
||||
const xtc_automation_point *a = project->automation + i;
|
||||
if (track_index(project, a->track_id) == SIZE_MAX || a->tick > XTC_PROJECT_MAX_TICK ||
|
||||
!isfinite(a->value) || (a->parameter != XTC_AUTOMATION_GAIN && a->parameter != XTC_AUTOMATION_PAN) ||
|
||||
if (track_index(project, a->track_id) == SIZE_MAX || a->tick > XTC_PROJECT_MAX_TICK || !isfinite(a->value) ||
|
||||
(a->parameter != XTC_AUTOMATION_GAIN && a->parameter != XTC_AUTOMATION_PAN) ||
|
||||
a->value < (a->parameter == XTC_AUTOMATION_GAIN ? 0 : -1) ||
|
||||
a->value > (a->parameter == XTC_AUTOMATION_GAIN ? 4 : 1)) return XTC_ERR_FORMAT;
|
||||
for (j = 0; j < i; ++j) if (project->automation[j].track_id == a->track_id &&
|
||||
project->automation[j].parameter == a->parameter && project->automation[j].tick == a->tick) return XTC_ERR_FORMAT;
|
||||
a->value > (a->parameter == XTC_AUTOMATION_GAIN ? 4 : 1))
|
||||
return XTC_ERR_FORMAT;
|
||||
for (j = 0; j < i; ++j)
|
||||
if (project->automation[j].track_id == a->track_id && project->automation[j].parameter == a->parameter &&
|
||||
project->automation[j].tick == a->tick)
|
||||
return XTC_ERR_FORMAT;
|
||||
}
|
||||
return XTC_OK;
|
||||
}
|
||||
|
|
@ -272,14 +398,20 @@ xtc_result xtc_project_validate(const xtc_project *project)
|
|||
xtc_result xtc_project_set_clip_asset(xtc_project *project, uint64_t id, const char *path)
|
||||
{
|
||||
size_t index, length = 0U, i;
|
||||
if (!project || !path) return XTC_ERR_INVALID_ARGUMENT;
|
||||
while (length < XTC_ASSET_PATH_BYTES && path[length]) ++length;
|
||||
if (length == XTC_ASSET_PATH_BYTES || (length && path[0] != '/')) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (!project || !path)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
while (length < XTC_ASSET_PATH_BYTES && path[length])
|
||||
++length;
|
||||
if (length == XTC_ASSET_PATH_BYTES || (length && path[0] != '/'))
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
for (i = 0; i < length; ++i)
|
||||
if ((unsigned char)path[i] < 32U || path[i] == 127) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if ((unsigned char)path[i] < 32U || path[i] == 127)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
index = clip_index(project, id);
|
||||
if (index == SIZE_MAX) return XTC_ERR_NOT_FOUND;
|
||||
if (project->clips[index].midi) return XTC_ERR_INVALID_STATE;
|
||||
if (index == SIZE_MAX)
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
if (project->clips[index].midi)
|
||||
return XTC_ERR_INVALID_STATE;
|
||||
memmove(project->clips[index].asset_path, path, length + 1U);
|
||||
return XTC_OK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue