sequencer play fix.

This commit is contained in:
Johannes Findeisen 2026-11-06 22:00:35 +01:00
commit d6ded83beb
16 changed files with 372 additions and 16 deletions

View file

@ -239,3 +239,18 @@ wait for asynchronous fade acknowledgement. `test_quality` in the unit suite
checks exact source envelopes/timing and variable blocks, including tempo
snapshot changes. See [AUDIO_QUALITY.md](../../docs/AUDIO_QUALITY.md) for commands,
latency limitations and the pending listening checklist.
`playback-example` renders the bundled four-bar song offline and checks finite,
bounded stereo audio in every bar. `test_playback` exercises real window events
for Play/Stop/Space and verifies stereo audio with a private JACK software probe
while saved MIDI devices are absent. Run from the repository root:
```sh
cmake --build build --target test_playback
xvfb-run -a ./build/test_playback
```
It requires an existing JACK/PipeWire server, changes no user preferences and
connects no physical outputs. Enable `XTC_ENABLE_LIVE_TESTS` to include
`playback` in CTest. The fake-backend `gui-midi-workflows` test also checks absent
input/output/both, returning devices and genuine backend failures.

View file

@ -48,6 +48,9 @@ xtc_result status_result = XTC_OK, submit_result = XTC_OK, reserve_result = XTC_
unsigned int submissions = 0U;
xtc_backend_status engine_status = {};
bool partial_open = false;
bool midi_ports_available = true;
xtc_result midi_visit_result = XTC_OK, midi_route_result = XTC_OK;
std::string routed_input, routed_output;
unsigned int opens = 0, starts = 0, stops = 0, closes = 0, collects = 0, alive = 0;
int failures = 0;
#define CHECK(c) do { if (!(c)) { std::fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); ++failures; } } while (0)
@ -116,6 +119,8 @@ void reset()
engine_status.sample_rate = 48000U;
engine_status.buffer_frames = 1024U;
partial_open = false;
midi_ports_available=true; midi_visit_result=midi_route_result=XTC_OK;
routed_input.clear(); routed_output.clear();
}
}
extern "C" {
@ -171,13 +176,13 @@ xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph)
return XTC_OK;
}
xtc_result xtc_backend_start_routed(xtc_backend *b,const char *input,const char *output)
{auto r=xtc_backend_start(b);if(r==XTC_OK && input && *input)r=xtc_backend_route_midi(b,XTC_PORT_INPUT,input,0);if(r==XTC_OK && output && *output)r=xtc_backend_route_midi(b,XTC_PORT_OUTPUT,output,0);if(r!=XTC_OK && handle.running)(void)xtc_backend_stop(b);return r;}
{routed_input=input ? input : "";routed_output=output ? output : "";auto r=xtc_backend_start(b);if(r==XTC_OK && input && *input)r=xtc_backend_route_midi(b,XTC_PORT_INPUT,input,0);if(r==XTC_OK && output && *output)r=xtc_backend_route_midi(b,XTC_PORT_OUTPUT,output,0);if(r!=XTC_OK && handle.running)(void)xtc_backend_stop(b);return r;}
xtc_result xtc_backend_set_midi_options(xtc_backend *,int input,int output,int thru)
{CHECK(input>=-1 && input<16 && output>=-1 && output<16 && thru>=0 && thru<2);return handle.running ? XTC_ERR_INVALID_STATE : XTC_OK;}
xtc_result xtc_backend_visit_midi_ports(xtc_backend *,xtc_port_direction d,xtc_backend_port_visitor visit,void *c)
{visit(d==XTC_PORT_OUTPUT ? "Keyboard:out" : "Synth:in",c);return XTC_OK;}
{if(midi_visit_result!=XTC_OK)return midi_visit_result;if(midi_ports_available)visit(d==XTC_PORT_OUTPUT ? "Keyboard:out" : "Synth:in",c);return XTC_OK;}
xtc_result xtc_backend_route_midi(xtc_backend *,xtc_port_direction,const char *name,int)
{return !strcmp(name,"missing:port") ? XTC_ERR_NOT_FOUND : XTC_OK;}
{return !strcmp(name,"missing:port") ? XTC_ERR_NOT_FOUND : midi_route_result;}
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)
{
@ -372,7 +377,31 @@ int midi_workflow_tests()
CHECK(find(&dialog,"Refresh MIDI devices"));press(find(&dialog,"Refresh MIDI devices"));
CHECK(window.settings().midi_output_channel==4);
auto missing=settings;strcpy(missing.midi_input_port,"missing:port");CHECK(window.apply_preferences(missing)==XTC_OK);
CHECK(window.start_configured_backend()==XTC_ERR_NOT_FOUND && !handle.running);CHECK(window.apply_preferences(settings)==XTC_OK);
CHECK(window.start_configured_backend()==XTC_OK && handle.running);
CHECK(routed_input.empty() && routed_output=="Synth:in");
CHECK(!strcmp(window.settings().midi_input_port,"missing:port"));
CHECK(contains(&window,"MIDI device unavailable"));
CHECK(xtc_backend_stop(&handle)==XTC_OK);
strcpy(missing.midi_input_port,"Keyboard:out");strcpy(missing.midi_output_port,"missing:port");
CHECK(window.apply_preferences(missing)==XTC_OK);
CHECK(window.start_configured_backend()==XTC_OK && routed_input=="Keyboard:out" && routed_output.empty());
CHECK(xtc_backend_stop(&handle)==XTC_OK);
CHECK(window.apply_preferences(settings)==XTC_OK);
midi_ports_available=false;
press(find(&window,"Play"));
CHECK(handle.running && routed_input.empty() && routed_output.empty());
CHECK(contains(&window,"MIDI device unavailable"));
CHECK(xtc_backend_stop(&handle)==XTC_OK);
midi_ports_available=true;
CHECK(window.start_configured_backend()==XTC_OK && routed_input=="Keyboard:out" && routed_output=="Synth:in");
CHECK(xtc_backend_stop(&handle)==XTC_OK);
midi_visit_result=XTC_ERR_IO;
CHECK(window.start_configured_backend()==XTC_ERR_IO && !handle.running);
midi_visit_result=XTC_OK;midi_route_result=XTC_ERR_IO;
CHECK(window.start_configured_backend()==XTC_ERR_IO && !handle.running);
midi_route_result=XTC_OK;start_result=XTC_ERR_IO;
CHECK(window.start_configured_backend()==XTC_ERR_IO && !handle.running);
start_result=XTC_OK;
CHECK(window.create_track(true)==XTC_OK && window.open_piano_roll()==XTC_OK);
CHECK(window.piano_roll()->add_note(0,240,64,90,2)==XTC_OK);
std::string midi=std::string(dir)+"/clip.mid";auto *original=s->project();

View file

@ -0,0 +1,129 @@
/* Live GUI playback regression. Connects only to a private software probe;
* neither user preferences nor physical audio/MIDI routes are changed. */
#include "../../src/gui/main_window.hpp"
#include "../../src/gui/sequencer_widget.hpp"
#include <FL/Fl.H>
#include <jack/jack.h>
#include <atomic>
#include <cerrno>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <stdexcept>
#include <string>
#include <unistd.h>
#define REQUIRE(c) do { if(!(c)) throw std::runtime_error(#c); } while(0)
namespace {
struct Probe {
jack_client_t *client=nullptr;
jack_port_t *ports[2]{};
std::atomic<bool> heard[2]{};
std::atomic<bool> invalid{false};
~Probe() { if(client) jack_client_close(client); }
static int process(jack_nframes_t frames, void *data) {
auto &probe=*static_cast<Probe *>(data);
for(int channel=0;channel<2;++channel) {
auto *samples=static_cast<const float *>(jack_port_get_buffer(probe.ports[channel],frames));
for(jack_nframes_t i=0;i<frames;++i) {
if(!std::isfinite(samples[i])) probe.invalid.store(true,std::memory_order_relaxed);
if(std::fabs(samples[i])>.001f) probe.heard[channel].store(true,std::memory_order_relaxed);
}
}
return 0;
}
};
struct TemporaryDirectory {
std::string path;
TemporaryDirectory() {
char name[]="/tmp/xtc-playback-XXXXXX"; REQUIRE(mkdtemp(name)); path=name;
}
~TemporaryDirectory() { std::filesystem::remove_all(path); }
};
Fl_Widget *find(Fl_Group &group, const char *text, bool exact=true) {
for(int i=0;i<group.children();++i) {
auto *child=group.child(i);
if(child->label() && (exact ? !std::strcmp(child->label(),text) : std::strstr(child->label(),text)!=nullptr)) return child;
if(auto *nested=dynamic_cast<Fl_Group *>(child)) if(auto *result=find(*nested,text,exact)) return result;
}
return nullptr;
}
SequencerWidget *timeline(Fl_Group &group) {
for(int i=0;i<group.children();++i) {
if(auto *s=dynamic_cast<SequencerWidget *>(group.child(i))) return s;
if(auto *nested=dynamic_cast<Fl_Group *>(group.child(i))) if(auto *s=timeline(*nested)) return s;
}
return nullptr;
}
void pump(int milliseconds) {
auto end=std::chrono::steady_clock::now()+std::chrono::milliseconds(milliseconds);
while(std::chrono::steady_clock::now()<end) Fl::wait(.01);
}
void click(MainWindow &window, Fl_Widget *button) {
REQUIRE(button && button->active());
int x=Fl::e_x,y=Fl::e_y,key=Fl::e_keysym,state=Fl::e_state;
Fl::e_x=button->x()+button->w()/2; Fl::e_y=button->y()+button->h()/2;
Fl::e_keysym=FL_Button+FL_LEFT_MOUSE; Fl::e_state=FL_BUTTON1;
REQUIRE(Fl::handle(FL_PUSH,&window)==1);
Fl::e_state=0; REQUIRE(Fl::handle(FL_RELEASE,&window)==1);
Fl::e_x=x;Fl::e_y=y;Fl::e_keysym=key;Fl::e_state=state;
}
}
int main() {
try {
TemporaryDirectory directory;
xtc_settings settings{}; xtc_settings_defaults(&settings);
std::strcpy(settings.midi_input_port,"xtc-missing-production-keyboard:out");
std::strcpy(settings.midi_output_port,"xtc-missing-production-synth:in");
std::snprintf(settings.client_name,sizeof(settings.client_name),"xtc-smoke-%ld",static_cast<long>(getpid()));
settings.main_x=0;settings.main_y=0;settings.width=1280;settings.height=800;
settings.transport_detached=settings.sequencer_detached=settings.graph_detached=settings.mixer_detached=0;
settings.loop_enabled=0;settings.stop_after_tail=0;
std::string config=directory.path+"/preferences.conf";
REQUIRE(xtc_settings_save(&settings,config.c_str())==XTC_OK);
Probe probe;
jack_status_t status{};
probe.client=jack_client_open("xtc-playback-probe",JackNoStartServer,&status); REQUIRE(probe.client);
probe.ports[0]=jack_port_register(probe.client,"left",JACK_DEFAULT_AUDIO_TYPE,JackPortIsInput,0);
probe.ports[1]=jack_port_register(probe.client,"right",JACK_DEFAULT_AUDIO_TYPE,JackPortIsInput,0);
REQUIRE(probe.ports[0] && probe.ports[1]);
REQUIRE(jack_set_process_callback(probe.client,Probe::process,&probe)==0 && jack_activate(probe.client)==0);
MainWindow window(false,config.c_str());window.show();pump(100);
REQUIRE(window.open_project("examples/projects/playback-test.xtc")==XTC_OK);
auto *s=timeline(window); REQUIRE(s);
click(window,find(window,"Play"));pump(300);
if(!find(window,"running",false)) {
if(auto *error=find(window,"Start failed",false)) std::fprintf(stderr,"Observed: %s\n",error->label());
}
REQUIRE(find(window,"running",false));
REQUIRE(find(window,"MIDI device unavailable",false));
for(int channel=0;channel<2;++channel) {
std::string source=std::string(settings.client_name)+(channel ? ":output_right" : ":output");
int code=-1;
for(int attempt=0;attempt<50;++attempt) {
code=jack_connect(probe.client,source.c_str(),jack_port_name(probe.ports[channel]));
if(code==0 || code==EEXIST) break;
pump(10);
}
REQUIRE(code==0 || code==EEXIST);
}
auto start=s->position();pump(700);
REQUIRE(s->position()>start);
REQUIRE(probe.heard[0].load() && probe.heard[1].load() && !probe.invalid.load());
click(window,find(window,"Stop"));pump(300);
auto paused=s->position();pump(200);REQUIRE(s->position()==paused);
// Space resumes after a sequencer child has taken focus.
s->take_focus();int old_key=Fl::e_keysym;Fl::e_keysym=' ';
REQUIRE(Fl::handle(FL_SHORTCUT,&window)==1);Fl::e_keysym=old_key;
pump(300);REQUIRE(s->position()>paused);
window.do_callback();REQUIRE(!window.shown());
xtc_settings saved{};REQUIRE(xtc_settings_load(&saved,config.c_str())==XTC_OK);
REQUIRE(!std::strcmp(saved.midi_input_port,settings.midi_input_port) && !std::strcmp(saved.midi_output_port,settings.midi_output_port));
std::puts("Live Play/Stop/Space and stereo playback passed with unavailable saved MIDI devices; preferences retained.");
return 0;
} catch(const std::exception &error) {
std::fprintf(stderr,"Playback regression failed: %s\n",error.what());return 1;
}
}

View file

@ -0,0 +1,32 @@
/* Verify the bundled song produces finite stereo audio in every bar offline. */
#include "xtc/xtc_midi.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>
int main(void)
{
xtc_project *project = NULL;
xtc_midi_plan *midi = NULL;
xtc_plan *graph = NULL;
double energy[4] = {0};
assert(xtc_project_create(&project) == XTC_OK);
assert(xtc_project_load(project, "examples/projects/playback-test.xtc") == XTC_OK);
assert(xtc_midi_plan_create(project, 48000, &midi) == XTC_OK);
assert(xtc_graph_compile(xtc_project_graph(project), 48000, 256, &graph) == XTC_OK);
for (uint64_t pos = 0; pos < 384000; pos += 256) {
float ml[256], mr[256], left[256], right[256];
assert(xtc_midi_plan_render_stereo(midi, pos, NULL, 0, ml, mr, 256) == XTC_OK);
assert(xtc_plan_process_stereo(graph, NULL, NULL, NULL, ml, mr, left, right, 256) == XTC_OK);
for (size_t i = 0; i < 256 && pos + i < 384000; ++i) {
assert(isfinite(left[i]) && isfinite(right[i]));
assert(fabsf(left[i]) <= 1 && fabsf(right[i]) <= 1);
energy[(pos + i) / 96000] += fabsf(left[i]) + fabsf(right[i]);
}
}
for (size_t bar = 0; bar < 4; ++bar) assert(energy[bar] > 1);
xtc_plan_destroy(graph);
xtc_midi_plan_destroy(midi);
xtc_project_destroy(project);
puts("Playback example renders all four bars without devices or media.");
return 0;
}