sequencer play fix.
This commit is contained in:
parent
e664414796
commit
d6ded83beb
16 changed files with 372 additions and 16 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
56
examples/projects/playback-test.xtc
Normal file
56
examples/projects/playback-test.xtc
Normal 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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
129
tests/integration/test_playback.cpp
Normal file
129
tests/integration/test_playback.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
32
tests/integration/test_playback_example.c
Normal file
32
tests/integration/test_playback_example.c
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue