Mema
Memory Matrix — multi-channel audio matrix monitor and router
Loading...
Searching...
No Matches
HeadlessCLIMenu.cpp
Go to the documentation of this file.
1/* Copyright (c) 2026, Christian Ahrens
2 *
3 * This file is part of Mema <https://github.com/ChristianAhrens/Mema>
4 *
5 * This tool is free software; you can redistribute it and/or modify it under
6 * the terms of the GNU Lesser General Public License version 3.0 as published
7 * by the Free Software Foundation.
8 *
9 * This tool is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
12 * details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this tool; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 */
18
19#include "HeadlessCLIMenu.h"
20
23
24#include <AppConfigurationBase.h>
25
26#include <iostream>
27#include <iomanip>
28
29namespace Mema
30{
31
33static constexpr int sc_separatorWidth = 56;
34
39static constexpr int sc_matrixDisplayMax = 16;
40
41//==============================================================================
43 : juce::Thread("HeadlessCLIMenu"), m_processor(processor)
44{
45}
46
48{
49 // Allow up to 3 s for the thread to exit cleanly from its current stdin
50 // read. If the thread is still blocked after the timeout, juce::Thread
51 // will force-terminate it.
52 stopThread(3000);
53}
54
55//==============================================================================
57{
58 // Give the processor and audio device a moment to finish initialising
59 // before the first channel-count query.
60 Thread::sleep(800);
61 if (threadShouldExit())
62 return;
63
64 runMainMenu();
65}
66
67//==============================================================================
68// I/O helpers
69//==============================================================================
70
71juce::String HeadlessCLIMenu::readLine()
72{
73 std::string line;
74 if (!std::getline(std::cin, line))
75 {
76 // EOF — stdin was closed (e.g. the script that launched Mema finished,
77 // or the user closed the terminal). Signal the menu loops to exit.
78 m_quit = true;
79 return {};
80 }
81 return juce::String(line).trim();
82}
83
84void HeadlessCLIMenu::callOnMessageThread(std::function<void()> fn)
85{
86 // This helper must only be called from the CLI background thread.
87 jassert(!juce::MessageManager::getInstance()->isThisTheMessageThread());
88
89 juce::WaitableEvent done;
90 juce::MessageManager::callAsync([fn = std::move(fn), &done]()
91 {
92 fn();
93 done.signal();
94 });
95
96 // 5-second safety timeout. If the message loop becomes unresponsive the
97 // CLI thread will not block indefinitely; the change may simply not apply.
98 done.wait(5000);
99}
100
101//==============================================================================
102// Print helpers
103//==============================================================================
104
105void HeadlessCLIMenu::printSeparator()
106{
107 std::cout << juce::String::repeatedString("-", sc_separatorWidth) << "\n";
108}
109
110void HeadlessCLIMenu::printHeader(const juce::String& title)
111{
112 std::cout << "\n";
113 printSeparator();
114 std::cout << " Mema - " << title << "\n";
115 printSeparator();
116 std::cout << "\n";
117}
118
119void HeadlessCLIMenu::printPrompt()
120{
121 std::cout << "\n> " << std::flush;
122}
123
124//==============================================================================
125// Channel count helpers
126//==============================================================================
127
128int HeadlessCLIMenu::getActiveInputCount()
129{
130 auto* dm = m_processor.getDeviceManager();
131 auto* dev = dm ? dm->getCurrentAudioDevice() : nullptr;
132 return dev ? dev->getActiveInputChannels().countNumberOfSetBits() : 0;
133}
134
135int HeadlessCLIMenu::getActiveOutputCount()
136{
137 auto* dm = m_processor.getDeviceManager();
138 auto* dev = dm ? dm->getCurrentAudioDevice() : nullptr;
139 return dev ? dev->getActiveOutputChannels().countNumberOfSetBits() : 0;
140}
141
142//==============================================================================
143// Main menu
144//==============================================================================
145
146void HeadlessCLIMenu::runMainMenu()
147{
148 while (!threadShouldExit() && !m_quit)
149 {
150 // Collect live state for the summary lines shown next to each option.
151 auto* dm = m_processor.getDeviceManager();
152 auto* dev = dm ? dm->getCurrentAudioDevice() : nullptr;
153
154 int numIn = getActiveInputCount();
155 int numOut = getActiveOutputCount();
156
157 // Build a compact device description string.
158 juce::String deviceLine = "none";
159 if (dev != nullptr)
160 {
161 auto setup = dm->getAudioDeviceSetup();
162 deviceLine = dev->getName()
163 + " " + juce::String(static_cast<int>(setup.sampleRate)) + " Hz"
164 + " buf " + juce::String(setup.bufferSize);
165 }
166
167 // Count muted channels so the summary line is informative at a glance.
168 int mutedIn = 0;
169 for (int ch = 1; ch <= numIn; ++ch)
170 if (m_processor.getInputMuteState(static_cast<std::uint16_t>(ch)))
171 ++mutedIn;
172
173 int mutedOut = 0;
174 for (int ch = 1; ch <= numOut; ++ch)
175 if (m_processor.getOutputMuteState(static_cast<std::uint16_t>(ch)))
176 ++mutedOut;
177
178 // Build a compact plugin description string.
179 auto pluginDesc = m_processor.getPluginDescription();
180 juce::String pluginLine;
181 if (pluginDesc.name.isNotEmpty())
182 pluginLine = pluginDesc.name
183 + " " + (m_processor.isPluginEnabled() ? "enabled" : "disabled")
184 + " " + (m_processor.isPluginPost() ? "post" : "pre") + "-matrix";
185 else
186 pluginLine = "none";
187
188 printHeader("Configuration");
189 std::cout << " 1 Input mutes [" << numIn << " ch, " << mutedIn << " muted]\n";
190 std::cout << " 2 Output mutes [" << numOut << " ch, " << mutedOut << " muted]\n";
191 std::cout << " 3 Matrix gains [" << numIn << " x " << numOut << "]\n";
192 std::cout << " 4 Audio device [" << deviceLine << "]\n";
193 std::cout << " 5 Plugin [" << pluginLine << "]\n";
194 std::cout << " 6 Load config\n";
195 std::cout << " 7 Save config\n";
196 std::cout << " q Quit\n";
197
198 printPrompt();
199 auto input = readLine().toLowerCase();
200 if (m_quit) break;
201
202 if (input == "1") runInputMutesMenu();
203 else if (input == "2") runOutputMutesMenu();
204 else if (input == "3") runMatrixMenu();
205 else if (input == "4") runAudioDeviceMenu();
206 else if (input == "5") runPluginMenu();
207 else if (input == "6") doLoadConfig();
208 else if (input == "7") doSaveConfig();
209 else if (input == "q")
210 {
211 m_quit = true;
212 std::cout << "\nQuitting Mema...\n";
213 // Request shutdown on the message thread.
214 juce::MessageManager::callAsync([]()
215 {
216 juce::JUCEApplication::getInstance()->systemRequestedQuit();
217 });
218 }
219 else
220 std::cout << " Unknown option '" << input << "'\n";
221 }
222}
223
224//==============================================================================
225// Input mutes menu
226//==============================================================================
227
228void HeadlessCLIMenu::runInputMutesMenu()
229{
230 while (!threadShouldExit() && !m_quit)
231 {
232 int numIn = getActiveInputCount();
233
234 printHeader("Input Mutes");
235
236 if (numIn == 0)
237 {
238 std::cout << " No active input channels.\n";
239 std::cout << "\n b Back\n";
240 printPrompt();
241 auto in = readLine().toLowerCase();
242 if (m_quit || in == "b" || in == "q") break;
243 continue;
244 }
245
246 for (int ch = 1; ch <= numIn; ++ch)
247 {
248 bool muted = m_processor.getInputMuteState(static_cast<std::uint16_t>(ch));
249 std::cout << " " << std::setw(2) << ch
250 << " Input " << std::setw(2) << ch
251 << " [" << (muted ? "MUTED " : "active") << "]\n";
252 }
253 std::cout << "\n b Back\n";
254 std::cout << "\n Enter channel number to toggle, or 'b' to go back.\n";
255
256 printPrompt();
257 auto input = readLine().toLowerCase();
258 if (m_quit) break;
259 if (input == "b" || input == "q") break;
260
261 int ch = input.getIntValue();
262 if (ch >= 1 && ch <= numIn)
263 {
264 bool currentlyMuted = m_processor.getInputMuteState(static_cast<std::uint16_t>(ch));
265 callOnMessageThread([this, ch, currentlyMuted]()
266 {
267 m_processor.setInputMuteState(static_cast<std::uint16_t>(ch), !currentlyMuted);
268 });
269 std::cout << " Input " << ch << (currentlyMuted ? " unmuted.\n" : " muted.\n");
270 }
271 else
272 std::cout << " Invalid channel number.\n";
273 }
274}
275
276//==============================================================================
277// Output mutes menu
278//==============================================================================
279
280void HeadlessCLIMenu::runOutputMutesMenu()
281{
282 while (!threadShouldExit() && !m_quit)
283 {
284 int numOut = getActiveOutputCount();
285
286 printHeader("Output Mutes");
287
288 if (numOut == 0)
289 {
290 std::cout << " No active output channels.\n";
291 std::cout << "\n b Back\n";
292 printPrompt();
293 auto in = readLine().toLowerCase();
294 if (m_quit || in == "b" || in == "q") break;
295 continue;
296 }
297
298 for (int ch = 1; ch <= numOut; ++ch)
299 {
300 bool muted = m_processor.getOutputMuteState(static_cast<std::uint16_t>(ch));
301 std::cout << " " << std::setw(2) << ch
302 << " Output " << std::setw(2) << ch
303 << " [" << (muted ? "MUTED " : "active") << "]\n";
304 }
305 std::cout << "\n b Back\n";
306 std::cout << "\n Enter channel number to toggle, or 'b' to go back.\n";
307
308 printPrompt();
309 auto input = readLine().toLowerCase();
310 if (m_quit) break;
311 if (input == "b" || input == "q") break;
312
313 int ch = input.getIntValue();
314 if (ch >= 1 && ch <= numOut)
315 {
316 bool currentlyMuted = m_processor.getOutputMuteState(static_cast<std::uint16_t>(ch));
317 callOnMessageThread([this, ch, currentlyMuted]()
318 {
319 m_processor.setOutputMuteState(static_cast<std::uint16_t>(ch), !currentlyMuted);
320 });
321 std::cout << " Output " << ch << (currentlyMuted ? " unmuted.\n" : " muted.\n");
322 }
323 else
324 std::cout << " Invalid channel number.\n";
325 }
326}
327
328//==============================================================================
329// Matrix menu
330//==============================================================================
331
332void HeadlessCLIMenu::runMatrixMenu()
333{
334 while (!threadShouldExit() && !m_quit)
335 {
336 int numIn = getActiveInputCount();
337 int numOut = getActiveOutputCount();
338
339 int dispIn = juce::jmin(numIn, sc_matrixDisplayMax);
340 int dispOut = juce::jmin(numOut, sc_matrixDisplayMax);
341
342 printHeader("Matrix Gains");
343
344 if (numIn == 0 || numOut == 0)
345 {
346 std::cout << " No active channels.\n";
347 std::cout << "\n b Back\n";
348 printPrompt();
349 auto in = readLine().toLowerCase();
350 if (m_quit || in == "b" || in == "q") break;
351 continue;
352 }
353
354 // --- header row (output numbers) ---
355 std::cout << " Outs:";
356 for (int o = 1; o <= dispOut; ++o)
357 std::cout << std::setw(4) << o;
358 if (numOut > sc_matrixDisplayMax)
359 std::cout << " (+" << (numOut - sc_matrixDisplayMax) << " more)";
360 std::cout << "\n";
361
362 // --- one row per input ---
363 for (int i = 1; i <= dispIn; ++i)
364 {
365 std::cout << " In" << std::setw(2) << i << ":";
366 for (int o = 1; o <= dispOut; ++o)
367 {
368 bool en = m_processor.getMatrixCrosspointEnabledValue(
369 static_cast<std::uint16_t>(i), static_cast<std::uint16_t>(o));
370 std::cout << std::setw(4) << (en ? "+" : ".");
371 }
372 std::cout << "\n";
373 }
374 if (numIn > sc_matrixDisplayMax)
375 std::cout << " (first " << sc_matrixDisplayMax << " inputs shown)\n";
376
377 std::cout << "\n + = crosspoint enabled . = disabled\n";
378 std::cout << "\n Enter 'in out' (e.g. '2 3') to edit a crosspoint, or 'b' to go back.\n";
379
380 printPrompt();
381 auto input = readLine();
382 if (m_quit) break;
383 if (input.toLowerCase() == "b" || input.toLowerCase() == "q") break;
384
385 // Parse "in out" token pair.
386 juce::StringArray tokens;
387 tokens.addTokens(input, " \t", "");
388 tokens.removeEmptyStrings();
389
390 if (tokens.size() != 2)
391 {
392 std::cout << " Please enter two numbers separated by a space.\n";
393 continue;
394 }
395
396 int selIn = tokens[0].getIntValue();
397 int selOut = tokens[1].getIntValue();
398
399 if (selIn < 1 || selIn > numIn || selOut < 1 || selOut > numOut)
400 {
401 std::cout << " Out of range. In: 1-" << numIn << " Out: 1-" << numOut << "\n";
402 continue;
403 }
404
405 // --- per-crosspoint sub-edit loop ---
406 while (!threadShouldExit() && !m_quit)
407 {
408 bool en = m_processor.getMatrixCrosspointEnabledValue(
409 static_cast<std::uint16_t>(selIn),
410 static_cast<std::uint16_t>(selOut));
411 float factor = m_processor.getMatrixCrosspointFactorValue(
412 static_cast<std::uint16_t>(selIn),
413 static_cast<std::uint16_t>(selOut));
414 float dB = (factor > 0.0f)
415 ? juce::Decibels::gainToDecibels(factor)
416 : -100.0f;
417
418 printHeader("Crosspoint In " + juce::String(selIn)
419 + " -> Out " + juce::String(selOut));
420
421 std::cout << " 1 Toggle enable [" << (en ? "enabled " : "disabled") << "]\n";
422 std::cout << " 2 Set gain [" << juce::String(dB, 1) << " dB]\n";
423 std::cout << "\n b Back\n";
424
425 printPrompt();
426 auto cpInput = readLine().toLowerCase();
427 if (m_quit) break;
428 if (cpInput == "b" || cpInput == "q") break;
429
430 if (cpInput == "1")
431 {
432 callOnMessageThread([this, selIn, selOut, en]()
433 {
435 static_cast<std::uint16_t>(selIn),
436 static_cast<std::uint16_t>(selOut),
437 !en);
438 });
439 std::cout << " Crosspoint " << (en ? "disabled.\n" : "enabled.\n");
440 }
441 else if (cpInput == "2")
442 {
443 std::cout << " Enter gain in dB (e.g. -6.0, 0.0): " << std::flush;
444 auto dbInput = readLine();
445 if (m_quit) break;
446
447 float newDb = dbInput.getFloatValue();
448 float newFactor = juce::Decibels::decibelsToGain(newDb, -100.0f);
449
450 callOnMessageThread([this, selIn, selOut, newFactor]()
451 {
453 static_cast<std::uint16_t>(selIn),
454 static_cast<std::uint16_t>(selOut),
455 newFactor);
456 });
457 std::cout << " Gain set to " << juce::String(newDb, 1) << " dB.\n";
458 }
459 else
460 std::cout << " Unknown option.\n";
461 }
462 }
463}
464
465//==============================================================================
466// Audio device menu
467//==============================================================================
468
472{
473 juce::String typeName;
474 juce::String deviceName;
475};
476
477static std::vector<DeviceEntry> collectDevices(juce::AudioDeviceManager* dm, bool wantInputs)
478{
479 std::vector<DeviceEntry> result;
480 for (auto* type : dm->getAvailableDeviceTypes())
481 {
482 type->scanForDevices();
483 for (auto& name : type->getDeviceNames(wantInputs))
484 result.push_back({ type->getTypeName(), name });
485 }
486 return result;
487}
488
489void HeadlessCLIMenu::runAudioDeviceMenu()
490{
491 while (!threadShouldExit() && !m_quit)
492 {
493 auto* dm = m_processor.getDeviceManager();
494 if (dm == nullptr)
495 {
496 std::cout << " Audio device manager not available.\n";
497 break;
498 }
499
500 auto setup = dm->getAudioDeviceSetup();
501 auto* dev = dm->getCurrentAudioDevice();
502 juce::String inputDeviceName = setup.inputDeviceName.isNotEmpty()
503 ? setup.inputDeviceName : "(none)";
504 juce::String outputDeviceName = setup.outputDeviceName.isNotEmpty()
505 ? setup.outputDeviceName : "(none)";
506 juce::String typeName = dev ? dev->getTypeName() : "(none)";
507 int sampleRate = dev ? static_cast<int>(setup.sampleRate) : 0;
508 int bufferSize = dev ? setup.bufferSize : 0;
509 int numIn = getActiveInputCount();
510 int numOut = getActiveOutputCount();
511
512 printHeader("Audio Device");
513 std::cout << " Input device: " << inputDeviceName << "\n";
514 std::cout << " Output device: " << outputDeviceName << "\n";
515 std::cout << " Device type: " << typeName << "\n";
516 std::cout << " Sample rate: " << sampleRate << " Hz\n";
517 std::cout << " Buffer size: " << bufferSize << " samples\n";
518 std::cout << " Active inputs: " << numIn << "\n";
519 std::cout << " Active outputs: " << numOut << "\n";
520 std::cout << "\n";
521 std::cout << " 1 Change input device\n";
522 std::cout << " 2 Change output device\n";
523 std::cout << " 3 Change sample rate\n";
524 std::cout << " 4 Change buffer size\n";
525 std::cout << "\n b Back\n";
526
527 printPrompt();
528 auto input = readLine().toLowerCase();
529 if (m_quit) break;
530 if (input == "b" || input == "q") break;
531
532 // ---- Change input device ------------------------------------------
533 if (input == "1")
534 {
535 auto devices = collectDevices(dm, true /*wantInputs*/);
536 if (devices.empty()) { std::cout << " No input devices found.\n"; continue; }
537
538 printHeader("Select Input Device");
539 for (int i = 0; i < static_cast<int>(devices.size()); ++i)
540 std::cout << " " << std::setw(2) << (i + 1)
541 << " [" << devices[i].typeName << "] "
542 << devices[i].deviceName
543 << (devices[i].deviceName == inputDeviceName ? " <-- current" : "") << "\n";
544 std::cout << "\n b Back\n";
545
546 printPrompt();
547 auto sel = readLine().toLowerCase();
548 if (m_quit) break;
549 if (sel == "b") continue;
550
551 int idx = sel.getIntValue() - 1;
552 if (idx < 0 || idx >= static_cast<int>(devices.size()))
553 {
554 std::cout << " Invalid selection.\n";
555 continue;
556 }
557
558 juce::String selectedType = devices[idx].typeName;
559 juce::String selectedDevice = devices[idx].deviceName;
560
561 callOnMessageThread([dm, selectedType, selectedDevice]()
562 {
563 dm->setCurrentAudioDeviceType(selectedType, true);
564 auto s = dm->getAudioDeviceSetup();
565 s.inputDeviceName = selectedDevice;
566 s.useDefaultInputChannels = true;
567 dm->setAudioDeviceSetup(s, true);
568 });
569 std::cout << " Input device changed to: " << selectedDevice << "\n";
570 }
571
572 // ---- Change output device -----------------------------------------
573 else if (input == "2")
574 {
575 auto devices = collectDevices(dm, false /*wantInputs*/);
576 if (devices.empty()) { std::cout << " No output devices found.\n"; continue; }
577
578 printHeader("Select Output Device");
579 for (int i = 0; i < static_cast<int>(devices.size()); ++i)
580 std::cout << " " << std::setw(2) << (i + 1)
581 << " [" << devices[i].typeName << "] "
582 << devices[i].deviceName
583 << (devices[i].deviceName == outputDeviceName ? " <-- current" : "") << "\n";
584 std::cout << "\n b Back\n";
585
586 printPrompt();
587 auto sel = readLine().toLowerCase();
588 if (m_quit) break;
589 if (sel == "b") continue;
590
591 int idx = sel.getIntValue() - 1;
592 if (idx < 0 || idx >= static_cast<int>(devices.size()))
593 {
594 std::cout << " Invalid selection.\n";
595 continue;
596 }
597
598 juce::String selectedType = devices[idx].typeName;
599 juce::String selectedDevice = devices[idx].deviceName;
600
601 callOnMessageThread([dm, selectedType, selectedDevice]()
602 {
603 dm->setCurrentAudioDeviceType(selectedType, true);
604 auto s = dm->getAudioDeviceSetup();
605 s.outputDeviceName = selectedDevice;
606 s.useDefaultOutputChannels = true;
607 dm->setAudioDeviceSetup(s, true);
608 });
609 std::cout << " Output device changed to: " << selectedDevice << "\n";
610 }
611
612 // ---- Change sample rate --------------------------------------------
613 else if (input == "3")
614 {
615 if (dev == nullptr) { std::cout << " No active device.\n"; continue; }
616
617 auto rates = dev->getAvailableSampleRates();
618 if (rates.isEmpty()) { std::cout << " No sample rates reported by device.\n"; continue; }
619
620 printHeader("Select Sample Rate");
621 for (int i = 0; i < rates.size(); ++i)
622 std::cout << " " << std::setw(2) << (i + 1)
623 << " " << static_cast<int>(rates[i]) << " Hz"
624 << (static_cast<int>(rates[i]) == sampleRate ? " <-- current" : "") << "\n";
625 std::cout << "\n b Back\n";
626
627 printPrompt();
628 auto sel = readLine().toLowerCase();
629 if (m_quit) break;
630 if (sel == "b") continue;
631
632 int idx = sel.getIntValue() - 1;
633 if (idx < 0 || idx >= rates.size())
634 {
635 std::cout << " Invalid selection.\n";
636 continue;
637 }
638
639 double newRate = rates[idx];
640 callOnMessageThread([dm, newRate]()
641 {
642 auto s = dm->getAudioDeviceSetup();
643 s.sampleRate = newRate;
644 dm->setAudioDeviceSetup(s, true);
645 });
646 std::cout << " Sample rate changed to " << static_cast<int>(newRate) << " Hz.\n";
647 }
648
649 // ---- Change buffer size --------------------------------------------
650 else if (input == "4")
651 {
652 if (dev == nullptr) { std::cout << " No active device.\n"; continue; }
653
654 auto sizes = dev->getAvailableBufferSizes();
655 if (sizes.isEmpty()) { std::cout << " No buffer sizes reported by device.\n"; continue; }
656
657 printHeader("Select Buffer Size");
658 for (int i = 0; i < sizes.size(); ++i)
659 std::cout << " " << std::setw(2) << (i + 1)
660 << " " << sizes[i] << " samples"
661 << (sizes[i] == bufferSize ? " <-- current" : "") << "\n";
662 std::cout << "\n b Back\n";
663
664 printPrompt();
665 auto sel = readLine().toLowerCase();
666 if (m_quit) break;
667 if (sel == "b") continue;
668
669 int idx = sel.getIntValue() - 1;
670 if (idx < 0 || idx >= sizes.size())
671 {
672 std::cout << " Invalid selection.\n";
673 continue;
674 }
675
676 int newSize = sizes[idx];
677 callOnMessageThread([dm, newSize]()
678 {
679 auto s = dm->getAudioDeviceSetup();
680 s.bufferSize = newSize;
681 dm->setAudioDeviceSetup(s, true);
682 });
683 std::cout << " Buffer size changed to " << newSize << " samples.\n";
684 }
685 else
686 std::cout << " Unknown option.\n";
687 }
688}
689
690//==============================================================================
691// Plugin menu
692//==============================================================================
693
694void HeadlessCLIMenu::runPluginMenu()
695{
696 while (!threadShouldExit() && !m_quit)
697 {
698 auto desc = m_processor.getPluginDescription();
699 bool loaded = desc.name.isNotEmpty();
700
701 printHeader("Plugin");
702 std::cout << " Plugin: " << (loaded ? desc.name : juce::String("none")) << "\n";
703
704 if (loaded)
705 {
706 bool enabled = m_processor.isPluginEnabled();
707 bool post = m_processor.isPluginPost();
708
709 std::cout << "\n";
710 std::cout << " 1 Toggle processing [" << (enabled ? "enabled " : "disabled") << "]\n";
711 std::cout << " 2 Toggle pre/post [" << (post ? "post" : "pre ") << "-matrix]\n";
712 std::cout << " 3 Parameter remote control\n";
713 }
714 else
715 {
716 std::cout << "\n No plugin loaded. Load a configuration file that includes a plugin.\n";
717 }
718
719 std::cout << "\n b Back\n";
720
721 printPrompt();
722 auto input = readLine().toLowerCase();
723 if (m_quit) break;
724 if (input == "b" || input == "q") break;
725
726 if (!loaded)
727 continue;
728
729 bool enabled = m_processor.isPluginEnabled();
730 bool post = m_processor.isPluginPost();
731
732 if (input == "1")
733 {
734 callOnMessageThread([this, enabled]()
735 {
736 m_processor.setPluginEnabledState(!enabled);
737 });
738 std::cout << " Plugin processing " << (enabled ? "disabled.\n" : "enabled.\n");
739 }
740 else if (input == "2")
741 {
742 callOnMessageThread([this, post]()
743 {
744 m_processor.setPluginPrePostState(!post);
745 });
746 std::cout << " Plugin insertion set to " << (post ? "pre-matrix.\n" : "post-matrix.\n");
747 }
748 else if (input == "3")
749 {
750 runPluginParametersMenu();
751 }
752 else
753 std::cout << " Unknown option.\n";
754 }
755}
756
757void HeadlessCLIMenu::runPluginParametersMenu()
758{
759 while (!threadShouldExit() && !m_quit)
760 {
761 auto& params = m_processor.getPluginParameterInfos();
762
763 printHeader("Plugin Parameters - Remote Control");
764
765 if (params.empty())
766 {
767 std::cout << " No parameters available.\n";
768 std::cout << "\n b Back\n";
769 printPrompt();
770 auto in = readLine().toLowerCase();
771 if (m_quit || in == "b" || in == "q") break;
772 continue;
773 }
774
775 for (int i = 0; i < static_cast<int>(params.size()); ++i)
776 {
777 const auto& p = params[i];
778 juce::String typeStr;
779 switch (p.type)
780 {
781 case ParameterControlType::Toggle: typeStr = "Toggle "; break;
782 case ParameterControlType::Discrete: typeStr = "Discrete "; break;
783 case ParameterControlType::Continuous: typeStr = "Continuous"; break;
784 default: typeStr = "Continuous"; break;
785 }
786 std::cout << " " << std::setw(3) << (i + 1)
787 << " " << p.name.paddedRight(' ', 24)
788 << " [" << (p.isRemoteControllable ? "remote: yes" : "remote: no ") << "]"
789 << " " << typeStr << "\n";
790 }
791
792 std::cout << "\n b Back\n";
793 std::cout << "\n Enter parameter number to configure, or 'b' to go back.\n";
794
795 printPrompt();
796 auto input = readLine().toLowerCase();
797 if (m_quit) break;
798 if (input == "b" || input == "q") break;
799
800 int idx = input.getIntValue() - 1;
801 if (idx < 0 || idx >= static_cast<int>(params.size()))
802 {
803 std::cout << " Invalid parameter number.\n";
804 continue;
805 }
806
807 // Per-parameter sub-edit loop.
808 while (!threadShouldExit() && !m_quit)
809 {
810 auto& params2 = m_processor.getPluginParameterInfos();
811 if (idx >= static_cast<int>(params2.size())) break;
812 const auto& p = params2[idx];
813
814 juce::String typeStr;
815 switch (p.type)
816 {
817 case ParameterControlType::Toggle: typeStr = "Toggle"; break;
818 case ParameterControlType::Discrete: typeStr = "Discrete"; break;
819 case ParameterControlType::Continuous: typeStr = "Continuous"; break;
820 default: typeStr = "Continuous"; break;
821 }
822
823 printHeader("Parameter: " + p.name);
824 std::cout << " Name: " << p.name << "\n";
825 std::cout << " Index: " << p.index << "\n";
826 std::cout << " Value: " << juce::String(p.currentValue, 3)
827 << (p.label.isNotEmpty() ? " " + p.label : juce::String()) << "\n";
828 std::cout << "\n";
829 std::cout << " 1 Toggle remote-controllable [" << (p.isRemoteControllable ? "yes" : "no ") << "]\n";
830 if (p.isRemoteControllable)
831 std::cout << " 2 Set control type [" << typeStr << "]\n";
832 std::cout << "\n b Back\n";
833
834 printPrompt();
835 auto cpInput = readLine().toLowerCase();
836 if (m_quit) break;
837 if (cpInput == "b" || cpInput == "q") break;
838
839 if (cpInput == "1")
840 {
841 bool nowRemote = p.isRemoteControllable;
842 ParameterControlType curType = p.type;
843 int curSteps = p.stepCount;
844 int capturedIdx = idx;
845 callOnMessageThread([this, capturedIdx, nowRemote, curType, curSteps]()
846 {
847 m_processor.setPluginParameterRemoteControlInfos(capturedIdx, !nowRemote, curType, curSteps);
848 });
849 std::cout << " Remote-controllable " << (nowRemote ? "disabled.\n" : "enabled.\n");
850 }
851 else if (cpInput == "2" && p.isRemoteControllable)
852 {
853 printHeader("Control Type: " + p.name);
854 std::cout << " 1 Continuous (slider / fader)\n";
855 std::cout << " 2 Discrete (combo box, requires step count >= 2)\n";
856 std::cout << " 3 Toggle (on/off button, 2 steps)\n";
857 std::cout << "\n b Back\n";
858
859 printPrompt();
860 auto typeInput = readLine().toLowerCase();
861 if (m_quit) break;
862 if (typeInput == "b") continue;
863
864 ParameterControlType newType = p.type;
865 int newSteps = p.stepCount;
866 bool validChoice = true;
867
868 if (typeInput == "1")
869 {
870 newType = ParameterControlType::Continuous;
871 newSteps = 0;
872 }
873 else if (typeInput == "2")
874 {
875 std::cout << " Enter number of steps (>= 2, current: "
876 << (p.stepCount >= 2 ? p.stepCount : 2) << "): " << std::flush;
877 auto stepsInput = readLine();
878 if (m_quit) break;
879 int enteredSteps = stepsInput.getIntValue();
880 newType = ParameterControlType::Discrete;
881 newSteps = juce::jmax(2, enteredSteps);
882 }
883 else if (typeInput == "3")
884 {
885 newType = ParameterControlType::Toggle;
886 newSteps = 2;
887 }
888 else
889 {
890 std::cout << " Invalid selection.\n";
891 validChoice = false;
892 }
893
894 if (validChoice)
895 {
896 int capturedIdx2 = idx;
897 callOnMessageThread([this, capturedIdx2, newType, newSteps]()
898 {
899 m_processor.setPluginParameterRemoteControlInfos(capturedIdx2, true, newType, newSteps);
900 });
901 std::cout << " Control type updated.\n";
902 }
903 }
904 else
905 std::cout << " Unknown option.\n";
906 }
907 }
908}
909
910//==============================================================================
911// Config load / save
912//==============================================================================
913
914void HeadlessCLIMenu::doLoadConfig()
915{
916 std::cout << "\n Enter path to .config file to load\n"
917 " (leave empty to cancel): " << std::flush;
918 auto path = readLine();
919 if (m_quit || path.isEmpty()) return;
920
921 juce::File file(path);
922 if (!file.existsAsFile())
923 {
924 std::cout << " File not found: " << path << "\n";
925 return;
926 }
927 if (!file.hasReadAccess())
928 {
929 std::cout << " Cannot read file: " << path << "\n";
930 return;
931 }
932
933 auto xmlConfig = juce::parseXML(file);
934 if (!xmlConfig)
935 {
936 std::cout << " File does not contain valid XML.\n";
937 return;
938 }
939 if (!MemaAppConfiguration::isValid(xmlConfig))
940 {
941 std::cout << " File does not contain a valid Mema configuration.\n";
942 return;
943 }
944
945 // std::function requires a copyable callable, but unique_ptr is move-only.
946 // Wrapping in shared_ptr makes the capture copyable while preserving sole ownership.
947 auto sharedXml = std::make_shared<std::unique_ptr<juce::XmlElement>>(std::move(xmlConfig));
948 callOnMessageThread([sharedXml]()
949 {
950 auto* config = MemaAppConfiguration::getInstance();
951 if (config == nullptr) return;
952 config->SetFlushAndUpdateDisabled();
953 config->resetConfigState(std::move(*sharedXml));
954 config->ResetFlushAndUpdateDisabled();
955 });
956
957 std::cout << " Configuration loaded from: " << path << "\n";
958}
959
960void HeadlessCLIMenu::doSaveConfig()
961{
962 juce::String defaultName = juce::Time::getCurrentTime().toISO8601(true).substring(0, 10)
963 + "_Mema.config";
964
965 std::cout << "\n Enter target path for the configuration file\n"
966 " (suggestion: " << defaultName << ", leave empty to cancel): " << std::flush;
967 auto path = readLine();
968 if (m_quit || path.isEmpty()) return;
969
970 juce::File targetFile(path);
971 if (targetFile.getFileExtension() != ".config")
972 targetFile = targetFile.withFileExtension(".config");
973
974 if (!targetFile.hasWriteAccess())
975 {
976 std::cout << " Cannot write to: " << targetFile.getFullPathName() << "\n";
977 return;
978 }
979
980 auto* config = MemaAppConfiguration::getInstance();
981 if (config == nullptr)
982 {
983 std::cout << " Configuration not available.\n";
984 return;
985 }
986
987 auto xmlConfig = config->getConfigState();
988 if (!xmlConfig)
989 {
990 std::cout << " Configuration state could not be read.\n";
991 return;
992 }
993
994 if (!xmlConfig->writeTo(targetFile))
995 std::cout << " Failed to write: " << targetFile.getFullPathName() << "\n";
996 else
997 std::cout << " Configuration saved to: " << targetFile.getFullPathName() << "\n";
998}
999
1000} // namespace Mema
~HeadlessCLIMenu() override
Destructor — stops the background thread (up to 3 s timeout).
HeadlessCLIMenu(MemaProcessor &processor)
Constructs the menu and stores a reference to the processor.
void run() override
Thread entry point — shows the main menu in a loop until the user quits.
Core audio processor — owns the AudioDeviceManager, routing matrix, plugin host, and IPC server.
bool getOutputMuteState(std::uint16_t channelNumber)
Returns the mute state of a specific output channel.
juce::PluginDescription getPluginDescription()
Returns the JUCE description of the currently loaded plugin.
bool getMatrixCrosspointEnabledValue(std::uint16_t inputNumber, std::uint16_t outputNumber)
Returns whether a specific crosspoint node is enabled (routing active).
void setMatrixCrosspointFactorValue(std::uint16_t inputNumber, std::uint16_t outputNumber, float factor, MemaChannelCommander *sender=nullptr, int userId=-1)
Sets the linear gain factor of a crosspoint node.
void setInputMuteState(std::uint16_t channelNumber, bool muted, MemaChannelCommander *sender=nullptr, int userId=-1)
Sets the mute state of an input channel and notifies all commanders except the sender.
AudioDeviceManager * getDeviceManager()
Returns a raw pointer to the JUCE AudioDeviceManager. Used by the audio-setup UI component.
void setOutputMuteState(std::uint16_t channelNumber, bool muted, MemaChannelCommander *sender=nullptr, int userId=-1)
Sets the mute state of an output channel and notifies all commanders except the sender.
bool isPluginPost()
Returns true when the plugin is inserted post-matrix.
bool getInputMuteState(std::uint16_t channelNumber)
Returns the mute state of a specific input channel.
float getMatrixCrosspointFactorValue(std::uint16_t inputNumber, std::uint16_t outputNumber)
Returns the linear gain factor of a crosspoint node.
void setMatrixCrosspointEnabledValue(std::uint16_t inputNumber, std::uint16_t outputNumber, bool enabled, MemaChannelCommander *sender=nullptr, int userId=-1)
Enables or disables a crosspoint routing node.
bool isPluginEnabled()
Returns true when a plugin is loaded and its processing is enabled.
static constexpr int sc_separatorWidth
static constexpr int sc_matrixDisplayMax
static std::vector< DeviceEntry > collectDevices(juce::AudioDeviceManager *dm, bool wantInputs)