OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
qml_midi_outbound_socket.hpp
1#pragma once
2#include <ossia/network/context.hpp>
3
4#include <ossia-qt/protocols/utils.hpp>
5
6#include <QJSValue>
7#include <QObject>
8#include <QQmlEngine>
9#include <QVariant>
10
11#include <libremidi/libremidi.hpp>
12
13#include <nano_observer.hpp>
14
15#include <verdigris>
16
17namespace ossia::qt
18{
19/*
20 var devices = Protocols.outboundMIDIDevices();
21 var midi = Protocols.outboundMIDI({
22 Transport: devices[0],
23 onOpen: function(socket) {
24 console.log("MIDI port opened!");
25 socket.sendNoteOn(1, 60, 100); // channel 1, middle C, velocity 100
26 socket.sendNoteOff(1, 60, 0); // channel 1, middle C, velocity 0
27 socket.sendControlChange(1, 7, 64); // channel 1, volume, value 64
28 socket.sendMessage([0x90, 60, 100]); // raw MIDI bytes
29 },
30 onError: function(error) {
31 console.log("MIDI error:", error);
32 }
33 });
34*/
35class qml_midi_outbound_socket
36 : public QObject
37 , public Nano::Observer
38{
39 W_OBJECT(qml_midi_outbound_socket)
40public:
41 qml_midi_outbound_socket() { }
42
43 ~qml_midi_outbound_socket() { close(); }
44
45 void open(const libremidi::port_information& pi)
46 {
47 try
48 {
49 // Configure MIDI output
50 libremidi::output_configuration config;
51
52 // Create MIDI output
53 m_midi_out = std::make_unique<libremidi::midi_out>(config);
54
55 // Open the port
56 if(auto err = m_midi_out->open_port(static_cast<const libremidi::output_port&>(pi));
57 err != stdx::error{})
58 {
59 if(onError.isCallable())
60 {
61 const auto& msg = err.message();
62 run_on_qt_thread(
63 { onError.call({QString::fromUtf8(msg.data(), msg.size())}); });
64 }
65 return;
66 }
67
68 m_is_open = true;
69
70 // Call onOpen callback
71 if(onOpen.isCallable())
72 {
73 run_on_qt_thread({ onOpen.call({qjsEngine(this)->newQObject(this)}); });
74 }
75 }
76 catch(const std::exception& e)
77 {
78 if(onError.isCallable())
79 {
80 run_on_qt_thread({ onError.call({QString::fromUtf8(e.what())}); });
81 }
82 }
83 }
84
85 void open()
86 {
87 try
88 {
89 // Create MIDI observer to enumerate ports
90 m_observer = std::make_unique<libremidi::observer>();
91
92 // Get available MIDI output ports
93 auto ports = m_observer->get_output_ports();
94
95 if(ports.empty())
96 {
97 if(onError.isCallable())
98 {
99 run_on_qt_thread({ onError.call({"No MIDI output ports available"}); });
100 }
101 return;
102 }
103
104 // Default to first available port
105 open(ports[0]);
106 }
107 catch(const std::exception& e)
108 {
109 if(onError.isCallable())
110 {
111 run_on_qt_thread({ onError.call({QString::fromUtf8(e.what())}); });
112 }
113 }
114 }
115
116 void close()
117 {
118 if(m_is_open)
119 {
120 m_is_open = false;
121 m_midi_out.reset();
122 m_observer.reset();
123
124 if(onClose.isCallable())
125 {
126 run_on_qt_thread({ onClose.call(); });
127 }
128 }
129 }
130 W_SLOT(close)
131
132 // Send raw MIDI message
133 void sendMessage(QVariantList bytes)
134 {
135 if(!m_is_open || !m_midi_out)
136 return;
137
138 libremidi::message msg;
139 for(const auto& byte : bytes)
140 {
141 msg.bytes.push_back(static_cast<uint8_t>(byte.toInt()));
142 }
143
144 if(auto err = m_midi_out->send_message(msg); err != stdx::error{})
145 {
146 if(onError.isCallable())
147 {
148 const auto& errmsg = err.message();
149 run_on_qt_thread(
150 { onError.call({QString::fromUtf8(errmsg.data(), errmsg.size())}); });
151 }
152 }
153 }
154 W_SLOT(sendMessage)
155
156 // Convenience methods for common MIDI messages
157 void sendNoteOn(int channel, int note, int velocity)
158 {
159 if(!m_is_open || !m_midi_out)
160 return;
161
162 if(channel < 1 || channel > 16 || note < 0 || note > 127 || velocity < 0 || velocity > 127)
163 return;
164
165 m_midi_out->send_message(0x90 | (channel - 1), note, velocity);
166 }
167 W_SLOT(sendNoteOn)
168
169 void sendNoteOff(int channel, int note, int velocity)
170 {
171 if(!m_is_open || !m_midi_out)
172 return;
173
174 if(channel < 1 || channel > 16 || note < 0 || note > 127 || velocity < 0 || velocity > 127)
175 return;
176
177 m_midi_out->send_message(0x80 | (channel - 1), note, velocity);
178 }
179 W_SLOT(sendNoteOff)
180
181 void sendControlChange(int channel, int controller, int value)
182 {
183 if(!m_is_open || !m_midi_out)
184 return;
185
186 if(channel < 1 || channel > 16 || controller < 0 || controller > 127 || value < 0 || value > 127)
187 return;
188
189 m_midi_out->send_message(0xB0 | (channel - 1), controller, value);
190 }
191 W_SLOT(sendControlChange)
192
193 void sendProgramChange(int channel, int program)
194 {
195 if(!m_is_open || !m_midi_out)
196 return;
197
198 if(channel < 1 || channel > 16 || program < 0 || program > 127)
199 return;
200
201 m_midi_out->send_message(0xC0 | (channel - 1), program);
202 }
203 W_SLOT(sendProgramChange)
204
205 void sendPitchBend(int channel, int value)
206 {
207 if(!m_is_open || !m_midi_out)
208 return;
209
210 if(channel < 1 || channel > 16 || value < 0 || value > 16383)
211 return;
212
213 m_midi_out->send_message(0xE0 | (channel - 1), value & 0x7F, (value >> 7) & 0x7F);
214 }
215 W_SLOT(sendPitchBend)
216
217 void sendSysex(QVariantList data)
218 {
219 if(!m_is_open || !m_midi_out)
220 return;
221
222 libremidi::message msg;
223 msg.bytes.push_back(0xF0); // Start of SysEx
224 for(const auto& byte : data)
225 {
226 int val = byte.toInt();
227 if(val >= 0 && val <= 127) // SysEx data bytes must be 7-bit
228 msg.bytes.push_back(static_cast<uint8_t>(val));
229 }
230 msg.bytes.push_back(0xF7); // End of SysEx
231
232 if(auto err = m_midi_out->send_message(msg); err != stdx::error{})
233 {
234 if(onError.isCallable())
235 {
236 const auto& errmsg = err.message();
237 run_on_qt_thread(
238 { onError.call({QString::fromUtf8(errmsg.data(), errmsg.size())}); });
239 }
240 }
241 }
242 W_SLOT(sendSysex)
243
244 // Callbacks
245 QJSValue onOpen;
246 QJSValue onClose;
247 QJSValue onError;
248
249private:
250 std::unique_ptr<libremidi::observer> m_observer;
251 std::unique_ptr<libremidi::midi_out> m_midi_out;
252 std::atomic_bool m_is_open{false};
253};
254
255}
Definition qml_device.cpp:43