Skip to content

Commit cf21987

Browse files
committed
GUI - send the Link settings again whenever the engine is ready, so Share Audio survives a fresh launch
1 parent efbabff commit cf21987

8 files changed

Lines changed: 150 additions & 5 deletions

File tree

‎app/gui-tests/CMakeLists.txt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,11 @@ add_executable(${PROJECT_NAME}
6363
${GUI_TESTS_ROOT}/nodetreemotion.test.cpp
6464
${GUI_TESTS_ROOT}/nodetreegraph_look.test.cpp
6565
${GUI_TESTS_ROOT}/loopscope_lifecycle.test.cpp
66+
${GUI_TESTS_ROOT}/linkaudio_engine_ready.test.cpp
6667
${GUI_ROOT}/widgets/nodetreegraph.cpp
6768
${GUI_ROOT}/widgets/trackspanel.cpp
69+
${GUI_ROOT}/widgets/linkaudiostreamswidget.cpp
70+
${GUI_ROOT}/widgets/linkvisibilitytoggle.cpp
6871
${GUI_ROOT}/widgets/tutorialpane.cpp
6972
${GUI_ROOT}/widgets/sonicpiscintilla.cpp
7073
${GUI_ROOT}/widgets/sonicpilexer.cpp
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//--
2+
// This file is part of Sonic Pi: http://sonic-pi.net
3+
// Full project source: https://github.com/sonic-pi-net/sonic-pi
4+
// License: https://github.com/sonic-pi-net/sonic-pi/blob/main/LICENSE.md
5+
//
6+
// Copyright 2026 by Sam Aaron (http://sam.aaron.name).
7+
// All rights reserved.
8+
//
9+
// Permission is granted for use, copying, modification, and
10+
// distribution of modified versions of this work as long as this
11+
// notice is included.
12+
//++
13+
14+
// The window builds the Link panel before there is an engine, so anything it
15+
// sends then reaches nothing. Share Audio was sent only then: a fresh launch
16+
// with it On left the engine publishing nothing, and advertising no channels,
17+
// while the button said On. The panel sends it again whenever an engine is
18+
// ready: on first boot, and after every restart.
19+
20+
#include <catch2/catch_test_macros.hpp>
21+
22+
#include <QApplication>
23+
#include <QTemporaryDir>
24+
25+
#include <memory>
26+
#include <string>
27+
#include <vector>
28+
29+
#include "api/osc/osc_pkt.hh"
30+
#include "api/sonicpi_api.h"
31+
#include "utils/gui_settings.h"
32+
#include "widgets/linkaudiostreamswidget.h"
33+
34+
namespace {
35+
36+
using namespace SonicPi;
37+
38+
struct NoClient : IAPIClient
39+
{
40+
void Report(const MessageInfo&) override {}
41+
void Status(const StatusInfo&) override {}
42+
void Cue(const CueInfo&) override {}
43+
void Midi(const MidiInfo&) override {}
44+
void Version(const VersionInfo&) override {}
45+
void AudioDataAvailable(ProcessedAudioPtr) override {}
46+
void Buffer(const BufferInfo&) override {}
47+
void ActiveLinks(const int) override {}
48+
void BPM(const double) override {}
49+
void Scsynth(const ScsynthInfo&) override {}
50+
void AudioDevices(const AudioDevicesInfo&) override {}
51+
void AudioInputDevices(const AudioInputDevicesInfo&) override {}
52+
void AudioDeviceConfig(const AudioDeviceConfigInfo&) override {}
53+
void SupersonicSetup(int, int) override {}
54+
void SpiderReady() override {}
55+
};
56+
57+
// An engine that only listens: every message the GUI sends it, in order.
58+
struct ListeningEngine : SonicPiAPI
59+
{
60+
explicit ListeningEngine(IAPIClient* client) : SonicPiAPI(client, APIProtocol::UDP, LogOption::Console) {}
61+
62+
bool SupersonicSendOSC(oscpkt::Message m) override
63+
{
64+
sent.push_back(m);
65+
return true;
66+
}
67+
68+
// The int the last message to `address` carried, or -1 if none came.
69+
int lastInt(const std::string& address) const
70+
{
71+
for (auto it = sent.rbegin(); it != sent.rend(); ++it)
72+
{
73+
int32_t v = 0;
74+
if (it->addressPattern() == address && it->arg().popInt32(v).isOkNoMoreArgs())
75+
return v;
76+
}
77+
return -1;
78+
}
79+
80+
std::vector<oscpkt::Message> sent;
81+
};
82+
83+
// A gui.ini of the test's own, put back as it was afterwards.
84+
struct OwnSettings
85+
{
86+
QTemporaryDir dir;
87+
QString before = SonicPi::guiSettingsPath();
88+
OwnSettings() { SonicPi::setGuiSettingsPath(dir.filePath("gui.ini")); }
89+
~OwnSettings() { SonicPi::setGuiSettingsPath(before); }
90+
};
91+
92+
const std::string kPublish = "/clockwork/clock/audio/publish/set";
93+
94+
} // namespace
95+
96+
TEST_CASE("Link Audio: Share Audio reaches every engine that comes up", "[link]")
97+
{
98+
for (const bool share : { true, false })
99+
{
100+
INFO("Share Audio " << (share ? "On" : "Off"));
101+
OwnSettings settings;
102+
SonicPi::guiSettings().setValue("link/audioPublish", share);
103+
NoClient client;
104+
auto engine = std::make_shared<ListeningEngine>(&client);
105+
LinkAudioStreamsWidget panel(engine);
106+
engine->sent.clear(); // built before the engine was up: nothing heard it
107+
108+
panel.onEngineReady(); // first boot
109+
CHECK(engine->lastInt(kPublish) == (share ? 1 : 0));
110+
111+
engine->sent.clear();
112+
panel.onEngineReady(); // a restarted engine starts with none of it
113+
CHECK(engine->lastInt(kPublish) == (share ? 1 : 0));
114+
}
115+
}

‎app/gui/mainwindow.cpp‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9608,7 +9608,7 @@ void MainWindow::onSupersonicSetup(int sampleRate, int bufferSize)
96089608
// not initial boot (SupersonicEngine gates the emit on mWorldRebuilt);
96099609
// first-boot attach is handled by onSpiderReady.
96109610
m_spAPI->AudioProcessor_ResetConnection();
9611-
if (tracksPanel) tracksPanel->onEngineReady();
9611+
onEngineReady();
96129612
}
96139613

96149614
void MainWindow::onSpiderReady()
@@ -9619,7 +9619,13 @@ void MainWindow::onSpiderReady()
96199619
// First-boot scope-reader attach. /clockwork/setup covers
96209620
// subsequent cold-swap re-attaches; the two handlers are disjoint.
96219621
m_spAPI->AudioProcessor_ResetConnection();
9622+
onEngineReady();
9623+
}
9624+
9625+
void MainWindow::onEngineReady()
9626+
{
96229627
if (tracksPanel) tracksPanel->onEngineReady();
9628+
if (metroPane) metroPane->onEngineReady();
96239629
}
96249630

96259631
void MainWindow::onAudioSwitchDone(const SonicPi::AudioSwitchOutcome& outcome)

‎app/gui/mainwindow.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,9 @@ private slots:
569569
void setMidiPortEnabled(QString direction, QString name, bool enabled);
570570
void setGamepadDeviceEnabled(QString name, bool enabled);
571571
void honourPrefs();
572+
// An engine is ready: on first boot, and after every restart. The panels
573+
// that talk to it catch up with it here.
574+
void onEngineReady();
572575

573576
// Toggle the bottom Help/Debug dock (double-clicking the divider bar).
574577

‎app/gui/widgets/linkaudiostreamswidget.cpp‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,16 +225,18 @@ LinkAudioStreamsWidget::LinkAudioStreamsWidget(std::shared_ptr<SonicPiAPI> spAPI
225225
m_pollTimer->setInterval(2000);
226226
connect(m_pollTimer, &QTimer::timeout, this, &LinkAudioStreamsWidget::refresh);
227227

228-
// Push the loaded publish flag so SuperSonic starts matching the UI.
229-
if (m_spAPI) m_spAPI->SetLinkAudioPublish(m_shareAudioBox->isChecked());
230-
231228
// Initial visibility from QSettings (Local default); Link is session-only,
232229
// always off on launch.
233230
applyMasterVisibility(
234231
SonicPi::guiSettings().value("supersonic/networkVisibility", 1).toInt());
235232
applyLinkEnabled(false);
236233
}
237234

235+
void LinkAudioStreamsWidget::onEngineReady()
236+
{
237+
if (m_spAPI) m_spAPI->SetLinkAudioPublish(m_shareAudioBox->isChecked());
238+
}
239+
238240
void LinkAudioStreamsWidget::setVisibilityColors(const QColor& thumb,
239241
const QColor& activeIcon)
240242
{

‎app/gui/widgets/linkaudiostreamswidget.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ class LinkAudioStreamsWidget : public QWidget
8585
// reach the theme itself). Called by SonicPiMetro, which owns the theme.
8686
void setVisibilityColors(const QColor& thumb, const QColor& activeIcon);
8787

88+
// Share Audio, sent whenever an engine is ready: on first boot and after
89+
// every restart. A new engine starts with it off, and this panel is built
90+
// before there is one to hear it.
91+
void onEngineReady();
92+
8893
public slots:
8994
// Called by SonicPiMetro when Visibility scope (Local/Net) changes.
9095
void applyMasterVisibility(int mode);

‎app/gui/widgets/sonicpimetro.cpp‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,6 @@ SonicPiMetro::SonicPiMetro(std::shared_ptr<SonicPi::QtAPIClient> spClient, std::
262262
m_networkMode = static_cast<SonicPi::SonicPiAPI::LinkVisibility>(savedNet);
263263
}
264264
m_linkEnabled = false;
265-
pushLinkConfigToServer();
266265
updateLinkButtonDisplay();
267266
updateRowVisibility();
268267
}
@@ -386,6 +385,14 @@ void SonicPiMetro::toggleLink()
386385
if (m_linkEnabled) linkDisable(); else linkEnable();
387386
}
388387

388+
void SonicPiMetro::onEngineReady()
389+
{
390+
mutex->lock();
391+
pushLinkConfigToServer();
392+
mutex->unlock();
393+
if (linkStreamsWidget) linkStreamsWidget->onEngineReady();
394+
}
395+
389396
void SonicPiMetro::pushLinkConfigToServer()
390397
{
391398
if (!m_spAPI) return;

‎app/gui/widgets/sonicpimetro.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ class SonicPiMetro : public QWidget
4747
void setFocusTimeWarpScrubber();
4848
void toggleLink(); // Flips the Link button.
4949

50+
// The Link settings, sent whenever an engine is ready: on first boot and
51+
// after every restart. A new engine starts with none of them.
52+
void onEngineReady();
53+
5054
// Expand or collapse the inline Link Audio Streams panel below the
5155
// main metro row.
5256
void toggleLinkAudioStreams();

0 commit comments

Comments
 (0)