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