1
0
Fork 0
forked from hanez/xtc

Phase 24: add mix buses, coherent peak meters and master clipping indicators

This commit is contained in:
Johannes Findeisen 2026-10-27 16:15:34 +01:00
commit 2fd7724065
32 changed files with 632 additions and 61 deletions

View file

@ -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, &note) == 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
View 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;
}

View file

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

View file

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