sequencer play fix.
This commit is contained in:
parent
e664414796
commit
d6ded83beb
16 changed files with 372 additions and 16 deletions
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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue