]> git.sesse.net Git - nageru/blob - nageru/midi_device.cpp
Split out the ALSA-specific parts from MIDIMapper into a new class MIDIDevice.
[nageru] / nageru / midi_device.cpp
1 #include "midi_device.h"
2
3 #include <alsa/asoundlib.h>
4 #include <sys/eventfd.h>
5 #include <unistd.h>
6 #include <thread>
7 #include <utility>
8
9 using namespace std;
10
11 MIDIDevice::MIDIDevice(MIDIReceiver *receiver)
12         : receiver(receiver)
13 {
14         should_quit_fd = eventfd(/*initval=*/0, /*flags=*/0);
15         assert(should_quit_fd != -1);
16 }
17
18 MIDIDevice::~MIDIDevice()
19 {
20         should_quit = true;
21         const uint64_t one = 1;
22         if (write(should_quit_fd, &one, sizeof(one)) != sizeof(one)) {
23                 perror("write(should_quit_fd)");
24                 exit(1);
25         }
26         midi_thread.join();
27         close(should_quit_fd);
28 }
29
30 void MIDIDevice::start_thread()
31 {
32         midi_thread = thread(&MIDIDevice::thread_func, this);
33 }
34
35 #define RETURN_ON_ERROR(msg, expr) do {                            \
36         int err = (expr);                                          \
37         if (err < 0) {                                             \
38                 fprintf(stderr, msg ": %s\n", snd_strerror(err));  \
39                 return;                                            \
40         }                                                          \
41 } while (false)
42
43 #define WARN_ON_ERROR(msg, expr) do {                              \
44         int err = (expr);                                          \
45         if (err < 0) {                                             \
46                 fprintf(stderr, msg ": %s\n", snd_strerror(err));  \
47         }                                                          \
48 } while (false)
49
50
51 void MIDIDevice::thread_func()
52 {
53         pthread_setname_np(pthread_self(), "MIDIDevice");
54
55         snd_seq_t *seq;
56         int err;
57
58         RETURN_ON_ERROR("snd_seq_open", snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0));
59         RETURN_ON_ERROR("snd_seq_nonblock", snd_seq_nonblock(seq, 1));
60         RETURN_ON_ERROR("snd_seq_client_name", snd_seq_set_client_name(seq, "nageru"));
61         RETURN_ON_ERROR("snd_seq_create_simple_port",
62                 snd_seq_create_simple_port(seq, "nageru",
63                         SND_SEQ_PORT_CAP_READ |
64                                 SND_SEQ_PORT_CAP_SUBS_READ |
65                                 SND_SEQ_PORT_CAP_WRITE |
66                                 SND_SEQ_PORT_CAP_SUBS_WRITE,
67                         SND_SEQ_PORT_TYPE_MIDI_GENERIC |
68                                 SND_SEQ_PORT_TYPE_APPLICATION));
69
70         int queue_id = snd_seq_alloc_queue(seq);
71         RETURN_ON_ERROR("snd_seq_create_queue", queue_id);
72         RETURN_ON_ERROR("snd_seq_start_queue", snd_seq_start_queue(seq, queue_id, nullptr));
73
74         // The sequencer object is now ready to be used from other threads.
75         {
76                 lock_guard<mutex> lock(mu);
77                 alsa_seq = seq;
78                 alsa_queue_id = queue_id;
79         }
80
81         // Listen to the announce port (0:1), which will tell us about new ports.
82         RETURN_ON_ERROR("snd_seq_connect_from", snd_seq_connect_from(seq, 0, /*client=*/0, /*port=*/1));
83
84         // Now go through all ports and subscribe to them.
85         snd_seq_client_info_t *cinfo;
86         snd_seq_client_info_alloca(&cinfo);
87
88         snd_seq_client_info_set_client(cinfo, -1);
89         while (snd_seq_query_next_client(seq, cinfo) >= 0) {
90                 int client = snd_seq_client_info_get_client(cinfo);
91
92                 snd_seq_port_info_t *pinfo;
93                 snd_seq_port_info_alloca(&pinfo);
94
95                 snd_seq_port_info_set_client(pinfo, client);
96                 snd_seq_port_info_set_port(pinfo, -1);
97                 while (snd_seq_query_next_port(seq, pinfo) >= 0) {
98                         constexpr int mask = SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ;
99                         if ((snd_seq_port_info_get_capability(pinfo) & mask) == mask) {
100                                 lock_guard<mutex> lock(mu);
101                                 subscribe_to_port_lock_held(seq, *snd_seq_port_info_get_addr(pinfo));
102                         }
103                 }
104         }
105
106         int num_alsa_fds = snd_seq_poll_descriptors_count(seq, POLLIN);
107         unique_ptr<pollfd[]> fds(new pollfd[num_alsa_fds + 1]);
108
109         while (!should_quit) {
110                 snd_seq_poll_descriptors(seq, fds.get(), num_alsa_fds, POLLIN);
111                 fds[num_alsa_fds].fd = should_quit_fd;
112                 fds[num_alsa_fds].events = POLLIN;
113                 fds[num_alsa_fds].revents = 0;
114
115                 err = poll(fds.get(), num_alsa_fds + 1, -1);
116                 if (err == 0 || (err == -1 && errno == EINTR)) {
117                         continue;
118                 }
119                 if (err == -1) {
120                         perror("poll");
121                         break;
122                 }
123                 if (fds[num_alsa_fds].revents) {
124                         // Activity on should_quit_fd.
125                         break;
126                 }
127
128                 // Seemingly we can get multiple events in a single poll,
129                 // and if we don't handle them all, poll will _not_ alert us!
130                 while (!should_quit) {
131                         snd_seq_event_t *event;
132                         err = snd_seq_event_input(seq, &event);
133                         if (err < 0) {
134                                 if (err == -EINTR) continue;
135                                 if (err == -EAGAIN) break;
136                                 if (err == -ENOSPC) {
137                                         fprintf(stderr, "snd_seq_event_input: Some events were lost.\n");
138                                         continue;
139                                 }
140                                 fprintf(stderr, "snd_seq_event_input: %s\n", snd_strerror(err));
141                                 return;
142                         }
143                         if (event) {
144                                 handle_event(seq, event);
145                         }
146                 }
147         }
148 }
149
150 void MIDIDevice::handle_event(snd_seq_t *seq, snd_seq_event_t *event)
151 {
152         if (event->source.client == snd_seq_client_id(seq)) {
153                 // Ignore events we sent out ourselves.
154                 return;
155         }
156
157         lock_guard<mutex> lock(mu);
158         switch (event->type) {
159         case SND_SEQ_EVENT_CONTROLLER: {
160                 receiver->controller_received(event->data.control.param, event->data.control.value);
161                 break;
162         }
163         case SND_SEQ_EVENT_NOTEON: {
164                 receiver->note_on_received(event->data.note.note);
165                 break;
166         }
167         case SND_SEQ_EVENT_PORT_START:
168                 subscribe_to_port_lock_held(seq, event->data.addr);
169                 break;
170         case SND_SEQ_EVENT_PORT_EXIT:
171                 printf("MIDI port %d:%d went away.\n", event->data.addr.client, event->data.addr.port);
172                 break;
173         case SND_SEQ_EVENT_PORT_SUBSCRIBED:
174                 if (event->data.connect.sender.client != 0 &&  // Ignore system senders.
175                     event->data.connect.sender.client != snd_seq_client_id(seq) &&
176                     event->data.connect.dest.client == snd_seq_client_id(seq)) {
177                         receiver->update_num_subscribers(++num_subscribed_ports);
178                 }
179                 break;
180         case SND_SEQ_EVENT_PORT_UNSUBSCRIBED:
181                 if (event->data.connect.sender.client != 0 &&  // Ignore system senders.
182                     event->data.connect.sender.client != snd_seq_client_id(seq) &&
183                     event->data.connect.dest.client == snd_seq_client_id(seq)) {
184                         receiver->update_num_subscribers(--num_subscribed_ports);
185                 }
186                 break;
187         case SND_SEQ_EVENT_NOTEOFF:
188         case SND_SEQ_EVENT_CLIENT_START:
189         case SND_SEQ_EVENT_CLIENT_EXIT:
190         case SND_SEQ_EVENT_CLIENT_CHANGE:
191         case SND_SEQ_EVENT_PORT_CHANGE:
192                 break;
193         default:
194                 printf("Ignoring MIDI event of unknown type %d.\n", event->type);
195         }
196 }
197
198 void MIDIDevice::subscribe_to_port_lock_held(snd_seq_t *seq, const snd_seq_addr_t &addr)
199 {
200         // Client 0 (SNDRV_SEQ_CLIENT_SYSTEM) is basically the system; ignore it.
201         // MIDI through (SNDRV_SEQ_CLIENT_DUMMY) echoes back what we give it, so ignore that, too.
202         if (addr.client == 0 || addr.client == 14) {
203                 return;
204         }
205
206         // Don't listen to ourselves.
207         if (addr.client == snd_seq_client_id(seq)) {
208                 return;
209         }
210
211         int err = snd_seq_connect_from(seq, 0, addr.client, addr.port);
212         if (err < 0) {
213                 // Just print out a warning (i.e., don't die); it could
214                 // very well just be e.g. another application.
215                 printf("Couldn't subscribe to MIDI port %d:%d (%s).\n",
216                         addr.client, addr.port, snd_strerror(err));
217         } else {
218                 printf("Subscribed to MIDI port %d:%d.\n", addr.client, addr.port);
219         }
220
221         // For sending data back.
222         err = snd_seq_connect_to(seq, 0, addr.client, addr.port);
223         if (err < 0) {
224                 printf("Couldn't subscribe MIDI port %d:%d (%s) to us.\n",
225                         addr.client, addr.port, snd_strerror(err));
226         } else {
227                 printf("Subscribed MIDI port %d:%d to us.\n", addr.client, addr.port);
228         }
229
230         // The current status of the device is unknown, so refresh it.
231         set<unsigned> active_lights;
232         for (const auto &it : current_light_status) {
233                 if (it.second) {
234                         active_lights.insert(it.first);
235                 }
236         }
237         current_light_status.clear();
238         update_lights_lock_held(active_lights);
239 }
240
241 void MIDIDevice::update_lights_lock_held(const set<unsigned> &active_lights)
242 {
243         if (alsa_seq == nullptr) {
244                 return;
245         }
246
247         unsigned num_events = 0;
248         for (unsigned note_num = 1; note_num <= 127; ++note_num) {
249                 bool active = active_lights.count(note_num);
250                 if (current_light_status.count(note_num) &&
251                     current_light_status[note_num] == active) {
252                         // Already known to be in the desired state.
253                         continue;
254                 }
255
256                 snd_seq_event_t ev;
257                 snd_seq_ev_clear(&ev);
258
259                 // Some devices drop events if we throw them onto them
260                 // too quickly. Add a 1 ms delay for each.
261                 snd_seq_real_time_t tm{0, num_events++ * 1000000};
262                 snd_seq_ev_schedule_real(&ev, alsa_queue_id, true, &tm);
263                 snd_seq_ev_set_source(&ev, 0);
264                 snd_seq_ev_set_subs(&ev);
265
266                 // For some reason, not all devices respond to note off.
267                 // Use note-on with velocity of 0 (which is equivalent) instead.
268                 snd_seq_ev_set_noteon(&ev, /*channel=*/0, note_num, active ? 1 : 0);
269                 WARN_ON_ERROR("snd_seq_event_output", snd_seq_event_output(alsa_seq, &ev));
270                 current_light_status[note_num] = active;
271         }
272         WARN_ON_ERROR("snd_seq_drain_output", snd_seq_drain_output(alsa_seq));
273 }