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

@ -165,6 +165,9 @@ Read `AGENT.md` and `docs/ARCHITECTURE.md`. Inspect the existing implementation,
- Johannes Findeisen <you@hanez.org> — Core developer and Founder
For a song that needs no MIDI hardware, media or plugins, open the
[playback test example](examples/projects/README.md#playback-test).
## License
The project is scaffolded under the MIT License; see `LICENSE`. Review the licenses of any future dependencies before distribution. See also [docs/LICENSES.md](docs/LICENSES.md).
The project is scaffolded under the MIT License; see `LICENSE`. Review the licenses of any future dependencies before distribution. See also [docs/LICENSES.md](docs/LICENSES.md).

View file

@ -78,7 +78,7 @@ if(XTC_XVFB_RUN AND BUILD_TESTING)
"${PROJECT_SOURCE_DIR}/tests/integration/test_configuration_startup.py" $<TARGET_FILE:xtc>)
set_tests_properties(configuration-startup PROPERTIES LABELS gui)
if(JACK_FOUND AND XTC_WITH_JACK)
foreach(kind gui_jack workflow)
foreach(kind gui_jack workflow playback)
add_executable(test_${kind} tests/integration/test_${kind}.cpp)
xtc_warnings(test_${kind})
target_link_libraries(test_${kind} PRIVATE xtc-gui)

View file

@ -11,6 +11,8 @@ endfunction()
if(NOT BUILD_TESTING)
return()
endif()
xtc_executable(test_playback_example tests/integration/test_playback_example.c)
xtc_test(playback-example test_playback_example)
xtc_executable(test_offline tests/unit/test_offline.c)
xtc_test(offline test_offline)
xtc_executable(test_realtime tests/unit/test_realtime.c)

View file

@ -12,9 +12,12 @@ Creation adds an instrument-to-output route if no instrument exists. Existing
instruments retain their routes; use Modular Graph to connect them as needed.
Scheduled MIDI clips now apply their track mix/automation before summing to
paired stereo instrument buffers; live input stays centered and unassigned. No
hardware ports are connected automatically. Connect a MIDI source to the
backend's `midi_in` JACK port, and route audio output explicitly to hear it
while Play is running.
hardware ports are needed for built-in song playback. Preferences can restore
selected MIDI routes on Play/Record; disconnected devices are skipped with a
warning and their names remain saved for the next start. See
[MIDI device preferences](MIDI_WORKFLOWS.md). You can also connect a MIDI source
to the backend's `midi_in` JACK port manually. Route audio output explicitly to
hear it while Play is running.
The piano roll shows all 128 pitches and edits clip-source time. Click blank
space to draw a one-beat note (velocity 100, filtered channel or channel 0);

View file

@ -50,8 +50,11 @@ ports are visible only when the host exposes them through its MIDI bridge.
Input channel `-1` accepts all channels; `0..15` selects MIDI channel 1..16.
Output channel `-1` preserves note channels; `0..15` forces a channel. Thru
defaults off. Apply saves these choices; the next Play/Record restores the
selected routes. A missing port rejects that start and stops the backend,
preserving the saved choice for retry. Reconnect audio after server loss, then
available selected routes. Missing ports produce a nonblocking warning; playback
continues with built-in instruments and audio clips. Saved device choices remain
unchanged and are checked again on every start, including Record. An available
counterpart still connects. Backend activation and other routing errors remain
errors. Reconnect audio after server loss, then
retry Play. Defaults also apply when playback resumes after a loop edit. MIDI
input/thru currently requires active transport. Existing editing and
project_defaults tabs contain note length, velocity, channel, filters and

View file

@ -3705,6 +3705,40 @@ is unaffected.
---
## Phase 112 — Playback without production MIDI devices
### Tasks and acceptance criteria
- [x] Reproduce the playback failure caused by disconnected saved MIDI devices.
- [x] Skip unavailable MIDI input/output routes in the GUI without changing saved
preferences; connect available counterparts and retry devices on each start.
- [x] Show a nonblocking warning while allowing built-in instruments and audio
playback. Preserve genuine enumeration, activation and routing failures.
- [x] Add a self-contained four-bar `examples/projects/playback-test.xtc` song.
- [x] Cover absent input/output/both, returning devices, retained preferences and
real backend failures in the fake-backend GUI regression.
- [x] Verify the example offline and test real Play/Stop/Space, playhead movement
and finite nonzero stereo output through a private JACK software probe.
- [x] Update MIDI/preferences help, example and integration-test documentation.
### Implementation status
Implemented on 2026-11-06. The saved-device failure was reproduced with the live
GUI test before the fix. GUI starts now enumerate optional MIDI routes before
calling the strict backend start API. Missing devices warn without preventing
playback; the C backend API retains its strict routing contract. No settings
format or audio callback changes are required. The example uses only a built-in
sine instrument, needs no external files or plugins, and renders eight seconds
at 120 BPM. Audio outputs still require normal JACK routing to hear speakers.
The live regression connects only to a private software probe and uses temporary
preferences; no physical listening/hardware qualification is claimed.
Validation: the warning-enabled CMake build and all 57 default CTests passed,
including offline example rendering and GUI MIDI-device regressions. The live
Play/Stop/Space/stereo check also passed. See the integration-test guide for commands.
---
## Recommended Execution Order
Reorder the phases when the audit reveals a blocking dependency. Do not mark a

View file

@ -10,6 +10,9 @@ WAVs remain in the ignored build directory. See docs/AUDIO_QUALITY.md for the
listening sequence and the distinction between automated checks and human
listening acceptance. It never starts a backend or plays audio.
`projects/playback-test.xtc` is a four-bar playback check requiring no MIDI
hardware, media or plugins. See [example songs](projects/README.md).
`projects/midi-demo.xtc` is an original media/plugin-free MIDI example installed
under `share/xtc/examples`. See projects/README.md for opening and playback.

View file

@ -1,4 +1,4 @@
# MIDI demonstration
# Example songs
`midi-demo.xtc` is an original MIT-licensed metadata-only example based on the
repository v10 fixture. It contains one A4 MIDI note, a gain node and an output.
@ -10,3 +10,13 @@ Save a working copy in your own folder; installed examples may be read-only.
for the Phase 33 workspace. Its 16 MIDI clips contain 128 notes for built-in sine
voices. It needs no plugins or media. Open it to explore colored tracks, MIDI
previews, clip selection and the piano roll. Save a copy before editing.
## Playback test
Open `playback-test.xtc` with `./build/xtc examples/projects/playback-test.xtc`.
Press Play or Space to hear a four-bar sine melody (eight seconds at 120 BPM).
It uses the built-in instrument and needs no media, plugins or MIDI devices,
even when Preferences remembers disconnected production devices. Missing MIDI
routes produce a warning and their names stay saved for the next start.
Connect xtc's JACK stereo outputs to your speakers to listen. Save a working
copy before editing. The melody is original and MIT-licensed.

View file

@ -0,0 +1,56 @@
XTC_PROJECT 10
APP 0 1 0
TEMPO 120000
IDS 1 1
GRAPH 1 3 3 2
NODE 0 6 00000000
NODE 1 1 3e800000
NODE 2 4 00000000
EDGE 0 1
EDGE 1 2
TRACKS 1
TRACK 0 506c61796261636b207465737420286275696c742d696e2073696e6529
MIX 3f800000 00000000 0 0
ROUTE 0 18446744073709551615
CLIPS 1
CLIP 0 0 0 15360 0 15360 466f75722062617220706c61796261636b2074657374
ASSET -
MIDI 1
AUDIO 3f800000 0 0
NOTES 16 16
NOTE 0 0 0 720 60 96 0
NOTE 1 0 960 720 64 96 0
NOTE 2 0 1920 720 67 96 0
NOTE 3 0 2880 720 72 96 0
NOTE 4 0 3840 720 67 96 0
NOTE 5 0 4800 720 64 96 0
NOTE 6 0 5760 720 62 96 0
NOTE 7 0 6720 720 65 96 0
NOTE 8 0 7680 720 69 96 0
NOTE 9 0 8640 720 72 96 0
NOTE 10 0 9600 720 69 96 0
NOTE 11 0 10560 720 65 96 0
NOTE 12 0 11520 720 67 96 0
NOTE 13 0 12480 720 64 96 0
NOTE 14 0 13440 720 62 96 0
NOTE 15 0 14400 720 60 96 0
AUTOMATION 1
POINT 0 0 0 3f800000 0
CONTROL_MAPS 16
MAP 0 7 10 64
MAP 1 7 10 64
MAP 2 7 10 64
MAP 3 7 10 64
MAP 4 7 10 64
MAP 5 7 10 64
MAP 6 7 10 64
MAP 7 7 10 64
MAP 8 7 10 64
MAP 9 7 10 64
MAP 10 7 10 64
MAP 11 7 10 64
MAP 12 7 10 64
MAP 13 7 10 64
MAP 14 7 10 64
MAP 15 7 10 64
END

View file

@ -913,13 +913,16 @@ void MainWindow::start_engine()
if (result != XTC_OK)
report_error("Start failed; check audio and saved MIDI devices in Preferences", result);
else {
set_label(error_, "");
set_label(error_, midi_route_warning_.c_str());
const char *issue = xtc_backend_asset_issue(backend_);
if (issue[0]) {
char message[640];
std::snprintf(message, sizeof(message),
"Missing/unreadable audio (silent): %s. Use File/Relink selected audio or Load sampler WAV to repair.", issue);
set_label(error_, message);
std::string warning=midi_route_warning_;
if(!warning.empty())warning+=" ";
warning+=message;
set_label(error_, warning.c_str());
}
}
refresh_status();
@ -2573,8 +2576,41 @@ std::vector<std::string> MainWindow::midi_devices(bool input)
}
xtc_result MainWindow::start_configured_backend()
{
midi_route_warning_.clear();
const char *input=settings_.midi_input_port, *output=settings_.midi_output_port;
// Saved production devices are optional. Keep their names so a later start
// reconnects them, but do not ask the strict backend API to route absent ports.
auto available=[this](const char *name, xtc_port_direction direction, bool &found) {
struct Search { const char *name; bool found; } search{name,false};
xtc_result result=xtc_backend_visit_midi_ports(backend_,direction,
[](const char *port,void *context) {
auto &search=*static_cast<Search *>(context);
search.found=std::strcmp(port,search.name)==0;
return search.found ? 1 : 0;
},&search);
found=search.found;
return result;
};
auto optional_route=[&](const char *&name,xtc_port_direction direction) {
if(!*name)return XTC_OK;
bool found=false;
xtc_result result=available(name,direction,found);
if(result==XTC_OK && !found) {
if(midi_route_warning_.empty())midi_route_warning_="MIDI device unavailable: ";
else midi_route_warning_+=", ";
midi_route_warning_+=name;
name="";
}
return result;
};
xtc_result r=xtc_backend_set_midi_options(backend_,static_cast<int>(settings_.midi_input_channel),static_cast<int>(settings_.midi_output_channel),settings_.midi_thru!=0);
if(r==XTC_OK)r=xtc_backend_start_routed(backend_,settings_.midi_input_port,settings_.midi_output_port);
if(r==XTC_OK)r=optional_route(input,XTC_PORT_OUTPUT);
if(r==XTC_OK)r=optional_route(output,XTC_PORT_INPUT);
if(r==XTC_OK)r=xtc_backend_start_routed(backend_,input,output);
if(r==XTC_OK) {
if(!midi_route_warning_.empty())midi_route_warning_+=". Playback continues; saved device choices are kept.";
set_label(error_,midi_route_warning_.c_str());
}
return r;
}
void MainWindow::show_preferences()

View file

@ -254,6 +254,7 @@ class MainWindow : public Fl_Double_Window {
bool transport_detached_=false, mixer_detached_=false;
xtc_settings settings_{};
std::string configuration_;
std::string midi_route_warning_;
bool configuration_valid_=true;
Fl_Check_Button *loop_button_=nullptr;
xtc_result configure_loop(const xtc_project *project,const xtc_settings &settings);

View file

@ -84,7 +84,7 @@ class PreferencesDialog : public Fl_Double_Window {
}
if(scroll) { scroll->end(); group->end(); } tabs->end();
tabs->begin();auto *midi_info=new Fl_Group(12,82,1026,478,"MIDI help");
auto *help=new Fl_Box(42,110,950,190,"Choose JACK/PipeWire MIDI ports on the midi tab. Refresh to see newly connected devices.\nYou can also type a remembered port name while it is offline; blank means no automatic route.\nRouting is restored on Play/Record; missing ports are reported without deleting preferences.\nInput/output channels use 0..15 for MIDI channels 1..16. MIDI thru defaults off.\nNote length/velocity/channel defaults are on editing; CC defaults are on project_defaults.\nHardware MIDI devices must be exposed by your host JACK/PipeWire MIDI bridge.");
auto *help=new Fl_Box(42,110,950,190,"Choose JACK/PipeWire MIDI ports on the midi tab. Refresh to see newly connected devices.\nYou can also type a remembered port name while it is offline; blank means no automatic route.\nRouting is restored on Play/Record; missing ports are skipped with a warning; playback continues and choices stay saved.\nInput/output channels use 0..15 for MIDI channels 1..16. MIDI thru defaults off.\nNote length/velocity/channel defaults are on editing; CC defaults are on project_defaults.\nHardware MIDI devices must be exposed by your host JACK/PipeWire MIDI bridge.");
help->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE|FL_ALIGN_WRAP);midi_info->end();tabs->end();
refresh_devices();
auto *refresh=new Fl_Button(16,614,200,30,"Refresh MIDI devices");

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