Umsci
Upmix Spatial Control Interface — OCA/OCP.1 spatial audio utility
Loading...
Searching...
No Matches
UmsciExternalControlComponent.cpp
Go to the documentation of this file.
1/* Copyright (c) 2026, Christian Ahrens
2 *
3 * This file is part of Umsci <https://github.com/ChristianAhrens/Umsci>
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
20
21
22// ─── Shared static data ───────────────────────────────────────────────────────
23
25 "Rotation",
26 "Translation (scale)",
27 "Height translation",
28 "Angle stretch",
29 "Offset X",
30 "Offset Y"
31};
32
33const std::array<std::pair<float, float>, UmsciExternalControlComponent::UpmixMidiParam_COUNT>
35 { -juce::MathConstants<float>::pi, juce::MathConstants<float>::pi }, // Rotation: −π – +π rad
36 { 0.0f, 3.0f }, // Translation: radial scale factor
37 { 0.0f, 2.0f }, // HeightTranslation: height ring fraction
38 { 0.0f, 2.0f }, // AngleStretch: front/rear angular spread
39 { -2.0f, 2.0f }, // OffsetX: ring centre X (base-radius units)
40 { -2.0f, 2.0f }, // OffsetY: ring centre Y (base-radius units)
41 }};
42
44 "/umsci/indicator/rot",
45 "/umsci/indicator/trans",
46 "/umsci/indicator/heighttrans",
47 "/umsci/indicator/anglestretch",
48 "/umsci/indicator/offsetx",
49 "/umsci/indicator/offsety"
50};
51
52
53// ─── MidiTab ─────────────────────────────────────────────────────────────────
54
55class UmsciExternalControlComponent::MidiTab : public juce::Component,
56 public juce::ComboBox::Listener
57{
58public:
59 MidiTab(UmsciExternalControlComponent& owner) : m_owner(owner)
60 {
61 m_midiDeviceLabel = std::make_unique<juce::Label>();
62 m_midiDeviceLabel->setText("MIDI input device:", juce::dontSendNotification);
63 addAndMakeVisible(m_midiDeviceLabel.get());
64
65 m_midiDeviceCombo = std::make_unique<juce::ComboBox>();
66 m_midiDeviceCombo->addListener(this);
67 addAndMakeVisible(m_midiDeviceCombo.get());
68
69 updateAvailableMidiInputDevices();
70
72 {
73 m_paramLabels[i] = std::make_unique<juce::Label>();
74 m_paramLabels[i]->setText(UmsciExternalControlComponent::s_paramLabels[i],
75 juce::dontSendNotification);
76 addAndMakeVisible(m_paramLabels[i].get());
77
78 m_learners[i] = std::make_unique<JUCEAppBasics::MidiLearnerComponent>(
79 static_cast<std::int16_t>(i),
80 JUCEAppBasics::MidiLearnerComponent::AT_ValueRange | JUCEAppBasics::MidiLearnerComponent::AT_CommandRange);
81
82 m_learners[i]->onMidiAssiSet = [this, i](juce::Component*, const JUCEAppBasics::MidiCommandRangeAssignment& assi) {
83 if (m_owner.onMidiAssiChanged)
85 };
86
87 addAndMakeVisible(m_learners[i].get());
88 }
89 }
90
91 void resized() override
92 {
93 constexpr int margin = 10;
94 constexpr int rowHeight = 28;
95 constexpr int rowGap = 4;
96 constexpr int labelWidth = 140;
97
98 auto bounds = getLocalBounds().reduced(margin);
99
100 auto deviceRow = bounds.removeFromTop(rowHeight);
101 m_midiDeviceLabel->setBounds(deviceRow.removeFromLeft(labelWidth));
102 m_midiDeviceCombo->setBounds(deviceRow);
103
104 bounds.removeFromTop(rowGap + 4);
105
107 {
108 if (i > 0) bounds.removeFromTop(rowGap);
109 auto row = bounds.removeFromTop(rowHeight);
110 m_paramLabels[i]->setBounds(row.removeFromLeft(labelWidth));
111 m_learners[i]->setBounds(row);
112 }
113 }
114
115 void comboBoxChanged(juce::ComboBox* cb) override
116 {
117 if (cb != m_midiDeviceCombo.get())
118 return;
119
120 auto it = m_midiInputDeviceIdentifiers.find(m_midiDeviceCombo->getSelectedId());
121 if (it == m_midiInputDeviceIdentifiers.end())
122 return;
123
124 m_currentDeviceIdentifier = it->second;
125
127 m_learners[i]->setSelectedDeviceIdentifier(m_currentDeviceIdentifier);
128
129 if (m_owner.onMidiInputDeviceChanged)
130 m_owner.onMidiInputDeviceChanged(m_currentDeviceIdentifier);
131 }
132
133 void setMidiInputDeviceIdentifier(const juce::String& identifier)
134 {
135 m_currentDeviceIdentifier = identifier;
136
137 for (auto& [id, devId] : m_midiInputDeviceIdentifiers)
138 {
139 if (devId == identifier)
140 {
141 m_midiDeviceCombo->setSelectedId(id, juce::dontSendNotification);
142 break;
143 }
144 }
145
147 m_learners[i]->setSelectedDeviceIdentifier(identifier);
148 }
149
150 const juce::String& getMidiInputDeviceIdentifier() const
151 {
152 return m_currentDeviceIdentifier;
153 }
154
155 void setMidiAssi(int param, const JUCEAppBasics::MidiCommandRangeAssignment& assi)
156 {
157 m_learners[param]->setCurrentMidiAssi(assi);
158 }
159
160 const JUCEAppBasics::MidiCommandRangeAssignment& getMidiAssi(int param) const
161 {
162 return m_learners[param]->getCurrentMidiAssi();
163 }
164
165private:
166 void updateAvailableMidiInputDevices()
167 {
168 m_midiInputDeviceIdentifiers.clear();
169 m_midiDeviceCombo->clear(juce::dontSendNotification);
170
171 int itemId = 1;
172 m_midiDeviceCombo->addItem("None", itemId);
173 m_midiInputDeviceIdentifiers[itemId] = {};
174 ++itemId;
175
176 for (auto& device : juce::MidiInput::getAvailableDevices())
177 {
178 m_midiDeviceCombo->addItem(device.name, itemId);
179 m_midiInputDeviceIdentifiers[itemId] = device.identifier;
180 ++itemId;
181 }
182
183 m_midiDeviceCombo->setSelectedId(1, juce::dontSendNotification);
184 }
185
187
188 std::unique_ptr<juce::Label> m_midiDeviceLabel;
189 std::unique_ptr<juce::ComboBox> m_midiDeviceCombo;
190 std::map<int, juce::String> m_midiInputDeviceIdentifiers;
191
192 std::unique_ptr<juce::Label> m_paramLabels[UmsciExternalControlComponent::UpmixMidiParam_COUNT];
193 std::unique_ptr<JUCEAppBasics::MidiLearnerComponent> m_learners[UmsciExternalControlComponent::UpmixMidiParam_COUNT];
194
195 juce::String m_currentDeviceIdentifier;
196};
197
198
199// ─── OscTab ──────────────────────────────────────────────────────────────────
200
201class UmsciExternalControlComponent::OscTab : public juce::Component
202{
203public:
204 OscTab(UmsciExternalControlComponent& owner) : m_owner(owner)
205 {
206 m_oscPortLabel = std::make_unique<juce::Label>();
207 m_oscPortLabel->setText("OSC listen port:", juce::dontSendNotification);
208 addAndMakeVisible(m_oscPortLabel.get());
209
210 m_oscPortEditor = std::make_unique<juce::TextEditor>();
211 m_oscPortEditor->setText("0", juce::dontSendNotification);
212 m_oscPortEditor->setInputRestrictions(5, "0123456789");
213 m_oscPortEditor->onReturnKey = [this] { notifyPortChanged(); };
214 m_oscPortEditor->onFocusLost = [this] { notifyPortChanged(); };
215 addAndMakeVisible(m_oscPortEditor.get());
216
218 {
219 m_oscParamLabels[i] = std::make_unique<juce::Label>();
220 m_oscParamLabels[i]->setText(UmsciExternalControlComponent::s_paramLabels[i],
221 juce::dontSendNotification);
222 addAndMakeVisible(m_oscParamLabels[i].get());
223
224 m_oscAddrEditors[i] = std::make_unique<juce::TextEditor>();
225 m_oscAddrEditors[i]->setText(UmsciExternalControlComponent::s_oscDefaultAddresses[i],
226 juce::dontSendNotification);
227 const int idx = i;
228 m_oscAddrEditors[i]->onReturnKey = [this, idx] { notifyAddrChanged(idx); };
229 m_oscAddrEditors[i]->onFocusLost = [this, idx] { notifyAddrChanged(idx); };
230 addAndMakeVisible(m_oscAddrEditors[i].get());
231 }
232 }
233
234 void resized() override
235 {
236 constexpr int margin = 10;
237 constexpr int rowHeight = 28;
238 constexpr int rowGap = 4;
239 constexpr int labelWidth = 140;
240
241 auto bounds = getLocalBounds().reduced(margin);
242
243 auto portRow = bounds.removeFromTop(rowHeight);
244 m_oscPortLabel->setBounds(portRow.removeFromLeft(labelWidth));
245 m_oscPortEditor->setBounds(portRow);
246
247 bounds.removeFromTop(rowGap + 4);
248
250 {
251 if (i > 0) bounds.removeFromTop(rowGap);
252 auto row = bounds.removeFromTop(rowHeight);
253 m_oscParamLabels[i]->setBounds(row.removeFromLeft(labelWidth));
254 m_oscAddrEditors[i]->setBounds(row);
255 }
256 }
257
258 void setOscInputPort(int port)
259 {
260 m_oscPortEditor->setText(juce::String(port), juce::dontSendNotification);
261 }
262
263 int getOscInputPort() const
264 {
265 return m_oscPortEditor->getText().getIntValue();
266 }
267
268 void setOscAddr(int param, const juce::String& address)
269 {
270 m_oscAddrEditors[param]->setText(address, juce::dontSendNotification);
271 }
272
273 juce::String getOscAddr(int param) const
274 {
275 return m_oscAddrEditors[param]->getText();
276 }
277
278private:
279 void notifyPortChanged()
280 {
281 if (m_owner.onOscInputPortChanged)
282 m_owner.onOscInputPortChanged(m_oscPortEditor->getText().getIntValue());
283 }
284
285 void notifyAddrChanged(int idx)
286 {
287 if (m_owner.onOscAddrChanged)
288 m_owner.onOscAddrChanged(
290 m_oscAddrEditors[idx]->getText());
291 }
292
294
295 std::unique_ptr<juce::Label> m_oscPortLabel;
296 std::unique_ptr<juce::TextEditor> m_oscPortEditor;
297
298 std::unique_ptr<juce::Label> m_oscParamLabels[UmsciExternalControlComponent::UpmixMidiParam_COUNT];
299 std::unique_ptr<juce::TextEditor> m_oscAddrEditors[UmsciExternalControlComponent::UpmixMidiParam_COUNT];
300};
301
302
303// ─── UmsciExternalControlComponent ───────────────────────────────────────────
304
306{
307 m_tabs = std::make_unique<juce::TabbedComponent>(juce::TabbedButtonBar::TabsAtTop);
308
309 auto midiTabOwned = std::make_unique<MidiTab>(*this);
310 m_midiTab = midiTabOwned.get();
311
312 auto oscTabOwned = std::make_unique<OscTab>(*this);
313 m_oscTab = oscTabOwned.get();
314
315 auto bgColour = getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId);
316 m_tabs->addTab("MIDI", bgColour, midiTabOwned.release(), true);
317 m_tabs->addTab("OSC", bgColour, oscTabOwned.release(), true);
318
319 addAndMakeVisible(m_tabs.get());
320
321 setSize(480, 280);
322}
323
327
329{
330 m_tabs->setBounds(getLocalBounds());
331}
332
333// ── MIDI delegation ──────────────────────────────────────────────────────────
334
336{
337 m_midiTab->setMidiInputDeviceIdentifier(identifier);
338}
339
341{
342 return m_midiTab->getMidiInputDeviceIdentifier();
343}
344
346 const JUCEAppBasics::MidiCommandRangeAssignment& assi)
347{
348 m_midiTab->setMidiAssi(static_cast<int>(param), assi);
349}
350
351const JUCEAppBasics::MidiCommandRangeAssignment& UmsciExternalControlComponent::getMidiAssi(UpmixMidiParam param) const
352{
353 return m_midiTab->getMidiAssi(static_cast<int>(param));
354}
355
356// ── OSC delegation ───────────────────────────────────────────────────────────
357
359{
360 m_oscTab->setOscInputPort(port);
361}
362
364{
365 return m_oscTab->getOscInputPort();
366}
367
368void UmsciExternalControlComponent::setOscAddr(UpmixMidiParam param, const juce::String& address)
369{
370 m_oscTab->setOscAddr(static_cast<int>(param), address);
371}
372
374{
375 return m_oscTab->getOscAddr(static_cast<int>(param));
376}
MidiTab(UmsciExternalControlComponent &owner)
const JUCEAppBasics::MidiCommandRangeAssignment & getMidiAssi(int param) const
void setMidiInputDeviceIdentifier(const juce::String &identifier)
void setMidiAssi(int param, const JUCEAppBasics::MidiCommandRangeAssignment &assi)
void setOscAddr(int param, const juce::String &address)
OscTab(UmsciExternalControlComponent &owner)
Settings panel for MIDI- and OSC-based external control of the six upmix transform parameters (rotati...
std::function< void(UpmixMidiParam, JUCEAppBasics::MidiCommandRangeAssignment)> onMidiAssiChanged
std::function< void(int)> onOscInputPortChanged
static const juce::String s_oscDefaultAddresses[UpmixMidiParam_COUNT]
Default OSC address for each parameter, indexed by UpmixMidiParam.
void setMidiAssi(UpmixMidiParam param, const JUCEAppBasics::MidiCommandRangeAssignment &assi)
static const juce::String s_paramLabels[UpmixMidiParam_COUNT]
Human-readable parameter labels shared by both tabs.
static const std::array< std::pair< float, float >, UpmixMidiParam_COUNT > s_paramRanges
Natural parameter ranges for normalised MIDI→domain mapping. Indexed by UpmixMidiParam.
const juce::String & getMidiInputDeviceIdentifier() const
const JUCEAppBasics::MidiCommandRangeAssignment & getMidiAssi(UpmixMidiParam param) const
UpmixMidiParam
Identifies each controllable upmix transform parameter.
juce::String getOscAddr(UpmixMidiParam param) const
void setOscAddr(UpmixMidiParam param, const juce::String &address)
std::function< void(const juce::String &)> onMidiInputDeviceChanged
std::function< void(UpmixMidiParam, juce::String)> onOscAddrChanged
void setMidiInputDeviceIdentifier(const juce::String &identifier)