/* Native JUCE GUI playback regression. Connects only to a private software probe; * neither user preferences nor physical audio/MIDI routes are changed. */ #include "../../src/gui/JUCE/main_window.hpp" #include "../../src/gui/JUCE/sequencer_track_widget.hpp" #include "../../src/gui/JUCE/sequencer_widget.hpp" #include "../../src/gui/JUCE/ui.hpp" #include #include #include #include #include #include #include #include #include #include #include #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 heard[2]{}; std::atomic invalid{false}; ~Probe() { if (client) jack_client_close(client); } static int process(jack_nframes_t frames, void *data) { auto &probe = *static_cast(data); for (int channel = 0; channel < 2; ++channel) { auto *samples = static_cast(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); } }; UiWidget *find(UiGroup &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(child)) if (auto *result = find(*nested, text, exact)) return result; } return nullptr; } SequencerWidget *timeline(UiGroup &group) { for (int i = 0; i < group.children(); ++i) { if (auto *s = dynamic_cast(group.child(i))) return s; if (auto *nested = dynamic_cast(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) Ui::wait(.01); } void mouse_click(juce::Component *target, juce::Point point) { REQUIRE(target); auto now = juce::Time::getCurrentTime(); juce::MouseEvent down(juce::Desktop::getInstance().getMainMouseSource(), point, juce::ModifierKeys(juce::ModifierKeys::leftButtonModifier), 1, 0, 0, 0, 0, target, target, now, point, now, 1, false); target->mouseDown(down); juce::MouseEvent up(juce::Desktop::getInstance().getMainMouseSource(), point, juce::ModifierKeys(), 1, 0, 0, 0, 0, target, target, now, point, now, 1, false); target->mouseUp(up); } void click(MainWindow &, UiWidget *button) { REQUIRE(button && button->active()); // Deliver JUCE MouseEvents to the native TextButton child. juce::Component *target = nullptr; for (int i = 0; i < button->getNumChildComponents(); ++i) if (dynamic_cast(button->getChildComponent(i))) target = button->getChildComponent(i); REQUIRE(target); mouse_click(target, target->getLocalBounds().toFloat().getCentre()); } } // namespace int main() { UiInitialiser initialise; 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(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(); REQUIRE(static_cast(s)->keyPressed(juce::KeyPress(' '))); pump(300); REQUIRE(s->position() > paused); // A child at local (0,0) must paint consistently when composed into // the full workstation; this catches retained-coordinate origin errors. xtc_track_info track{}; REQUIRE(xtc_project_get_track(s->project(), 0, &track) == XTC_OK); auto *header = s->track_widget(track.id)->header(); auto *menu = window.child(0); REQUIRE(header->isShowing() && menu->isShowing()); mouse_click(header, {14, 12}); pump(60); REQUIRE(s->track_expanded(track.id)); auto menu_image = menu->createComponentSnapshot(menu->getLocalBounds()); auto header_image = header->createComponentSnapshot(header->getLocalBounds()); auto image = window.createComponentSnapshot(window.getLocalBounds()); REQUIRE(image.getPixelAt(2, 2) == menu_image.getPixelAt(2, 2)); REQUIRE(image.getPixelAt(header->x() + 5, header->y() + 5) == header_image.getPixelAt(5, 5)); if (const char *capture = std::getenv("XTC_JUCE_SCREENSHOT")) { juce::FileOutputStream stream{juce::File(capture)}; REQUIRE(stream.openedOk()); juce::PNGImageFormat png; REQUIRE(png.writeImageToStream(image, stream)); } 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; } }