Phase 24: add mix buses, coherent peak meters and master clipping indicators
This commit is contained in:
parent
ba5c2503a7
commit
2fd7724065
32 changed files with 632 additions and 61 deletions
8
Makefile
8
Makefile
|
|
@ -399,3 +399,11 @@ test-routing: $(BUILD_DIR)/test_routing
|
|||
./$(BUILD_DIR)/test_routing
|
||||
test: test-routing
|
||||
-include $(BUILD_DIR)/tests/unit/test_routing.d
|
||||
|
||||
$(BUILD_DIR)/test_buses: $(BUILD_DIR)/tests/unit/test_buses.o $(BUILD_DIR)/libxtc.a
|
||||
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
|
||||
.PHONY: test-buses
|
||||
test-buses: $(BUILD_DIR)/test_buses
|
||||
./$(BUILD_DIR)/test_buses
|
||||
test: test-buses
|
||||
-include $(BUILD_DIR)/tests/unit/test_buses.d
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ This repository is a starter scaffold, not a production DAW. It currently provid
|
|||
- sample-counted transport, selection, move/trim, timeline zoom and scrolling;
|
||||
- WAV import/graph playback, bounded recording and missing-asset relinking;
|
||||
- MIDI clips, a sine instrument, JACK MIDI input and a basic piano roll;
|
||||
- stereo track gain/pan/mute/solo, output meters and gain/pan automation lanes;
|
||||
- stereo track/bus gain/pan/mute/solo, dBFS peak meters and gain/pan automation lanes;
|
||||
- bounded undo/redo with keyboard shortcuts;
|
||||
- configurable boundary fades, gain ramps and graceful GUI playback transitions;
|
||||
- native C processors, deterministic source packaging and staged install/uninstall;
|
||||
|
|
@ -138,3 +138,6 @@ Time stretching and independent pitch shifting remain deferred.
|
|||
Phase 23 adds signal-flow graph layout, routing diagnostics, direction/role
|
||||
indicators and atomic unity-gain insertion into a wire. See
|
||||
[ROUTING.md](docs/ROUTING.md); `make test-routing` verifies the C routing contract.
|
||||
|
||||
Phase 24 adds one-level mix buses, routing, coherent track/bus peaks and master
|
||||
clipping indicators, persisted in v9 (reading v1–v8). See [MIXER.md](docs/MIXER.md).
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
zoom/selection and graph positions are session UI state. Undo retains 32 edits
|
||||
plus baseline; allocation failures are reported, not silently hidden. Media is
|
||||
never removed by undo/delete. Broad screen-reader/accessibility testing is pending.
|
||||
- Format v8 reads v1–v7 and uses relative local media references. External absolute
|
||||
- Format v9 reads v1–v8 and uses relative local media references. External absolute
|
||||
media/module paths need manual repair after relocation. Saving does not bundle
|
||||
media; missing media stays referenced, missing plugins reject GUI installation.
|
||||
Unknown data is rejected rather than stripped. Post-rename directory-sync errors
|
||||
|
|
@ -42,3 +42,8 @@ PERFORMANCE.md, PLUGIN_HOSTING.md and RECOVERY.md for tested contracts/evidence.
|
|||
- Graph folders, per-track graph routing and track templates remain deferred:
|
||||
timeline and MIDI sources expose shared mixes. Layout is session-only; no
|
||||
feedback cycles, plugin isolation or latency compensation are supplied.
|
||||
|
||||
- Stereo one-level mix buses share the 64-track limit. Nested buses, independent
|
||||
bus plugin inserts, per-track aux sends and sidechains remain deferred. Track
|
||||
meters are pre-bus; bus meters are pre-graph; master is post-graph. Sample peaks
|
||||
and sticky clipping do not provide loudness, true-peak analysis or limiting.
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ recording. Graph edits retain their existing live publication behavior.
|
|||
Gain is a linear multiplier from 0 to 4. Pan ranges from -1 (left) to +1 (right):
|
||||
center has unity on both sides and moving away attenuates the opposite side
|
||||
linearly. This preserves existing centered mono levels; it is not an equal-power
|
||||
pan law. Mute wins over Solo. If any track is soloed, only soloed, unmuted tracks
|
||||
play. WAV clips and scheduled MIDI use their containing track's settings before
|
||||
pan law. Mute wins over Solo. Solo behavior across tracks and buses is defined below. WAV clips and scheduled MIDI use their containing track's settings before
|
||||
summing into the audio/instrument graph sources. Live MIDI input and independent
|
||||
graph oscillators/external input are unassigned sources and bypass track mixing.
|
||||
Stereo WAV files are still downmixed at import; stereo output comes from panning
|
||||
|
|
@ -52,12 +51,50 @@ uses explicit paired buffers, not new port IDs. Output buffers must be distinct;
|
|||
stereo callers must not alias them with source buffers. Graph-node meter reads
|
||||
report the maximum of both sides on the processing owner only.
|
||||
|
||||
Mixer Output L/R bars show last-block post-graph absolute peaks, polled at 100 ms.
|
||||
Peaks above unity turn red. Independent lock-free integer snapshots avoid sharing
|
||||
DSP pointers or float state between callback and GUI; stopped meters show zero.
|
||||
They are output meters, without per-track RMS, peak hold or loudness analysis.
|
||||
There is no limiter: finite summed output can exceed unity. GUI polling may miss
|
||||
brief peaks between refreshes; these bars are not overload history.
|
||||
Track and bus meters measure coherent audio plus scheduled MIDI sums, after their
|
||||
own gain/pan/mute/solo controls, before the shared modular graph. A track tap is
|
||||
before its parent bus: a muted bus can have active member-track meters. Bus taps
|
||||
are after bus controls. Master L/R measure the final graph output after transport
|
||||
fades; downstream effects and graph routing can change its level independently.
|
||||
|
||||
The backend retains interval sample-peak maxima until one serialized control
|
||||
reader consumes them with `xtc_backend_read_meters`. The GUI polls about every
|
||||
100 ms. Atomic channels are independent (not a simultaneous waveform snapshot);
|
||||
peaks are quantized to 0.000001 linear and capped at 1000. Legacy backend status
|
||||
peaks remain last-block linear values. GUI bars display 20*log10(peak) dBFS, floor
|
||||
-90, instantaneous attack and 20 dB/second release, with a +6 display ceiling.
|
||||
Each channel latches red CLIP at linear peak >=1 (0 dBFS) until Reset peaks.
|
||||
Reset is session state, not an audio edit. Selection changes reset selected-strip
|
||||
history. These are sample-peak meters, not RMS, LUFS or true-peak measurements.
|
||||
There is no limiter; summed levels can exceed unity.
|
||||
|
||||
## Mix buses and gain staging (Phase 24)
|
||||
|
||||
Use Add bus in Mixer, select an ordinary track, then choose its Route: Master or
|
||||
an existing bus. Buses share the 64-track capacity and gain/pan automation APIs.
|
||||
They contain no clips and feed master directly; nesting is rejected. Deleting a
|
||||
bus reroutes its members to master without deleting their clips; undo restores
|
||||
routing. Editing uses the existing stopped-transport preparation workflow.
|
||||
|
||||
Track controls apply first, then parent bus controls, then the shared graph.
|
||||
Gain multiplies (track 2 times bus 2 gives 4); pan uses the same center-unity
|
||||
balance law at both stages. There is no normalization or automatic attenuation.
|
||||
Solo is one inclusive domain: a soloed track passes through its bus, a soloed bus
|
||||
admits all its members, and unrelated tracks are silent. Track mute silences
|
||||
that member; bus mute silences its entire output even when a member is soloed.
|
||||
Live MIDI, external audio inputs and independent graph oscillators remain
|
||||
unassigned and outside this solo domain.
|
||||
|
||||
Master here means the source mix feeding the shared graph, not independent
|
||||
per-bus effect chains. Graph fan-out and gain branches permit parallel processing
|
||||
of shared sources. Independent per-track aux sends, sidechains, bus plugin
|
||||
inserts and nested buses are not represented and remain deferred.
|
||||
|
||||
Preparation allocates shared stereo monitor scratch on the control thread:
|
||||
2 * track_count * max_frames * sizeof(float), at most 32 MiB at current limits.
|
||||
The callback clears, accumulates and scans bounded buffers, with no allocation,
|
||||
I/O, locks or GUI calls. Immutable audio/MIDI snapshots borrow this scratch until
|
||||
stopped replacement or backend destruction.
|
||||
|
||||
## Undo and keyboard workflow
|
||||
|
||||
|
|
@ -96,3 +133,7 @@ saved/reopened hard-right playback and stereo peaks using only software clients.
|
|||
A native Xvfb mouse smoke check covers sliders and automation drawing.
|
||||
Physical stereo devices, listening quality, realtime endurance and high-density
|
||||
project performance remain unverified; no new dependency was introduced.
|
||||
|
||||
`test_buses` covers bus routing, exact taps, audio/MIDI cancellation, controls,
|
||||
automation, persistence, invalid routes, deletion/undo and dBFS ballistics. Backend
|
||||
tests cover consuming interval peaks; GUI tests cover routing undo and clipping.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Project format v8 (reads v1–v7)
|
||||
# Project format v9 (reads v1–v8)
|
||||
|
||||
Phases 5–15 use a bounded text format implemented in C99 without a parser library.
|
||||
The extension is `.xtc`. File operations run exclusively on the control thread.
|
||||
|
|
@ -192,7 +192,7 @@ or a claim of power-cut qualification.
|
|||
|
||||
## Phase 22: version 8 audio clip settings
|
||||
|
||||
Current writer emits v8 and reads v1–v7 with unity clip gain and no explicit
|
||||
Phase 22 introduced v8 and reads v1–v7 with unity clip gain and no explicit
|
||||
clip fades. After **every** clip's `MIDI` record (including silent and MIDI
|
||||
clips), v8 requires:
|
||||
|
||||
|
|
@ -215,3 +215,15 @@ a fresh ID/source offset, retains outer explicit fades and clears inner ones.
|
|||
No waveform cache or source audio is serialized. Checked-in v1–v7 migrations,
|
||||
v8 exact roundtrips, malformed new metadata and byte-mutation tests cover the
|
||||
format change (`make test test-portability test-audio-editing`).
|
||||
|
||||
## Phase 24: version 9 mix routing
|
||||
|
||||
The writer emits v9. Immediately after each TRACK's MIX record it writes
|
||||
`ROUTE is_bus bus_id`. `is_bus` is 0 or 1; `bus_id` is an existing bus track ID,
|
||||
or decimal UINT64_MAX (18446744073709551615) for master. Buses must route to
|
||||
master and cannot contain clips. Invalid destinations, flags and nested routing
|
||||
are rejected. v1–v8 migrate all tracks to ordinary direct-to-master tracks.
|
||||
Bus controls and automation use existing MIX and POINT records. Older readers
|
||||
reject v9 rather than discard routing. The public `xtc_track_info` grew fields;
|
||||
API consumers must rebuild. Fixtures cover v1–v8 migration and tests exercise
|
||||
v9 exact roundtrips and malformed routing.
|
||||
|
|
|
|||
|
|
@ -587,10 +587,26 @@ Provide the essential mixing controls needed to finish a small or medium-sized p
|
|||
- Test mute, solo, pan, gain, and routing interactions.
|
||||
|
||||
### Acceptance criteria
|
||||
- Mixer controls behave consistently across tracks, buses, and master output.
|
||||
- Meter readings correspond to documented signal points and units.
|
||||
- Solo and mute behavior is covered by automated tests.
|
||||
- No mixer operation introduces allocations, locks, or other prohibited work in the audio callback.
|
||||
- [x] Mixer controls behave consistently across tracks, buses, and master output.
|
||||
- [x] Meter readings correspond to documented signal points and units.
|
||||
- [x] Solo and mute behavior is covered by automated tests.
|
||||
- [x] No mixer operation introduces allocations, locks, or other prohibited work in the audio callback.
|
||||
|
||||
Implemented: one-level stereo mix buses with routing, shared gain/pan automation,
|
||||
inclusive solo and mute precedence; coherent audio/MIDI track and bus peaks;
|
||||
interval master peaks, dBFS ballistics and sticky clipping; v9 persistence reading
|
||||
v1–v8. See MIXER.md for exact taps, gain staging and callback scratch bounds.
|
||||
Independent per-track aux sends/sidechains and per-bus graph effects remain
|
||||
conditional/deferred because graph sources still expose shared mixes.
|
||||
|
||||
Validation: warning-enabled core/GUI builds and unit/integration tests, bus and
|
||||
backend fault tests, GUI plugin/workflow tests, software JACK GUI/audio/MIDI,
|
||||
allocation-failure/callback-allocation guard, static analysis, installation and
|
||||
package relocation, optional-dependency-free build, and core/GUI ASan+UBSan.
|
||||
The live MIDI connection test required one retry after a JACK connection failure;
|
||||
its full playback assertions passed on retry. GUI leak checks use the existing
|
||||
fontconfig suppression; core checks are unsuppressed. Physical hardware,
|
||||
human listening and extended high-density endurance remain unverified.
|
||||
|
||||
## Phase 25 — Plugin and Instrument Usability
|
||||
|
||||
|
|
|
|||
|
|
@ -58,9 +58,9 @@ allocates a fresh stable node ID and is **one** semantic edit/undo step. It
|
|||
preserves all other edges. Failure (including capacity, a missing/stale wire or
|
||||
backend refusal) keeps the previous graph and plan. It introduces no new node
|
||||
type or project record: v8 persistence already represents the resulting graph.
|
||||
There are no track templates, folders or per-track graph buses yet. The flat
|
||||
track model and shared source buffers cannot represent those relationships
|
||||
faithfully; these conditional roadmap tasks remain deferred.
|
||||
Phase 24 adds one-level stereo mix buses before shared graph sources (see
|
||||
MIXER.md). Track templates, folders and independent per-track/bus graph inserts
|
||||
remain deferred because graph sources still expose shared mixes.
|
||||
|
||||
## Transactions, cycles and realtime ownership
|
||||
|
||||
|
|
|
|||
|
|
@ -125,3 +125,8 @@ branches. L arranges nodes by signal flow. Select a wire and press I to insert a
|
|||
unity gain/bus node as one undoable edit. These commands are also in View/Edit
|
||||
and the graph context menu. Unwired effect inputs use external audio, as shown
|
||||
by “ext in”; they do not imply silence. See [ROUTING.md](ROUTING.md).
|
||||
|
||||
Phase 24: Mixer also offers Add bus and a Route choice for ordinary tracks.
|
||||
Select Track/Bus strips to edit their controls and automation. Master and selected
|
||||
strip meters show dBFS with 20 dB/s release; Reset peaks clears latched clipping.
|
||||
See MIXER.md for solo rules, gain staging and meter taps.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,17 @@
|
|||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Control-consumed interval maxima; quantized to 1e-6 linear, capped at 1000.
|
||||
* Track tap is post-track controls/solo, pre-bus; bus tap is post-bus controls,
|
||||
* both before the modular graph. Master is post-graph and transport envelope.
|
||||
* Readers consume maxima, so use one control-thread reader at UI refresh rate. */
|
||||
typedef struct { uint64_t id; float left, right; } xtc_track_peak;
|
||||
typedef struct {
|
||||
size_t count;
|
||||
xtc_track_peak tracks[XTC_PROJECT_MAX_TRACKS];
|
||||
float master_left, master_right;
|
||||
} xtc_meter_snapshot;
|
||||
|
||||
typedef struct xtc_backend xtc_backend;
|
||||
|
||||
typedef enum {
|
||||
|
|
@ -229,6 +240,8 @@ xtc_result xtc_backend_connect(xtc_backend *backend, xtc_port_direction own_dire
|
|||
*/
|
||||
xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction own_direction, const char *remote_name);
|
||||
|
||||
xtc_result xtc_backend_read_meters(xtc_backend *backend, xtc_meter_snapshot *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -21,6 +21,18 @@ typedef struct {
|
|||
float value;
|
||||
} xtc_automation_point;
|
||||
|
||||
/* Buses share the 64 track/automation capacities. A bus cannot contain clips
|
||||
* or feed another bus. Deleting a bus returns its member tracks to master.
|
||||
* Solo is inclusive: a solo bus admits all its members, a solo track admits
|
||||
* itself through its bus; bus mute always wins at the bus output. */
|
||||
xtc_result xtc_project_add_bus(xtc_project *project, const char *name, uint64_t *out_id);
|
||||
xtc_result xtc_project_route_track(xtc_project *project, uint64_t track, uint64_t bus_or_master);
|
||||
/* UI/control-thread sample peak display, dBFS floor -90; instant attack,
|
||||
* 20 dB/s release, sticky >=1 clip indication. No loudness/true-peak estimate. */
|
||||
typedef struct { float db; int clipped; } xtc_peak_meter;
|
||||
void xtc_peak_meter_reset(xtc_peak_meter *meter);
|
||||
xtc_result xtc_peak_meter_update(xtc_peak_meter *meter, float linear_peak, double seconds);
|
||||
|
||||
/* 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,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define XTC_PROJECT_FORMAT_VERSION 8U
|
||||
#define XTC_PROJECT_FORMAT_VERSION 9U
|
||||
#define XTC_PROJECT_MAX_TRACKS 64U
|
||||
#define XTC_PROJECT_MAX_CLIPS 1024U
|
||||
#define XTC_PROJECT_NAME_BYTES 64U
|
||||
|
|
@ -27,6 +27,8 @@ typedef struct {
|
|||
char name[XTC_PROJECT_NAME_BYTES];
|
||||
float gain, pan;
|
||||
int mute, solo;
|
||||
int is_bus; /* One-level summing bus; no clips, routes only to master. */
|
||||
uint64_t bus_id; /* UINT64_MAX = master, otherwise an existing bus ID. */
|
||||
} xtc_track_info;
|
||||
|
||||
/* Arrangement region: empty asset_path means silent metadata.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ typedef struct {
|
|||
} region;
|
||||
|
||||
struct xtc_audio_plan {
|
||||
size_t fade_frames;
|
||||
size_t fade_frames, monitor_stride;
|
||||
float *monitor;
|
||||
xtc_mix_snapshot mix;
|
||||
xtc_wave waves[64];
|
||||
char paths[64][XTC_ASSET_PATH_BYTES];
|
||||
|
|
@ -288,6 +289,7 @@ xtc_result xtc_audio_plan_render_stereo(const xtc_audio_plan *plan, uint64_t pos
|
|||
}
|
||||
sample *= r->gain * envelope;
|
||||
}
|
||||
if (plan->monitor) xtc_mix_monitor(&plan->mix, plan->monitor, plan->monitor_stride, r->track, position+j, j, sample, right != NULL);
|
||||
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);
|
||||
|
|
@ -315,3 +317,6 @@ xtc_result xtc_audio_plan_render(const xtc_audio_plan *p, uint64_t position, flo
|
|||
{
|
||||
return xtc_audio_plan_render_stereo(p, position, out, NULL, frames);
|
||||
}
|
||||
|
||||
void xtc_audio_plan_monitor(xtc_audio_plan *p, float *samples, size_t stride)
|
||||
{ if (p) { p->monitor = samples; p->monitor_stride = stride; } }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "xtc/xtc_backend.h"
|
||||
#include "xtc/xtc_realtime.h"
|
||||
#include "xtc/xtc_dsp.h"
|
||||
#include "../mixer/mix_internal.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
|
@ -18,7 +19,10 @@ struct xtc_backend {
|
|||
xtc_midi_plan *midi;
|
||||
unsigned int panic_requested, panic_seen, midi_dropped, peak_left, peak_right;
|
||||
xtc_recorder *recorder; /* Borrowed, installed while callbacks quiescent. */
|
||||
float *timeline, *instrument;
|
||||
float *timeline, *instrument, *monitor;
|
||||
size_t meter_count;
|
||||
uint64_t meter_ids[XTC_PROJECT_MAX_TRACKS];
|
||||
unsigned int meter_peaks[XTC_PROJECT_MAX_TRACKS][2], master_peaks[2];
|
||||
size_t max_frames;
|
||||
size_t transition_frames; /* Control owner while stopped. */
|
||||
xtc_gain_ramp ramp; /* Audio owner while active. */
|
||||
|
|
@ -31,6 +35,15 @@ struct xtc_backend {
|
|||
int ready, running; /* Control thread only. */
|
||||
};
|
||||
|
||||
/* One producer, one exchanging reader; at most one CAS, never a retry loop. */
|
||||
static void publish_peak(unsigned int *slot, float peak)
|
||||
{
|
||||
unsigned int value = (unsigned int)(fminf(isfinite(peak) ? peak : 0, 1000) * 1000000);
|
||||
unsigned int old = __atomic_load_n(slot, __ATOMIC_RELAXED);
|
||||
if (value > old && !__atomic_compare_exchange_n(slot, &old, value, 0, __ATOMIC_RELAXED, __ATOMIC_RELAXED))
|
||||
__atomic_store_n(slot, value, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Decodes queued JACK MIDI and renders the instrument for the current audio block.
|
||||
*
|
||||
|
|
@ -127,6 +140,11 @@ static int process(jack_nframes_t frames, void *context)
|
|||
(void)xtc_realtime_position(backend->engine,position);
|
||||
if (backend->recorder)
|
||||
xtc_recorder_push(backend->recorder, input, (size_t)frames);
|
||||
{
|
||||
size_t channel;
|
||||
for (channel = 0; channel < backend->meter_count*2; ++channel)
|
||||
memset(backend->monitor + channel*backend->max_frames, 0, (size_t)frames*sizeof(float));
|
||||
}
|
||||
if (backend->audio)
|
||||
result =
|
||||
xtc_audio_plan_render_stereo(backend->audio, position, backend->timeline,
|
||||
|
|
@ -175,6 +193,20 @@ static int process(jack_nframes_t frames, void *context)
|
|||
if (r > right_peak)
|
||||
right_peak = r;
|
||||
}
|
||||
publish_peak(backend->master_peaks, left_peak);
|
||||
publish_peak(backend->master_peaks+1, right_peak);
|
||||
if (result == XTC_OK) {
|
||||
size_t track, side, frame;
|
||||
for (track = 0; track < backend->meter_count; ++track)
|
||||
for (side = 0; side < 2; ++side) {
|
||||
float peak = 0;
|
||||
for (frame = 0; frame < frames; ++frame) {
|
||||
float value = fabsf(backend->monitor[(track*2+side)*backend->max_frames+frame]);
|
||||
if (isfinite(value) && value > peak) peak = value;
|
||||
}
|
||||
publish_peak(&backend->meter_peaks[track][side], peak);
|
||||
}
|
||||
}
|
||||
__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);
|
||||
}
|
||||
|
|
@ -284,14 +316,16 @@ xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out_
|
|||
backend->instrument = calloc(config->max_frames * config->channel_count, sizeof(float));
|
||||
if (!backend->timeline || !backend->instrument) {
|
||||
free(backend->timeline);
|
||||
free(backend->instrument);
|
||||
free(backend->monitor);
|
||||
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->monitor);
|
||||
free(backend->instrument);
|
||||
free(backend->timeline);
|
||||
free(backend);
|
||||
return XTC_ERR_IO;
|
||||
|
|
@ -337,6 +371,7 @@ fail:
|
|||
xtc_realtime_destroy(backend->engine);
|
||||
xtc_audio_plan_destroy(backend->audio);
|
||||
xtc_midi_plan_destroy(backend->midi);
|
||||
free(backend->monitor);
|
||||
free(backend->instrument);
|
||||
free(backend->timeline);
|
||||
free(backend);
|
||||
|
|
@ -359,6 +394,7 @@ xtc_result xtc_backend_close(xtc_backend *backend)
|
|||
xtc_realtime_destroy(backend->engine);
|
||||
xtc_audio_plan_destroy(backend->audio);
|
||||
xtc_midi_plan_destroy(backend->midi);
|
||||
free(backend->monitor);
|
||||
free(backend->instrument);
|
||||
free(backend->timeline);
|
||||
free(backend);
|
||||
|
|
@ -487,12 +523,19 @@ xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *
|
|||
{
|
||||
xtc_audio_plan *candidate = NULL;
|
||||
xtc_midi_plan *midi = NULL;
|
||||
float *monitor = NULL;
|
||||
size_t tracks = 0, clips = 0, i;
|
||||
xtc_result result;
|
||||
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);
|
||||
result = xtc_project_get_counts(project, &tracks, &clips);
|
||||
if (result == XTC_OK && tracks) {
|
||||
monitor = calloc(tracks*2*backend->max_frames, sizeof(float));
|
||||
if (!monitor) result = XTC_ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
if (result == XTC_OK) result = xtc_audio_plan_create(project, backend->expected_rate, &candidate);
|
||||
if (result == XTC_OK)
|
||||
result = xtc_audio_plan_set_fade_frames(candidate, backend->transition_frames);
|
||||
if (result == XTC_OK)
|
||||
|
|
@ -504,8 +547,21 @@ xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *
|
|||
if (result != XTC_OK) {
|
||||
xtc_audio_plan_destroy(candidate);
|
||||
xtc_midi_plan_destroy(midi);
|
||||
free(monitor);
|
||||
return result;
|
||||
}
|
||||
free(backend->monitor);
|
||||
backend->monitor = monitor;
|
||||
backend->meter_count = tracks;
|
||||
memset(backend->meter_peaks, 0, sizeof(backend->meter_peaks));
|
||||
memset(backend->master_peaks, 0, sizeof(backend->master_peaks));
|
||||
for (i = 0; i < tracks; ++i) {
|
||||
xtc_track_info track;
|
||||
(void)xtc_project_get_track(project, i, &track);
|
||||
backend->meter_ids[i] = track.id;
|
||||
}
|
||||
xtc_audio_plan_monitor(candidate, monitor, backend->max_frames);
|
||||
xtc_midi_plan_monitor(midi, monitor, backend->max_frames);
|
||||
xtc_audio_plan_destroy(backend->audio);
|
||||
backend->audio = candidate;
|
||||
xtc_midi_plan_destroy(backend->midi);
|
||||
|
|
@ -773,6 +829,22 @@ xtc_result xtc_backend_disconnect(xtc_backend *backend, xtc_port_direction direc
|
|||
return route(backend, direction, remote, 1);
|
||||
}
|
||||
|
||||
xtc_result xtc_backend_read_meters(xtc_backend *backend, xtc_meter_snapshot *out)
|
||||
{
|
||||
size_t i;
|
||||
if (!backend || !out) return XTC_ERR_INVALID_ARGUMENT;
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->count = backend->meter_count;
|
||||
for (i = 0; i < out->count; ++i) {
|
||||
out->tracks[i].id = backend->meter_ids[i];
|
||||
out->tracks[i].left = (float)__atomic_exchange_n(&backend->meter_peaks[i][0], 0U, __ATOMIC_RELAXED)/1000000;
|
||||
out->tracks[i].right = (float)__atomic_exchange_n(&backend->meter_peaks[i][1], 0U, __ATOMIC_RELAXED)/1000000;
|
||||
}
|
||||
out->master_left = (float)__atomic_exchange_n(backend->master_peaks, 0U, __ATOMIC_RELAXED)/1000000;
|
||||
out->master_right = (float)__atomic_exchange_n(backend->master_peaks+1, 0U, __ATOMIC_RELAXED)/1000000;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
#else
|
||||
/**
|
||||
* @brief Reports that output name is unavailable without JACK support.
|
||||
|
|
@ -1038,4 +1110,7 @@ xtc_result xtc_backend_request_stop(xtc_backend *backend)
|
|||
return XTC_ERR_UNSUPPORTED;
|
||||
}
|
||||
|
||||
|
||||
xtc_result xtc_backend_read_meters(xtc_backend *backend, xtc_meter_snapshot *out)
|
||||
{ (void)backend; (void)out; return XTC_ERR_UNSUPPORTED; }
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -521,8 +521,7 @@ void MainWindow::reconnect_engine()
|
|||
*/
|
||||
void MainWindow::refresh_status()
|
||||
{
|
||||
if (mixer_)
|
||||
mixer_->peaks(0, 0);
|
||||
if (mixer_ && !ready_) { mixer_->peaks(0, 0); mixer_->track_peaks(0, 0); }
|
||||
bool can_play = false, can_record = false;
|
||||
if (!ready_) {
|
||||
set_label(indicator_, "Audio: disconnected");
|
||||
|
|
@ -558,7 +557,15 @@ void MainWindow::refresh_status()
|
|||
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);
|
||||
xtc_meter_snapshot meters{};
|
||||
float track_left = 0, track_right = 0;
|
||||
if (xtc_backend_read_meters(backend_, &meters) == XTC_OK) {
|
||||
mixer_->peaks(meters.master_left, meters.master_right);
|
||||
for (size_t i = 0; i < meters.count; ++i) if (meters.tracks[i].id == mixer_->selected_track()) {
|
||||
track_left = meters.tracks[i].left; track_right = meters.tracks[i].right; break;
|
||||
}
|
||||
} else mixer_->peaks(status.peak_left, status.peak_right);
|
||||
mixer_->track_peaks(track_left, track_right);
|
||||
char text[256];
|
||||
std::snprintf(
|
||||
text, sizeof(text),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include <FL/Fl.H>
|
||||
#include <FL/Fl_Box.H>
|
||||
#include <FL/Fl_Check_Button.H>
|
||||
#include <FL/Fl_Button.H>
|
||||
#include <FL/Fl_Choice.H>
|
||||
#include <FL/Fl_Progress.H>
|
||||
#include <FL/Fl_Value_Input.H>
|
||||
|
|
@ -193,26 +194,39 @@ MixerWidget::MixerWidget(int x, int y, int w, int h, xtc_project *p, BeforeEdit
|
|||
solo_ = new Fl_Check_Button(x + 440, y + 8, 90, 26, "Solo");
|
||||
mute_->callback(control, this);
|
||||
solo_->callback(control, this);
|
||||
routes_ = new Fl_Choice(x + 600, y + 8, 180, 26, "Route");
|
||||
routes_->callback([](Fl_Widget *, void *c) {
|
||||
auto *m = static_cast<MixerWidget *>(c);
|
||||
int index = m->routes_->value();
|
||||
if (index >= 0 && static_cast<size_t>(index) < m->route_ids_.size()) m->route(m->route_ids_[static_cast<size_t>(index)]);
|
||||
}, this);
|
||||
auto *bus = new Fl_Button(x + 820, y + 8, 110, 26, "Add bus");
|
||||
bus->callback([](Fl_Widget *, void *c) { static_cast<MixerWidget *>(c)->add_bus(); }, this);
|
||||
left_ = new Fl_Progress(x + 70, y + 90, 320, 20, "Output L");
|
||||
right_ = new Fl_Progress(x + 480, y + 90, 320, 20, "Output R");
|
||||
for (auto *meter : {left_, right_}) {
|
||||
meter->minimum(0);
|
||||
meter->maximum(1);
|
||||
track_left_ = new Fl_Progress(x + 70, y + 116, 320, 20, "Track L");
|
||||
track_right_ = new Fl_Progress(x + 480, y + 116, 320, 20, "Track R");
|
||||
for (auto *meter : {left_, right_, track_left_, track_right_}) {
|
||||
meter->minimum(-90);
|
||||
meter->maximum(6);
|
||||
meter->selection_color(fl_rgb_color(80, 180, 120));
|
||||
}
|
||||
parameter_ = new Fl_Choice(x + 110, y + 126, 150, 26, "Automation");
|
||||
auto *reset = new Fl_Button(x + 820, y + 90, 110, 46, "Reset peaks");
|
||||
reset->callback([](Fl_Widget *, void *c) { static_cast<MixerWidget *>(c)->reset_meters(); }, this);
|
||||
reset_meters();
|
||||
parameter_ = new Fl_Choice(x + 110, y + 146, 150, 26, "Automation");
|
||||
parameter_->add("Gain|Pan");
|
||||
parameter_->value(0);
|
||||
parameter_->callback(select, this);
|
||||
start_ = new Fl_Value_Input(x + 420, y + 126, 140, 26, "First tick");
|
||||
start_ = new Fl_Value_Input(x + 420, y + 146, 140, 26, "First tick");
|
||||
start_->bounds(0, XTC_PROJECT_MAX_TICK - lane_ticks);
|
||||
start_->step(3840);
|
||||
start_->callback(select, this);
|
||||
auto *hint = new Fl_Box(x + 10, y + 160, w - 20, 24,
|
||||
auto *hint = new Fl_Box(x + 10, y + 180, w - 20, 24,
|
||||
"Gain 0..4; pan -1 left / +1 right. M/S toggles; arrows change gain, "
|
||||
"Shift+arrows pan; Up/Down selects track.");
|
||||
hint->labelsize(12);
|
||||
lane_ = new AutomationLane(x + 10, y + 192, w - 20, h - 222, *this);
|
||||
lane_ = new AutomationLane(x + 10, y + 212, w - 20, h - 242, *this);
|
||||
end();
|
||||
set_project(p);
|
||||
}
|
||||
|
|
@ -267,6 +281,8 @@ void MixerWidget::set_project(xtc_project *p)
|
|||
uint64_t selected = project_ == p ? selected_track() : UINT64_MAX;
|
||||
project_ = p;
|
||||
tracks_->clear();
|
||||
routes_->clear(); routes_->add("Master");
|
||||
route_ids_.clear(); route_ids_.push_back(UINT64_MAX);
|
||||
size_t n = 0, c = 0;
|
||||
xtc_project_get_counts(p, &n, &c);
|
||||
int index = 0;
|
||||
|
|
@ -279,7 +295,8 @@ void MixerWidget::set_project(xtc_project *p)
|
|||
label += '\\';
|
||||
label += ch;
|
||||
}
|
||||
tracks_->add(label.c_str());
|
||||
tracks_->add((std::string(t.is_bus ? "Bus: " : "Track: ") + label).c_str());
|
||||
if (t.is_bus) { routes_->add(label.c_str()); route_ids_.push_back(t.id); }
|
||||
if (t.id == selected)
|
||||
index = static_cast<int>(i);
|
||||
}
|
||||
|
|
@ -311,6 +328,13 @@ void MixerWidget::selection()
|
|||
pan_->value(t.pan);
|
||||
mute_->value(t.mute);
|
||||
solo_->value(t.solo);
|
||||
if (valid && !t.is_bus) routes_->activate(); else routes_->deactivate();
|
||||
routes_->value(0);
|
||||
for (size_t i = 0; i < route_ids_.size(); ++i) if (route_ids_[i] == t.bus_id) routes_->value(static_cast<int>(i));
|
||||
if (meter_track_ != selected_track()) {
|
||||
meter_track_ = selected_track();
|
||||
xtc_peak_meter_reset(track_meter_); xtc_peak_meter_reset(track_meter_+1);
|
||||
}
|
||||
lane_->redraw();
|
||||
}
|
||||
|
||||
|
|
@ -325,10 +349,12 @@ void MixerWidget::selection()
|
|||
*/
|
||||
void MixerWidget::peaks(float left, float right)
|
||||
{
|
||||
left_->value(std::min(left, 1.0f));
|
||||
right_->value(std::min(right, 1.0f));
|
||||
left_->selection_color(left > 1 ? FL_RED : FL_GREEN);
|
||||
right_->selection_color(right > 1 ? FL_RED : FL_GREEN);
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
meter_elapsed_ = std::clamp(std::chrono::duration<double>(now-peak_time_).count(), 0.0, 60.0);
|
||||
peak_time_ = now;
|
||||
(void)xtc_peak_meter_update(master_, left, meter_elapsed_);
|
||||
(void)xtc_peak_meter_update(master_+1, right, meter_elapsed_);
|
||||
display_peaks(left_, right_, master_, "Master");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -456,3 +482,53 @@ int MixerWidget::handle(int e)
|
|||
}
|
||||
return Fl_Group::handle(e);
|
||||
}
|
||||
|
||||
void MixerWidget::display_peaks(Fl_Progress *left, Fl_Progress *right, xtc_peak_meter *meters, const char *prefix)
|
||||
{
|
||||
for (unsigned int side = 0; side < 2; ++side) {
|
||||
auto *bar = side ? right : left;
|
||||
char label[80];
|
||||
std::snprintf(label, sizeof(label), "%s %c %.1f dBFS%s", prefix, side ? 'R' : 'L',
|
||||
static_cast<double>(meters[side].db), meters[side].clipped ? " CLIP" : "");
|
||||
bar->copy_label(label);
|
||||
bar->value(std::clamp(meters[side].db, -90.0f, 6.0f));
|
||||
bar->selection_color(meters[side].clipped ? FL_RED : FL_GREEN);
|
||||
}
|
||||
}
|
||||
void MixerWidget::track_peaks(float left, float right)
|
||||
{
|
||||
(void)xtc_peak_meter_update(track_meter_, left, meter_elapsed_);
|
||||
(void)xtc_peak_meter_update(track_meter_+1, right, meter_elapsed_);
|
||||
xtc_track_info track{};
|
||||
bool bus = xtc_project_get_track(project_, static_cast<size_t>(tracks_->value()), &track) == XTC_OK && track.is_bus;
|
||||
display_peaks(track_left_, track_right_, track_meter_, bus ? "Bus" : "Track");
|
||||
}
|
||||
void MixerWidget::reset_meters()
|
||||
{
|
||||
for (unsigned int i = 0; i < 2; ++i) { xtc_peak_meter_reset(master_+i); xtc_peak_meter_reset(track_meter_+i); }
|
||||
display_peaks(left_, right_, master_, "Master");
|
||||
display_peaks(track_left_, track_right_, track_meter_, "Track");
|
||||
}
|
||||
xtc_result MixerWidget::add_bus()
|
||||
{
|
||||
uint64_t id;
|
||||
xtc_result r = before_ ? before_(context_) : XTC_OK;
|
||||
if (r == XTC_OK) r = xtc_project_add_bus(project_, "Bus", &id);
|
||||
if (r == XTC_OK) { changed(r); select_track(id); return r; }
|
||||
return changed(r);
|
||||
}
|
||||
xtc_result MixerWidget::route(uint64_t bus)
|
||||
{
|
||||
xtc_result r = before_ ? before_(context_) : XTC_OK;
|
||||
if (r == XTC_OK) r = xtc_project_route_track(project_, selected_track(), bus);
|
||||
return changed(r);
|
||||
}
|
||||
bool MixerWidget::select_track(uint64_t id)
|
||||
{
|
||||
size_t n = 0, clips = 0; xtc_project_get_counts(project_, &n, &clips);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
xtc_track_info track{}; xtc_project_get_track(project_, i, &track);
|
||||
if (track.id == id) { tracks_->value(static_cast<int>(i)); selection(); return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
#define XTC_MIXER_WIDGET_HPP
|
||||
#include "xtc/xtc_mixer.h"
|
||||
#include <FL/Fl_Group.H>
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
class Fl_Choice;
|
||||
class Fl_Value_Slider;
|
||||
|
|
@ -118,6 +120,12 @@ class MixerWidget : public Fl_Group {
|
|||
* @param solo Solo state: 0 or 1.
|
||||
* @return XTC_OK on success, or the before-edit or project mutation error.
|
||||
*/
|
||||
xtc_result add_bus();
|
||||
xtc_result route(uint64_t bus);
|
||||
bool select_track(uint64_t id);
|
||||
void track_peaks(float left, float right);
|
||||
void reset_meters();
|
||||
bool master_clipped() const { return master_[0].clipped || master_[1].clipped; }
|
||||
xtc_result mix(float gain, float pan, int mute, int solo);
|
||||
|
||||
/**
|
||||
|
|
@ -135,10 +143,17 @@ class MixerWidget : public Fl_Group {
|
|||
xtc_project *project_;
|
||||
BeforeEdit before_;
|
||||
void *context_;
|
||||
Fl_Choice *tracks_, *parameter_;
|
||||
Fl_Choice *tracks_, *parameter_, *routes_;
|
||||
std::vector<uint64_t> route_ids_;
|
||||
Fl_Value_Slider *gain_, *pan_;
|
||||
Fl_Check_Button *mute_, *solo_;
|
||||
Fl_Progress *left_, *right_;
|
||||
Fl_Progress *left_, *right_, *track_left_, *track_right_;
|
||||
xtc_peak_meter master_[2]{}, track_meter_[2]{};
|
||||
uint64_t meter_track_ = UINT64_MAX;
|
||||
std::chrono::steady_clock::time_point peak_time_ = std::chrono::steady_clock::now();
|
||||
double meter_elapsed_ = 0.1;
|
||||
void display_peaks(Fl_Progress *left, Fl_Progress *right, xtc_peak_meter *meters, const char *prefix);
|
||||
|
||||
Fl_Value_Input *start_;
|
||||
AutomationLane *lane_;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@ typedef struct {
|
|||
} voice;
|
||||
|
||||
struct xtc_midi_plan {
|
||||
size_t fade_frames, controller_frames;
|
||||
size_t fade_frames, controller_frames, monitor_stride;
|
||||
float *monitor;
|
||||
uint64_t monitor_position;
|
||||
xtc_gain_ramp volume[16], pan[16];
|
||||
unsigned int sustain[16];
|
||||
xtc_mix_snapshot mix;
|
||||
|
|
@ -334,6 +336,8 @@ static void segment(xtc_midi_plan *p, uint64_t position, float *out, float *righ
|
|||
if (distance < p->fade_frames)
|
||||
sample *= (float)distance / (float)p->fade_frames;
|
||||
}
|
||||
if (p->monitor) xtc_mix_monitor(&p->mix, p->monitor, p->monitor_stride, s->track, position+j,
|
||||
(size_t)(position-p->monitor_position)+j, sample, right != NULL);
|
||||
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);
|
||||
|
|
@ -393,6 +397,7 @@ xtc_result xtc_midi_plan_render_stereo(xtc_midi_plan *p, uint64_t position, cons
|
|||
(e->type == XTC_MIDI_NOTE_ON && !e->velocity))
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
p->monitor_position = position;
|
||||
if (!frames)
|
||||
return XTC_OK;
|
||||
if (p->next_position != position)
|
||||
|
|
@ -431,3 +436,6 @@ xtc_result xtc_midi_plan_render(xtc_midi_plan *p, uint64_t position, const xtc_m
|
|||
{
|
||||
return xtc_midi_plan_render_stereo(p, position, events, count, out, NULL, frames);
|
||||
}
|
||||
|
||||
void xtc_midi_plan_monitor(xtc_midi_plan *p, float *samples, size_t stride)
|
||||
{ if (p) { p->monitor = samples; p->monitor_stride = stride; } }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#define XTC_MIX_INTERNAL_H
|
||||
|
||||
#include "../project/project_internal.h"
|
||||
#include "xtc/xtc_audio.h"
|
||||
|
||||
typedef struct {
|
||||
uint64_t sample, tick;
|
||||
|
|
@ -15,6 +16,7 @@ typedef struct {
|
|||
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;
|
||||
size_t bus[XTC_PROJECT_MAX_TRACKS];
|
||||
} xtc_mix_snapshot;
|
||||
|
||||
/**
|
||||
|
|
@ -45,4 +47,11 @@ size_t xtc_mix_track(const xtc_mix_snapshot * m, uint64_t id);
|
|||
* @return Linear level for the chosen side, or zero for an absent or inaudible track.
|
||||
*/
|
||||
float xtc_mix_level(const xtc_mix_snapshot * m, size_t track, uint64_t sample, unsigned int side);
|
||||
float xtc_mix_leaf_level(const xtc_mix_snapshot *m, size_t track, uint64_t sample, unsigned int side);
|
||||
/* Borrowed control-installed scratch: 2*track_count channels, stride frames.
|
||||
* Clear once per callback before audio+MIDI rendering. No pointer changes active. */
|
||||
void xtc_audio_plan_monitor(xtc_audio_plan *plan, float *samples, size_t stride);
|
||||
void xtc_midi_plan_monitor(xtc_midi_plan *plan, float *samples, size_t stride);
|
||||
void xtc_mix_monitor(const xtc_mix_snapshot *mix, float *samples, size_t stride, size_t track,
|
||||
uint64_t position, size_t frame, float sample, int stereo);
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -129,3 +129,38 @@ xtc_result xtc_project_get_automation(const xtc_project *p, size_t i, xtc_automa
|
|||
*a = p->automation[i];
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_add_bus(xtc_project *p, const char *name, uint64_t *out)
|
||||
{
|
||||
xtc_result r = xtc_project_add_track(p, name, out);
|
||||
if (r == XTC_OK) p->tracks[p->track_count-1].is_bus = 1;
|
||||
return r;
|
||||
}
|
||||
xtc_result xtc_project_route_track(xtc_project *p, uint64_t id, uint64_t bus)
|
||||
{
|
||||
size_t i, destination;
|
||||
if (!p) return XTC_ERR_INVALID_ARGUMENT;
|
||||
i = find_track(p, id);
|
||||
if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND;
|
||||
if (p->tracks[i].is_bus) return XTC_ERR_INVALID_STATE;
|
||||
destination = find_track(p, bus);
|
||||
if (bus != UINT64_MAX && (destination == SIZE_MAX || !p->tracks[destination].is_bus)) return XTC_ERR_INVALID_ARGUMENT;
|
||||
p->tracks[i].bus_id = bus;
|
||||
return XTC_OK;
|
||||
}
|
||||
void xtc_peak_meter_reset(xtc_peak_meter *m)
|
||||
{
|
||||
if (m) { m->db = -90; m->clipped = 0; }
|
||||
}
|
||||
xtc_result xtc_peak_meter_update(xtc_peak_meter *m, float peak, double seconds)
|
||||
{
|
||||
float db, released;
|
||||
if (!m || !isfinite(peak) || peak < 0 || !isfinite(seconds) || seconds < 0 || seconds > 60)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
db = peak > 0 ? 20.0f * log10f(peak) : -90;
|
||||
if (db < -90) db = -90;
|
||||
released = m->db - (float)(20 * seconds);
|
||||
m->db = db > released ? db : released;
|
||||
if (peak >= 1) m->clipped = 1;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,9 +50,10 @@ xtc_result xtc_mix_prepare(const xtc_project *p, unsigned int rate, xtc_mix_snap
|
|||
memset(m, 0, sizeof(*m));
|
||||
m->count = p->track_count;
|
||||
memcpy(m->tracks, p->tracks, p->track_count * sizeof(*m->tracks));
|
||||
for (i = 0; i < m->count; ++i)
|
||||
if (m->tracks[i].solo)
|
||||
m->any_solo = 1;
|
||||
for (i = 0; i < m->count; ++i) {
|
||||
if (m->tracks[i].solo) m->any_solo = 1;
|
||||
m->bus[i] = xtc_mix_track(m, m->tracks[i].bus_id);
|
||||
}
|
||||
for (i = 0; i < p->automation_count; ++i) {
|
||||
const xtc_automation_point *a = p->automation + i;
|
||||
xtc_mix_point *b = m->points + i;
|
||||
|
|
@ -117,12 +118,13 @@ static float value(const xtc_mix_snapshot *m, size_t track, unsigned int param,
|
|||
* @param side Output side: 0 for left, 1 for right, 2 for mono gain only.
|
||||
* @return Linear level for the chosen side, or zero for an absent or inaudible track.
|
||||
*/
|
||||
float xtc_mix_level(const xtc_mix_snapshot *m, size_t track, uint64_t sample, unsigned int side)
|
||||
float xtc_mix_leaf_level(const xtc_mix_snapshot *m, size_t track, uint64_t sample, unsigned int side)
|
||||
{
|
||||
float gain, pan;
|
||||
if (track >= m->count)
|
||||
return 0;
|
||||
if (m->tracks[track].mute || (m->any_solo && !m->tracks[track].solo))
|
||||
if (m->tracks[track].mute || (m->any_solo && !m->tracks[track].solo &&
|
||||
!(m->bus[track] < m->count && m->tracks[m->bus[track]].solo)))
|
||||
return 0;
|
||||
gain = value(m, track, 0, sample);
|
||||
if (side == 2)
|
||||
|
|
@ -130,3 +132,31 @@ float xtc_mix_level(const xtc_mix_snapshot *m, size_t track, uint64_t sample, un
|
|||
pan = value(m, track, 1, sample);
|
||||
return gain * (side == 0 ? (pan > 0 ? 1 - pan : 1) : (pan < 0 ? 1 + pan : 1));
|
||||
}
|
||||
|
||||
float xtc_mix_level(const xtc_mix_snapshot *m, size_t track, uint64_t sample, unsigned int side)
|
||||
{
|
||||
float level = xtc_mix_leaf_level(m, track, sample, side);
|
||||
size_t bus;
|
||||
if (track >= m->count) return 0;
|
||||
bus = m->bus[track];
|
||||
if (bus < m->count) {
|
||||
float gain, pan;
|
||||
if (m->tracks[bus].mute) return 0;
|
||||
gain = value(m, bus, 0, sample);
|
||||
pan = side == 2 ? 0 : value(m, bus, 1, sample);
|
||||
level *= gain * (side == 0 ? (pan > 0 ? 1-pan : 1) : side == 1 ? (pan < 0 ? 1+pan : 1) : 1);
|
||||
}
|
||||
return level;
|
||||
}
|
||||
void xtc_mix_monitor(const xtc_mix_snapshot *m, float *samples, size_t stride, size_t track,
|
||||
uint64_t position, size_t frame, float sample, int stereo)
|
||||
{
|
||||
unsigned int side;
|
||||
if (!samples || track >= m->count || frame >= stride) return;
|
||||
for (side = 0; side < 2; ++side) {
|
||||
unsigned int channel = stereo ? side : 2;
|
||||
samples[(track*2+side)*stride+frame] += sample * xtc_mix_leaf_level(m, track, position, channel);
|
||||
if (m->bus[track] < m->count)
|
||||
samples[(m->bus[track]*2+side)*stride+frame] += sample * xtc_mix_level(m, track, position, channel);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -311,6 +311,14 @@ static xtc_result read_project(reader *input, xtc_project *project, const char *
|
|||
return XTC_ERR_FORMAT;
|
||||
project->tracks[i].solo = (int)value;
|
||||
}
|
||||
project->tracks[i].bus_id = UINT64_MAX;
|
||||
if (format_version >= 9U) {
|
||||
READ("ROUTE", 3U);
|
||||
NUM(1U, value);
|
||||
if (value > 1) return XTC_ERR_FORMAT;
|
||||
project->tracks[i].is_bus = (int)value;
|
||||
NUM(2U, project->tracks[i].bus_id);
|
||||
}
|
||||
}
|
||||
READ("CLIPS", 2U);
|
||||
NUM(1U, value);
|
||||
|
|
@ -496,7 +504,7 @@ static xtc_result write_project(FILE *file, const xtc_project *project, const ch
|
|||
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)
|
||||
fprintf(file, "MIX %08" PRIx32 " %08" PRIx32 " %d %d\nROUTE %d %" PRIu64 "\n", gain, pan, t->mute, t->solo, t->is_bus, t->bus_id) < 0)
|
||||
return XTC_ERR_IO;
|
||||
}
|
||||
if (fprintf(file, "CLIPS %zu\n", project->clip_count) < 0)
|
||||
|
|
|
|||
|
|
@ -296,6 +296,7 @@ xtc_result xtc_project_add_track(xtc_project *project, const char *name, uint64_
|
|||
return XTC_ERR_CAPACITY;
|
||||
track.id = project->next_track_id++;
|
||||
track.gain = 1.0f;
|
||||
track.bus_id = UINT64_MAX;
|
||||
memcpy(track.name, name, strlen(name) + 1U);
|
||||
project->tracks[project->track_count++] = track;
|
||||
*id = track.id;
|
||||
|
|
@ -321,6 +322,8 @@ xtc_result xtc_project_remove_track(xtc_project *project, uint64_t id)
|
|||
if (project->clips[i].track_id != id)
|
||||
project->clips[count++] = project->clips[i];
|
||||
project->clip_count = count;
|
||||
for (i = 0; i < project->track_count; ++i)
|
||||
if (project->tracks[i].bus_id == id) project->tracks[i].bus_id = UINT64_MAX;
|
||||
memmove(project->tracks + index, project->tracks + index + 1U,
|
||||
(project->track_count - index - 1U) * sizeof(*project->tracks));
|
||||
--project->track_count;
|
||||
|
|
@ -356,6 +359,7 @@ xtc_result xtc_project_add_clip(xtc_project *project, uint64_t track, const char
|
|||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (track_index(project, track) == SIZE_MAX)
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
if (project->tracks[track_index(project, track)].is_bus) return XTC_ERR_INVALID_STATE;
|
||||
if (project->clip_count == XTC_PROJECT_MAX_CLIPS || project->next_clip_id == UINT64_MAX)
|
||||
return XTC_ERR_CAPACITY;
|
||||
clip.gain = 1.0f;
|
||||
|
|
@ -417,6 +421,7 @@ xtc_result xtc_project_edit_clip(xtc_project *project, uint64_t id, uint64_t tra
|
|||
index = clip_index(project, id);
|
||||
if (index == SIZE_MAX || track_index(project, track) == SIZE_MAX)
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
if (project->tracks[track_index(project, track)].is_bus) return XTC_ERR_INVALID_STATE;
|
||||
if (!valid_range(start, length, offset, project->clips[index].source_length_tick))
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
project->clips[index].track_id = track;
|
||||
|
|
@ -514,6 +519,13 @@ xtc_result xtc_project_validate(const xtc_project *project)
|
|||
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;
|
||||
{
|
||||
const xtc_track_info *t = project->tracks+i;
|
||||
size_t bus = track_index(project, t->bus_id);
|
||||
if ((t->is_bus != 0 && t->is_bus != 1) ||
|
||||
(t->bus_id != UINT64_MAX && (t->is_bus || bus == SIZE_MAX || !project->tracks[bus].is_bus)))
|
||||
return XTC_ERR_FORMAT;
|
||||
}
|
||||
for (j = 0U; j < i; ++j)
|
||||
if (project->tracks[j].id == project->tracks[i].id)
|
||||
return XTC_ERR_FORMAT;
|
||||
|
|
@ -538,6 +550,7 @@ xtc_result xtc_project_validate(const xtc_project *project)
|
|||
!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;
|
||||
if (project->tracks[track_index(project, clip->track_id)].is_bus) return XTC_ERR_FORMAT;
|
||||
for (j = 0U; j < i; ++j)
|
||||
if (project->clips[j].id == clip->id)
|
||||
return XTC_ERR_FORMAT;
|
||||
|
|
@ -613,6 +626,7 @@ xtc_result xtc_project_duplicate_clip(xtc_project *p, uint64_t id, uint64_t trac
|
|||
if (!p || !out) return XTC_ERR_INVALID_ARGUMENT;
|
||||
index = clip_index(p,id);
|
||||
if (index == SIZE_MAX || track_index(p,track) == SIZE_MAX) return XTC_ERR_NOT_FOUND;
|
||||
if (p->tracks[track_index(p,track)].is_bus) return XTC_ERR_INVALID_STATE;
|
||||
c = p->clips[index];
|
||||
if (!valid_range(start,c.length_tick,c.offset_tick,c.source_length_tick)) return XTC_ERR_INVALID_ARGUMENT;
|
||||
for (i = 0; i < p->note_count; ++i) if (p->notes[i].clip_id == id) ++count;
|
||||
|
|
|
|||
2
tests/fixtures/README.md
vendored
2
tests/fixtures/README.md
vendored
|
|
@ -3,7 +3,7 @@
|
|||
Store small, redistributable fixtures here. Document their origin and license.
|
||||
Do not commit large recordings or assets with unclear licensing.
|
||||
|
||||
`project-v1.xtc` through `project-v7.xtc` are hand-authored xtc metadata fixtures
|
||||
`project-v1.xtc` through `project-v8.xtc` are hand-authored xtc metadata fixtures
|
||||
created for this repository, under the project license. They contain no audio
|
||||
or third-party resources. The v5/v6 plugin fixtures intentionally reference a
|
||||
missing module to exercise metadata migration without loading code.
|
||||
|
|
|
|||
21
tests/fixtures/project-v8.xtc
vendored
Normal file
21
tests/fixtures/project-v8.xtc
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
XTC_PROJECT 8
|
||||
APP 0 1 0
|
||||
TEMPO 120000
|
||||
IDS 1 1
|
||||
GRAPH 1 2 2 1
|
||||
NODE 0 1 3f000000
|
||||
NODE 1 4 00000000
|
||||
EDGE 0 1
|
||||
TRACKS 1
|
||||
TRACK 0 4b657973
|
||||
MIX 3f800000 00000000 0 0
|
||||
CLIPS 1
|
||||
CLIP 0 0 0 960 0 960 5665727365
|
||||
ASSET -
|
||||
MIDI 1
|
||||
AUDIO 3f800000 0 0
|
||||
NOTES 1 1
|
||||
NOTE 0 0 0 480 69 100 0
|
||||
AUTOMATION 1
|
||||
POINT 0 0 0 3f800000
|
||||
END
|
||||
|
|
@ -144,6 +144,11 @@ xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph)
|
|||
return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_backend_panic(xtc_backend *) { xtc_midi_plan_panic(handle.midi, engine_status.sample_position); return XTC_OK; }
|
||||
xtc_result xtc_backend_read_meters(xtc_backend *, xtc_meter_snapshot *out)
|
||||
{
|
||||
*out = {}; out->master_left = engine_status.peak_left; out->master_right = engine_status.peak_right;
|
||||
return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *project)
|
||||
{
|
||||
xtc_audio_plan *plan = nullptr;
|
||||
|
|
@ -602,6 +607,27 @@ int main(int argc, char **argv)
|
|||
CHECK((sx != gx || sy != gy) && (sx != ox || sy != oy) && (gx != ox || gy != oy));
|
||||
window.do_callback();
|
||||
}
|
||||
{
|
||||
reset(); MainWindow window(false);
|
||||
auto *sequence = timeline(&window); auto *mixer = window.mixer();
|
||||
CHECK(sequence->add_track() == XTC_OK);
|
||||
CHECK(mixer->add_bus() == XTC_OK);
|
||||
uint64_t bus = mixer->selected_track();
|
||||
xtc_track_info track{};
|
||||
CHECK(xtc_project_get_track(sequence->project(),1,&track) == XTC_OK && track.is_bus);
|
||||
CHECK(mixer->mix(.5f,1,0,1) == XTC_OK);
|
||||
CHECK(mixer->select_track(0) && mixer->route(bus) == XTC_OK);
|
||||
CHECK(xtc_project_get_track(sequence->project(),0,&track) == XTC_OK && track.bus_id == bus);
|
||||
CHECK(window.undo() == XTC_OK);
|
||||
CHECK(xtc_project_get_track(sequence->project(),0,&track) == XTC_OK && track.bus_id == UINT64_MAX);
|
||||
CHECK(window.undo(true) == XTC_OK);
|
||||
CHECK(xtc_project_get_track(sequence->project(),0,&track) == XTC_OK && track.bus_id == bus);
|
||||
mixer->peaks(1.25f,.5f); mixer->track_peaks(.5f,.25f);
|
||||
CHECK(mixer->master_clipped() && contains(&window,"dBFS CLIP"));
|
||||
mixer->peaks(0,0); CHECK(mixer->master_clipped());
|
||||
mixer->reset_meters(); CHECK(!mixer->master_clipped());
|
||||
window.do_callback();
|
||||
}
|
||||
if (const char *module = std::getenv("XTC_TEST_PLUGIN")) {
|
||||
reset();
|
||||
MainWindow window(false);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ def main():
|
|||
assert os.access(root / 'bin/xtc-check', os.X_OK)
|
||||
run([str(root / 'bin/xtc-check')])
|
||||
diagnose = subprocess.check_output([str(root / 'bin/xtc-check'), '--diagnose'], text=True)
|
||||
assert 'Project formats: 1..8' in diagnose and 'JACK compiled:' in diagnose
|
||||
assert 'Project formats: 1..9' in diagnose and 'JACK compiled:' in diagnose
|
||||
# Upgrade/repair repeats the same owned-file install and keeps unrelated data.
|
||||
(root / 'include/xtc/xtc.h').write_text('old installation marker\n')
|
||||
run(make + ['install'] + variables, env=env)
|
||||
|
|
|
|||
|
|
@ -18,12 +18,13 @@ int main(void)
|
|||
assert(mkdtemp(root));
|
||||
assert(xtc_project_create(&p) == XTC_OK);
|
||||
snprintf(file,sizeof(file),"%s/migration.xtc",root);
|
||||
for (version = 1; version <= 7; ++version) {
|
||||
for (version = 1; version <= 8; ++version) {
|
||||
xtc_track_info t; xtc_clip_info c; size_t notes, points;
|
||||
snprintf(old,sizeof(old),"tests/fixtures/project-v%u.xtc",version);
|
||||
assert(xtc_project_load(p,old) == XTC_OK);
|
||||
assert(xtc_project_get_track(p,0,&t) == XTC_OK && !strcmp(t.name,"Keys") && t.gain == 1);
|
||||
assert(xtc_project_get_clip(p,0,&c) == XTC_OK && c.midi == (version >= 3));
|
||||
assert(!t.is_bus && t.bus_id == UINT64_MAX);
|
||||
assert(c.gain == 1 && c.fade_in_tick == 0 && c.fade_out_tick == 0);
|
||||
assert(xtc_project_note_count(p,¬es) == XTC_OK && notes == (size_t)(version >= 3));
|
||||
assert(xtc_project_automation_count(p,&points) == XTC_OK && points == (size_t)(version >= 4));
|
||||
|
|
@ -84,6 +85,6 @@ int main(void)
|
|||
assert(xtc_project_load(p,file) == XTC_ERR_FORMAT);
|
||||
{ xtc_clip_info c; assert(xtc_project_find_clip(p,clip,&c) == XTC_OK && c.asset_path[0]); } }
|
||||
xtc_project_destroy(p); unlink(file); rmdir(moved);
|
||||
puts("v1-v7 migration, current folder relocation, cwd independence and missing-media preservation passed.");
|
||||
puts("v1-v8 migration, current folder relocation, cwd independence and missing-media preservation passed.");
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ static Fl_Widget *button(Fl_Group &group, const std::string &label)
|
|||
{
|
||||
for (int i = 0; i < group.children(); ++i) {
|
||||
auto *child = group.child(i);
|
||||
if (child->label() && label == child->label()) return child;
|
||||
if (child->label() && (label == child->label() || (label.compare(0, 7, "Master ") == 0 && std::string(child->label()).compare(0, label.size(), label) == 0))) return child;
|
||||
if (auto *nested = dynamic_cast<Fl_Group *>(child))
|
||||
if (auto *match = button(*nested, label)) return match;
|
||||
}
|
||||
|
|
@ -142,12 +142,13 @@ int main(int argc, char **argv)
|
|||
press(window, "Play"); pump();
|
||||
CHECK(!button(window, "Play")->active() && button(window, "Pause")->active());
|
||||
CHECK(timeline->position() > 1920);
|
||||
auto *left_meter = dynamic_cast<Fl_Progress *>(button(window, "Output L"));
|
||||
auto *right_meter = dynamic_cast<Fl_Progress *>(button(window, "Output R"));
|
||||
CHECK(left_meter && right_meter && left_meter->value() > 0 && right_meter->value() > 0);
|
||||
auto *left_meter = dynamic_cast<Fl_Progress *>(button(window, "Master L"));
|
||||
auto *right_meter = dynamic_cast<Fl_Progress *>(button(window, "Master R"));
|
||||
CHECK(left_meter && right_meter && left_meter->value() > -90 && right_meter->value() > -90);
|
||||
const double playing_left = left_meter->value(), playing_right = right_meter->value();
|
||||
CHECK(window.pause_playback() == XTC_OK);
|
||||
pump(); // Fade completion is acknowledged by the GUI timer.
|
||||
CHECK(left_meter->value() == 0 && right_meter->value() == 0);
|
||||
CHECK(left_meter->value() < playing_left && right_meter->value() < playing_right);
|
||||
uint64_t paused = timeline->position(); pump();
|
||||
CHECK(timeline->position() == paused);
|
||||
press(window, "Play"); pump();
|
||||
|
|
|
|||
|
|
@ -312,9 +312,18 @@ int main(void)
|
|||
CHECK(xtc_project_add_midi_clip(p, track, 0, 3840, &clip) == XTC_OK);
|
||||
CHECK(xtc_project_add_note(p, clip, 0, 960, 69, 127, 0, ¬e) == XTC_OK);
|
||||
CHECK(xtc_project_set_mix(p, track, 0.5f, 1, 0, 0) == XTC_OK);
|
||||
uint64_t bus;
|
||||
CHECK(xtc_project_add_bus(p, "Bus", &bus) == XTC_OK && xtc_project_route_track(p, track, bus) == XTC_OK);
|
||||
CHECK(xtc_project_set_mix(p, bus, 2, 0, 0, 0) == XTC_OK);
|
||||
CHECK(xtc_backend_prepare_project(backend, p) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
|
||||
CHECK(process_cb(32, context) == 0 && output[1] == 0 && output_right[1] > 0);
|
||||
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.peak_left == 0 && status.peak_right > 0);
|
||||
xtc_meter_snapshot meters;
|
||||
CHECK(xtc_backend_read_meters(backend, &meters) == XTC_OK && meters.count == 2);
|
||||
CHECK(meters.tracks[0].id == track && meters.tracks[0].left == 0 && meters.tracks[0].right > 0);
|
||||
CHECK(meters.tracks[1].id == bus && meters.tracks[1].left == 0 && meters.tracks[1].right > meters.tracks[0].right);
|
||||
CHECK(meters.master_left == 0 && meters.master_right == meters.tracks[1].right);
|
||||
CHECK(xtc_backend_read_meters(backend, &meters) == XTC_OK && meters.master_right == 0 && meters.tracks[0].right == 0);
|
||||
CHECK(xtc_backend_stop(backend) == XTC_OK);
|
||||
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.peak_right == 0);
|
||||
CHECK(xtc_backend_close(backend) == XTC_OK); xtc_project_destroy(p);
|
||||
|
|
|
|||
115
tests/unit/test_buses.c
Normal file
115
tests/unit/test_buses.c
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "xtc/xtc_audio.h"
|
||||
#include "xtc/xtc_midi.h"
|
||||
#include "xtc/xtc_mixer.h"
|
||||
#include "../../src/mixer/mix_internal.h"
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#define TAU 6.28318530717958647692
|
||||
static void render(xtc_project *p, float *left, float *right, float *meters)
|
||||
{
|
||||
xtc_audio_plan *a = NULL;
|
||||
memset(meters, 0, 8*2*64*sizeof(float));
|
||||
assert(xtc_audio_plan_create(p, 48000, &a) == XTC_OK);
|
||||
xtc_audio_plan_monitor(a, meters, 64);
|
||||
assert(xtc_audio_plan_render_stereo(a, 0, left, right, 64) == XTC_OK);
|
||||
xtc_audio_plan_destroy(a);
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
xtc_project *p = NULL, *q = NULL, *undo = NULL;
|
||||
xtc_recorder *rec = NULL;
|
||||
xtc_history *history = NULL;
|
||||
xtc_audio_plan *audio = NULL;
|
||||
xtc_midi_plan *midi = NULL;
|
||||
xtc_track_info info;
|
||||
xtc_peak_meter meter;
|
||||
xtc_automation_point point;
|
||||
uint64_t a,b,c,bus,clip,id;
|
||||
float in[64], left[64], right[64], meters[8*2*64], ml[64], mr[64];
|
||||
char directory[] = "/tmp/xtc-buses-XXXXXX", wave[256], song[256];
|
||||
size_t i;
|
||||
assert(mkdtemp(directory));
|
||||
snprintf(wave,sizeof(wave),"%s/source.wav",directory);
|
||||
snprintf(song,sizeof(song),"%s/song.xtc",directory);
|
||||
for(i=0;i<64;++i) in[i]=1;
|
||||
assert(xtc_recorder_create(wave,48000,64,&rec)==XTC_OK);
|
||||
xtc_recorder_push(rec,in,64); assert(xtc_recorder_finish(rec)==XTC_OK); xtc_recorder_destroy(rec);
|
||||
assert(xtc_project_create(&p)==XTC_OK && xtc_project_create(&q)==XTC_OK);
|
||||
assert(xtc_project_add_track(p,"A",&a)==XTC_OK && xtc_project_add_track(p,"B",&b)==XTC_OK);
|
||||
assert(xtc_project_add_track(p,"Direct",&c)==XTC_OK && xtc_project_add_bus(p,"Group",&bus)==XTC_OK);
|
||||
assert(xtc_project_route_track(p,a,bus)==XTC_OK && xtc_project_route_track(p,b,bus)==XTC_OK);
|
||||
assert(xtc_project_route_track(p,bus,bus)==XTC_ERR_INVALID_STATE);
|
||||
assert(xtc_project_route_track(p,a,b)==XTC_ERR_INVALID_ARGUMENT);
|
||||
assert(xtc_project_import_audio(p,bus,wave,0,&clip)==XTC_ERR_INVALID_STATE);
|
||||
assert(xtc_project_import_audio(p,a,wave,0,&clip)==XTC_OK);
|
||||
assert(xtc_project_duplicate_clip(p,clip,bus,0,&id)==XTC_ERR_INVALID_STATE);
|
||||
assert(xtc_project_edit_clip(p,clip,bus,0,3,0)==XTC_ERR_INVALID_STATE);
|
||||
assert(xtc_project_import_audio(p,b,wave,0,&clip)==XTC_OK && xtc_project_import_audio(p,c,wave,0,&clip)==XTC_OK);
|
||||
assert(xtc_project_set_mix(p,a,.5f,0,0,0)==XTC_OK && xtc_project_set_mix(p,b,.5f,0,0,0)==XTC_OK);
|
||||
assert(xtc_project_set_mix(p,c,.25f,0,0,0)==XTC_OK && xtc_project_set_mix(p,bus,.5f,1,0,0)==XTC_OK);
|
||||
render(p,left,right,meters);
|
||||
assert(left[0]==.25f && right[0]==.75f);
|
||||
assert(meters[0]==.5f && meters[64]==.5f && meters[(3*2)*64]==0 && meters[(3*2+1)*64]==.5f);
|
||||
assert(xtc_project_set_mix(p,a,.5f,0,0,1)==XTC_OK);
|
||||
render(p,left,right,meters); assert(left[0]==0 && right[0]==.25f && meters[2*64]==0);
|
||||
assert(xtc_project_set_mix(p,a,.5f,0,0,0)==XTC_OK && xtc_project_set_mix(p,bus,.5f,1,0,1)==XTC_OK);
|
||||
render(p,left,right,meters); assert(left[0]==0 && right[0]==.5f && meters[4*64]==0);
|
||||
assert(xtc_project_set_mix(p,bus,.5f,1,1,1)==XTC_OK);
|
||||
render(p,left,right,meters); assert(right[0]==0 && meters[0]==.5f && meters[7*64]==0);
|
||||
assert(xtc_project_set_mix(p,bus,.5f,0,0,0)==XTC_OK);
|
||||
point=(xtc_automation_point){bus,0,XTC_AUTOMATION_GAIN,.25f};
|
||||
assert(xtc_project_set_automation(p,&point)==XTC_OK);
|
||||
render(p,left,right,meters); assert(left[0]==.5f && right[0]==.5f); /* .25 bus + .25 direct */
|
||||
assert(xtc_project_save(p,song)==XTC_OK && xtc_project_load(q,song)==XTC_OK);
|
||||
assert(xtc_project_get_track(q,3,&info)==XTC_OK && info.is_bus && info.bus_id==UINT64_MAX);
|
||||
assert(xtc_project_get_track(q,0,&info)==XTC_OK && info.bus_id==bus);
|
||||
assert(xtc_history_create(q,&history)==XTC_OK && xtc_project_remove_track(q,bus)==XTC_OK);
|
||||
assert(xtc_project_get_track(q,0,&info)==XTC_OK && info.bus_id==UINT64_MAX);
|
||||
assert(xtc_history_record(history,q)==XTC_OK && xtc_history_checkout(history,0,&undo)==XTC_OK);
|
||||
assert(xtc_project_get_track(undo,0,&info)==XTC_OK && info.bus_id==bus);
|
||||
xtc_project_destroy(undo); xtc_history_destroy(history);
|
||||
/* Invalid destination on load is transactional. */
|
||||
{
|
||||
char text[10000], *route; size_t length; FILE *file=fopen(song,"rb"); assert(file);
|
||||
length=fread(text,1,sizeof(text)-1,file); assert(fclose(file)==0); text[length]=0;
|
||||
route=strstr(text,"ROUTE 0 3\n"); assert(route); route[8]='9';
|
||||
file=fopen(song,"wb"); assert(file); assert(fwrite(text,1,length,file)==length && fclose(file)==0);
|
||||
assert(xtc_project_load(q,song)==XTC_ERR_FORMAT);
|
||||
}
|
||||
xtc_project_destroy(p); xtc_project_destroy(q);
|
||||
/* Audio+MIDI on one track cancel before their shared bus/meter tap. */
|
||||
assert(unlink(wave)==0);
|
||||
for(i=0;i<64;++i) in[i]=-(float)(sin(TAU*fmod((double)i*(440.0/48000),1.0))*.1);
|
||||
assert(xtc_recorder_create(wave,48000,64,&rec)==XTC_OK);
|
||||
xtc_recorder_push(rec,in,64); assert(xtc_recorder_finish(rec)==XTC_OK); xtc_recorder_destroy(rec);
|
||||
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Cancel",&a)==XTC_OK);
|
||||
assert(xtc_project_add_bus(p,"Sum",&bus)==XTC_OK && xtc_project_route_track(p,a,bus)==XTC_OK);
|
||||
assert(xtc_project_import_audio(p,a,wave,0,&clip)==XTC_OK && xtc_project_add_midi_clip(p,a,0,960,&clip)==XTC_OK);
|
||||
assert(xtc_project_add_note(p,clip,0,960,69,127,0,&id)==XTC_OK);
|
||||
assert(xtc_audio_plan_create(p,48000,&audio)==XTC_OK && xtc_midi_plan_create(p,48000,&midi)==XTC_OK);
|
||||
memset(meters,0,sizeof(meters)); xtc_audio_plan_monitor(audio,meters,64); xtc_midi_plan_monitor(midi,meters,64);
|
||||
assert(xtc_audio_plan_render_stereo(audio,0,left,right,64)==XTC_OK);
|
||||
assert(xtc_midi_plan_render_stereo(midi,0,NULL,0,ml,mr,17)==XTC_OK);
|
||||
assert(xtc_midi_plan_render_stereo(midi,17,NULL,0,ml+17,mr+17,47)==XTC_OK);
|
||||
/* Standalone callers clear/offset scratch per render; backend renders one MIDI call per block. */
|
||||
memset(meters,0,sizeof(meters));
|
||||
assert(xtc_audio_plan_render_stereo(audio,0,left,right,64)==XTC_OK);
|
||||
assert(xtc_midi_plan_render_stereo(midi,0,NULL,0,ml,mr,64)==XTC_OK);
|
||||
for(i=0;i<64;++i) { assert(fabsf(meters[i])<.000001f); assert(fabsf(meters[2*64+i])<.000001f); }
|
||||
xtc_audio_plan_destroy(audio); xtc_midi_plan_destroy(midi); xtc_project_destroy(p);
|
||||
xtc_peak_meter_reset(&meter); assert(meter.db==-90 && !meter.clipped);
|
||||
assert(xtc_peak_meter_update(&meter,.5f,.1)==XTC_OK && fabsf(meter.db+6.0206f)<.0001f);
|
||||
assert(xtc_peak_meter_update(&meter,0,.1)==XTC_OK && fabsf(meter.db+8.0206f)<.0001f);
|
||||
assert(xtc_peak_meter_update(&meter,1,0)==XTC_OK && meter.db==0 && meter.clipped);
|
||||
assert(xtc_peak_meter_update(&meter,0,1)==XTC_OK && meter.db==-20 && meter.clipped);
|
||||
assert(xtc_peak_meter_update(&meter,NAN,.1)==XTC_ERR_INVALID_ARGUMENT && meter.db==-20);
|
||||
xtc_peak_meter_reset(&meter); assert(!meter.clipped);
|
||||
assert(unlink(wave)==0 && unlink(song)==0 && rmdir(directory)==0);
|
||||
puts("Bus routing/gain/pan/mute/solo/automation, coherent meters, persistence and undo passed.");
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -197,7 +197,7 @@ int main(void)
|
|||
fputc('\n',legacy); continue;
|
||||
}
|
||||
if (strncmp(line, "MIX ", 4) && strncmp(line, "AUTOMATION ", 11) &&
|
||||
strncmp(line, "POINT ", 6) && strncmp(line, "AUDIO ", 6))
|
||||
strncmp(line, "POINT ", 6) && strncmp(line, "AUDIO ", 6) && strncmp(line, "ROUTE ", 6))
|
||||
assert(fputs(line, legacy) >= 0);
|
||||
}
|
||||
assert(fclose(f) == 0 && fclose(legacy) == 0);
|
||||
|
|
|
|||
|
|
@ -86,8 +86,7 @@ int main(void)
|
|||
assert(xtc_project_load(q, bad) != XTC_OK);
|
||||
assert(xtc_project_find_clip(q, c, &info) == XTC_OK && memcmp(&info, &before, sizeof(info)) == 0);
|
||||
}
|
||||
memcpy(damaged, data, length); damaged[12] = '9';
|
||||
write_bytes(bad, damaged, length);
|
||||
write_bytes(bad, "XTC_PROJECT 999\n", strlen("XTC_PROJECT 999\n"));
|
||||
assert(xtc_project_load(q, bad) == XTC_ERR_UNSUPPORTED);
|
||||
memcpy(damaged, data, length); damaged[length] = '\n';
|
||||
write_bytes(bad, damaged, length + 1);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue