28#include <CustomLookAndFeel.h>
29#include <WebUpdateDetector.h>
40 m_config = std::make_unique<MemaMoAppConfiguration>(JUCEAppBasics::AppConfigurationBase::getDefaultConfigFilePath());
41 m_config->addDumper(
this);
44 if (!m_config->isValid())
46 m_config->ResetToDefault();
49 m_networkConnection = std::make_unique<InterprocessConnectionImpl>();
50 m_networkConnection->onConnectionMade = [=]() {
53 std::vector<Mema::SerializableMessage::SerializableMessageType> desiredTrafficTypes = {
59 m_networkConnection->sendMessage(std::make_unique<Mema::DataTrafficTypeSelectionMessage>(desiredTrafficTypes)->getSerializedMessage());
61 setStatus(Status::Monitoring);
63 m_networkConnection->onConnectionLost = [=]() {
68 setStatus(Status::Connecting);
70 m_networkConnection->onMessageReceived = [=](
const juce::MemoryBlock& message) {
74 m_settingsHostLookAndFeelId = epm->getPaletteStyle();
75 jassert(m_settingsHostLookAndFeelId >= JUCEAppBasics::CustomLookAndFeel::PS_Dark && m_settingsHostLookAndFeelId <= JUCEAppBasics::CustomLookAndFeel::PS_Light);
79 m_settingsItems[1].second = 1;
83 else if (m_monitorComponent &&
nullptr != knownMessage && Status::Monitoring == m_currentStatus)
85 m_monitorComponent->handleMessage(*knownMessage);
90 m_monitorComponent = std::make_unique<MemaMoComponent>();
91 m_monitorComponent->onExitClick = [=]() {
92 setStatus(Status::Discovering);
94 addAndMakeVisible(m_monitorComponent.get());
96 m_discoverComponent = std::make_unique<MemaClientDiscoverComponent>();
98 m_discoverComponent->onServiceSelected = [=](
const JUCEAppBasics::SessionMasterAwareService& selectedService) {
99 m_selectedService = selectedService;
104 m_config->triggerConfigurationDump(
false);
106 addAndMakeVisible(m_discoverComponent.get());
108 m_connectingComponent = std::make_unique<MemaClientConnectingComponent>();
109 addAndMakeVisible(m_connectingComponent.get());
111 m_aboutComponent = std::make_unique<AboutComponent>(BinaryData::MemaMoRect_png, BinaryData::MemaMoRect_pngSize);
112 m_aboutButton = std::make_unique<juce::DrawableButton>(
"About", juce::DrawableButton::ButtonStyle::ImageFitted);
113 m_aboutButton->setTooltip(juce::String(
"About") + juce::JUCEApplication::getInstance()->getApplicationName());
114 m_aboutButton->onClick = [
this] {
115 juce::PopupMenu aboutMenu;
116 aboutMenu.addCustomItem(1, std::make_unique<CustomAboutItem>(m_aboutComponent.get(), juce::Rectangle<int>(250, 250)),
nullptr, juce::String(
"Info about") + juce::JUCEApplication::getInstance()->getApplicationName());
117 aboutMenu.showMenuAsync(juce::PopupMenu::Options());
119 m_aboutButton->setAlwaysOnTop(
true);
120 m_aboutButton->setColour(juce::DrawableButton::ColourIds::backgroundColourId, juce::Colours::transparentBlack);
121 m_aboutButton->setColour(juce::DrawableButton::ColourIds::backgroundOnColourId, juce::Colours::transparentBlack);
122 addAndMakeVisible(m_aboutButton.get());
126 auto fullscreenShortCutHint = std::string(
" (Ctrl+F11)");
128 auto fullscreenShortCutHint = std::string(
" (Cmd+Ctrl+F)");
132 m_settingsItems[MemaMoSettingsOption::LookAndFeel_FollowHost] = std::make_pair(
"Follow Mema", 0);
133 m_settingsItems[MemaMoSettingsOption::LookAndFeel_Dark] = std::make_pair(
"Dark", 1);
134 m_settingsItems[MemaMoSettingsOption::LookAndFeel_Light] = std::make_pair(
"Light", 0);
155#if JUCE_WINDOWS || JUCE_MAC
157 m_settingsItems[MemaMoSettingsOption::FullscreenWindowMode] = std::make_pair(
"Toggle fullscreen mode" + fullscreenShortCutHint, 0);
160 m_settingsButton = std::make_unique<juce::DrawableButton>(
"Settings", juce::DrawableButton::ButtonStyle::ImageFitted);
161 m_settingsButton->setTooltip(juce::String(
"Settings for") + juce::JUCEApplication::getInstance()->getApplicationName());
162 m_settingsButton->onClick = [
this] {
163 juce::PopupMenu lookAndFeelSubMenu;
164 for (
int i = MemaMoSettingsOption::LookAndFeel_First; i <= MemaMoSettingsOption::LookAndFeel_Last; i++)
165 lookAndFeelSubMenu.addItem(i, m_settingsItems[i].first,
true, m_settingsItems[i].second == 1);
167 juce::PopupMenu outputVisuTypeSubMenu;
169 outputVisuTypeSubMenu.addItem(i, m_settingsItems[i].first,
true, m_settingsItems[i].second == 1);
171 juce::PopupMenu meteringColourSubMenu;
173 meteringColourSubMenu.addItem(i, m_settingsItems[i].first,
true, m_settingsItems[i].second == 1);
175 juce::PopupMenu settingsMenu;
176 settingsMenu.addSubMenu(
"LookAndFeel", lookAndFeelSubMenu);
177 settingsMenu.addSubMenu(
"Output monitoring", outputVisuTypeSubMenu);
178 settingsMenu.addSubMenu(
"Metering colour", meteringColourSubMenu);
179#if JUCE_WINDOWS || JUCE_MAC
180 settingsMenu.addSeparator();
181 settingsMenu.addItem(MemaMoSettingsOption::FullscreenWindowMode, m_settingsItems[MemaMoSettingsOption::FullscreenWindowMode].first,
true,
false);
183 settingsMenu.showMenuAsync(juce::PopupMenu::Options(), [=](
int selectedId) {
184 handleSettingsMenuResult(selectedId);
186 m_config->triggerConfigurationDump();
189 m_settingsButton->setAlwaysOnTop(
true);
190 m_settingsButton->setColour(juce::DrawableButton::ColourIds::backgroundColourId, juce::Colours::transparentBlack);
191 m_settingsButton->setColour(juce::DrawableButton::ColourIds::backgroundOnColourId, juce::Colours::transparentBlack);
192 addAndMakeVisible(m_settingsButton.get());
194 m_disconnectButton = std::make_unique<juce::DrawableButton>(
"Disconnect", juce::DrawableButton::ButtonStyle::ImageFitted);
195 m_disconnectButton->setTooltip(juce::String(
"Disconnect ") + juce::JUCEApplication::getInstance()->getApplicationName());
196 m_disconnectButton->onClick = [
this] {
197 if (m_networkConnection)
198 m_networkConnection->disconnect();
200 m_selectedService = {};
201 if (m_discoverComponent)
202 m_discoverComponent->resetServices();
205 m_config->triggerConfigurationDump();
207 setStatus(Status::Discovering);
209 m_disconnectButton->setAlwaysOnTop(
true);
210 m_disconnectButton->setColour(juce::DrawableButton::ColourIds::backgroundColourId, juce::Colours::transparentBlack);
211 m_disconnectButton->setColour(juce::DrawableButton::ColourIds::backgroundOnColourId, juce::Colours::transparentBlack);
212 addAndMakeVisible(m_disconnectButton.get());
214#ifdef RUN_MESSAGE_TESTS
222#define IGNORE_UPDATES
223#elif defined JUCE_ANDROID
225#define IGNORE_UPDATES
228#if defined IGNORE_UPDATES
230 auto updater = JUCEAppBasics::WebUpdateDetector::getInstance();
231 updater->SetReferenceVersion(ProjectInfo::versionString);
232 updater->SetDownloadUpdateWebAddress(
"https://github.com/christianahrens/mema/releases/latest");
233 updater->CheckForNewVersion(
true,
"https://raw.githubusercontent.com/ChristianAhrens/Mema/refs/heads/main/");
238 m_config->addWatcher(
this);
241 setWantsKeyboardFocus(
true);
250 auto safety = JUCEAppBasics::iOS_utils::getDeviceSafetyMargins();
251 auto safeBounds = getLocalBounds();
252 safeBounds.removeFromTop(safety._top);
253 safeBounds.removeFromBottom(safety._bottom);
254 safeBounds.removeFromLeft(safety._left);
255 safeBounds.removeFromRight(safety._right);
257 switch (m_currentStatus)
259 case Status::Monitoring:
260 m_connectingComponent->setVisible(
false);
261 m_discoverComponent->setVisible(
false);
262 m_monitorComponent->setVisible(
true);
263 m_monitorComponent->setBounds(safeBounds);
265 case Status::Connecting:
266 m_monitorComponent->setVisible(
false);
267 m_discoverComponent->setVisible(
false);
268 m_connectingComponent->setVisible(
true);
269 m_connectingComponent->setBounds(safeBounds);
271 case Status::Discovering:
273 m_connectingComponent->setVisible(
false);
274 m_monitorComponent->setVisible(
false);
275 m_discoverComponent->setVisible(
true);
276 m_discoverComponent->setBounds(safeBounds);
280 m_aboutButton->setBounds(safeBounds.removeFromTop(35).removeFromLeft(30).removeFromBottom(30));
281 m_settingsButton->setBounds(safeBounds.removeFromTop(35).removeFromLeft(30).removeFromBottom(30));
282 m_disconnectButton->setBounds(safeBounds.removeFromTop(35).removeFromLeft(30).removeFromBottom(30));
287 g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId));
292 auto aboutButtonDrawable = juce::Drawable::createFromSVG(*juce::XmlDocument::parse(BinaryData::question_mark_24dp_svg).get());
293 aboutButtonDrawable->replaceColour(juce::Colours::black, getLookAndFeel().findColour(juce::TextButton::ColourIds::textColourOnId));
294 m_aboutButton->setImages(aboutButtonDrawable.get());
296 auto settingsDrawable = juce::Drawable::createFromSVG(*juce::XmlDocument::parse(BinaryData::settings_24dp_svg).get());
297 settingsDrawable->replaceColour(juce::Colours::black, getLookAndFeel().findColour(juce::TextButton::ColourIds::textColourOnId));
298 m_settingsButton->setImages(settingsDrawable.get());
300 auto disconnectDrawable = juce::Drawable::createFromSVG(*juce::XmlDocument::parse(BinaryData::link_off_24dp_svg).get());
301 disconnectDrawable->replaceColour(juce::Colours::black, getLookAndFeel().findColour(juce::TextButton::ColourIds::textColourOnId));
302 m_disconnectButton->setImages(disconnectDrawable.get());
304 applyMeteringColour();
310 handleSettingsMenuResult(option);
313void MainComponent::handleSettingsMenuResult(
int selectedId)
317 else if (MemaMoSettingsOption::LookAndFeel_First <= selectedId && MemaMoSettingsOption::LookAndFeel_Last >=
selectedId)
318 handleSettingsLookAndFeelMenuResult(
selectedId);
319 else if (MemaMoSettingsOption::OutputVisuType_First <= selectedId && MemaMoSettingsOption::OutputVisuType_Last >=
selectedId)
320 handleSettingsOutputVisuTypeMenuResult(
selectedId);
321 else if (MemaMoSettingsOption::MeteringColour_First <= selectedId && MemaMoSettingsOption::MeteringColour_Last >=
selectedId)
322 handleSettingsMeteringColourMenuResult(
selectedId);
323 else if (MemaMoSettingsOption::FullscreenWindowMode ==
selectedId)
324 handleSettingsFullscreenModeToggleResult();
329void MainComponent::handleSettingsLookAndFeelMenuResult(
int selectedId)
333 m_settingsItems[MemaMoSettingsOption::LookAndFeel_FollowHost].second =
a;
334 m_settingsItems[MemaMoSettingsOption::LookAndFeel_Dark].second =
b;
335 m_settingsItems[MemaMoSettingsOption::LookAndFeel_Light].second =
c;
340 case MemaMoSettingsOption::LookAndFeel_FollowHost:
345 case MemaMoSettingsOption::LookAndFeel_Dark:
350 case MemaMoSettingsOption::LookAndFeel_Light:
361void MainComponent::handleSettingsOutputVisuTypeMenuResult(
int selectedId)
364 std::function<
void(
int,
int,
int,
int,
int,
int,
int,
int,
int,
int,
int,
int,
int)>
setSettingsItemsCheckState = [=](
int a,
int b,
int c,
int d,
int e,
int f,
int g,
int h,
int i,
int j,
int k,
int l,
int m) {
383 setSettingsItemsCheckState(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
384 if (m_monitorComponent)
385 m_monitorComponent->setOutputMeteringVisuActive();
388 setSettingsItemsCheckState(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
389 if (m_monitorComponent)
390 m_monitorComponent->setOutputFieldVisuActive(juce::AudioChannelSet::createLRS());
393 setSettingsItemsCheckState(0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
394 if (m_monitorComponent)
395 m_monitorComponent->setOutputFieldVisuActive(juce::AudioChannelSet::createLCRS());
398 setSettingsItemsCheckState(0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0);
399 if (m_monitorComponent)
400 m_monitorComponent->setOutputFieldVisuActive(juce::AudioChannelSet::create5point0());
403 setSettingsItemsCheckState(0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0);
404 if (m_monitorComponent)
405 m_monitorComponent->setOutputFieldVisuActive(juce::AudioChannelSet::create5point1());
408 setSettingsItemsCheckState(0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0);
409 if (m_monitorComponent)
410 m_monitorComponent->setOutputFieldVisuActive(juce::AudioChannelSet::create5point1point2());
413 setSettingsItemsCheckState(0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0);
414 if (m_monitorComponent)
415 m_monitorComponent->setOutputFieldVisuActive(juce::AudioChannelSet::create7point0());
418 setSettingsItemsCheckState(0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0);
419 if (m_monitorComponent)
420 m_monitorComponent->setOutputFieldVisuActive(juce::AudioChannelSet::create7point1());
423 setSettingsItemsCheckState(0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0);
424 if (m_monitorComponent)
425 m_monitorComponent->setOutputFieldVisuActive(juce::AudioChannelSet::create7point1point4());
428 setSettingsItemsCheckState(0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0);
429 if (m_monitorComponent)
430 m_monitorComponent->setOutputFieldVisuActive(juce::AudioChannelSet::create9point1point6());
433 setSettingsItemsCheckState(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0);
434 if (m_monitorComponent)
435 m_monitorComponent->setOutputFieldVisuActive(juce::AudioChannelSet::quadraphonic());
438 setSettingsItemsCheckState(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0);
439 if (m_monitorComponent)
440 m_monitorComponent->setWaveformVisuActive();
443 setSettingsItemsCheckState(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
444 if (m_monitorComponent)
445 m_monitorComponent->setSpectrumVisuActive();
455void MainComponent::handleSettingsMeteringColourMenuResult(
int selectedId)
470 setMeteringColour(juce::Colours::forestgreen);
474 setMeteringColour(juce::Colours::orangered);
478 setMeteringColour(juce::Colours::dodgerblue);
482 setMeteringColour(juce::Colours::deeppink);
486 setMeteringColour(juce::Colour(0xd1, 0xff, 0x4f));
493void MainComponent::handleSettingsFullscreenModeToggleResult()
495 toggleFullscreenMode();
498void MainComponent::toggleFullscreenMode()
505void MainComponent::setMeteringColour(
const juce::Colour& meteringColour)
509 applyMeteringColour();
511 if (m_connectingComponent)
512 m_connectingComponent->lookAndFeelChanged();
515void MainComponent::applyMeteringColour()
522 case JUCEAppBasics::CustomLookAndFeel::PS_Light:
523 getLookAndFeel().setColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringPeakColourId, m_meteringColour.brighter());
524 getLookAndFeel().setColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringRmsColourId, m_meteringColour);
526 case JUCEAppBasics::CustomLookAndFeel::PS_Dark:
528 getLookAndFeel().setColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringPeakColourId, m_meteringColour.darker());
529 getLookAndFeel().setColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringRmsColourId, m_meteringColour);
535std::optional<int> MainComponent::getNumVisibleChannels()
537 if (!m_monitorComponent)
539 else if (-1 == m_monitorComponent->getNumVisibleChannels())
542 return m_monitorComponent->getNumVisibleChannels();
545void MainComponent::setStatus(
const Status& s)
548 juce::MessageManager::callAsync([
safeThis = juce::Component::SafePointer<MainComponent>(
this)]() {
556 return m_currentStatus;
559void MainComponent::connectToMema()
561 if (m_connectingComponent)
562 m_connectingComponent->setMasterServiceDescription(m_selectedService.description);
563 if (m_discoverComponent)
564 m_discoverComponent->setMasterServiceDescription(m_selectedService.description);
566 setStatus(Status::Connecting);
576 if (Status::Connecting == getStatus())
578 auto sl = m_discoverComponent->getAvailableServices();
579 auto const&
iter = std::find_if(
sl.begin(),
sl.end(), [=](
const auto&
service) { return service.description == m_selectedService.description; });
582 if ((m_selectedService.address !=
iter->address && m_selectedService.port !=
iter->port && m_selectedService.description !=
iter->description) || !m_networkConnection->isConnected())
584 m_selectedService = *
iter;
585 if (m_networkConnection)
586 m_networkConnection->ConnectToSocket(m_selectedService.address.toString(), m_selectedService.port);
588 else if (m_networkConnection && !m_networkConnection->isConnected())
589 m_networkConnection->RetryConnectToSocket();
599 if (
key == juce::KeyPress(juce::KeyPress::F11Key, juce::ModifierKeys::ctrlModifier, 0) ||
600 key == juce::KeyPress(
'f', juce::ModifierKeys::commandModifier | juce::ModifierKeys::ctrlModifier, 0))
602 toggleFullscreenMode();
625 for (
int i = MemaMoSettingsOption::LookAndFeel_First;
i <= MemaMoSettingsOption::LookAndFeel_Last;
i++)
627 if (m_settingsItems[
i].
second == 1)
635 if (m_settingsItems[
i].
second == 1)
638 if (m_monitorComponent && m_monitorComponent->getNumVisibleChannels().has_value())
645 if (m_settingsItems[
i].
second == 1)
665 if (m_networkConnection)
666 m_networkConnection->disconnect();
668 m_selectedService = {};
708 return juce::Desktop::getInstance().getKioskModeComponent() !=
nullptr;
712 return peer->isFullScreen();
void paint(juce::Graphics &g) override
Paints the background and any status overlay.
void performConfigurationDump() override
Serialises the current configuration to the XML file on disk.
std::function< void(int, bool)> onPaletteStyleChange
Called when the user changes the look-and-feel or metering colour.
void resized() override
Lays out the active child component to fill the window.
MemaMoSettingsOption
Identifiers for all user-configurable settings exposed via the settings popup menu.
@ OutputVisuType_LRS
3-channel Left/Right/Surround 2-D field.
@ OutputVisuType_5point0
5.0 surround 2-D field.
@ OutputVisuType_LCRS
4-channel LCRS 2-D field.
@ OutputVisuType_Waveform
Scrolling waveform plot.
@ OutputVisuType_Spectrum
FFT frequency-spectrum display.
@ OutputVisuType_Meterbridge
Level-bar meterbridge (default).
@ OutputVisuType_7point1
7.1 surround 2-D field.
@ OutputVisuType_Quadrophonic
Classic 4-channel quadrophonic 2-D field.
@ OutputVisuType_7point0
7.0 surround 2-D field.
@ MeteringColour_Pink
Pink metering bars.
@ MeteringColour_Blue
Blue metering bars.
@ MeteringColour_Green
Green metering bars (default).
@ OutputVisuType_5point1point2
5.1.2 with two height channels.
@ OutputVisuType_9point1point6
9.1.6 ATMOS full-3D layout.
@ MeteringColour_Red
Red metering bars.
@ MeteringColour_Laser
High-visibility laser-green metering bars.
@ OutputVisuType_7point1point4
7.1.4 Dolby Atmos layout.
@ OutputVisuType_5point1
5.1 surround 2-D field.
Status
Connection/application phase driven by the TCP session lifecycle.
void lookAndFeelChanged() override
Propagates a look-and-feel change to all owned child components.
bool keyPressed(const juce::KeyPress &key) override
Handles keyboard shortcuts (e.g. Escape to disconnect).
void applySettingsOption(const MemaMoSettingsOption &option)
Applies a settings menu selection, updating look-and-feel, visualisation type, or colour.
void onConfigUpdated() override
Reacts to external configuration changes.
bool isFullscreenEnabled()
Returns whether the window is currently displayed in fullscreen mode.
void timerCallback() override
Periodic callback used to retry TCP connections and poll network status.
std::function< void(bool)> onSetFullscreenWindow
Called to request a fullscreen/windowed transition from the application shell.
~MainComponent() override
@ METERINGCOLOUR
User-selected metering bar colour.
@ OUTPUTVISUTYPE
Active output visualisation mode (meterbridge, 2-D field, waveform, …).
@ VISUCONFIG
Root element for visualisation settings.
@ LOOKANDFEEL
Active look-and-feel (follow host / dark / light).
@ SERVICEDESCRIPTION
Stores the multicast service descriptor of the last connected Mema instance.
@ CONNECTIONCONFIG
Root element for connection settings (host, port).
static juce::String getAttributeName(AttributeID ID)
@ COUNT
Integer storing a channel or item count.
static juce::String getTagName(TagID ID)
Carries the active look-and-feel palette style from Mema to connected clients.
static void freeMessageData(SerializableMessage *message)
Type-correctly destroys a SerializableMessage* returned by initFromMemoryBlock().
static SerializableMessage * initFromMemoryBlock(const juce::MemoryBlock &blob)
Deserialises a raw TCP frame into the correct concrete SerializableMessage subclass.
@ AnalyzerParameters
Audio device sample rate and block size; lets clients initialise their local ProcessorDataAnalyzer.
@ ReinitIOCount
New input/output channel count; clients must rebuild their UI accordingly.
@ AudioInputBuffer
Raw PCM input buffer streamed from Mema to subscribed clients.
@ AudioOutputBuffer
Raw PCM output buffer streamed from Mema to subscribed clients.
@ EnvironmentParameters
Look-and-feel palette sent by Mema to clients on connect.
static juce::String getMonitorServiceTypeUID()
Returns the UID for the Mema.Mo monitor service.
static juce::String getServiceTypeUIDBase()
Returns the base string for building service type UIDs.