1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* ALSA sequencer binding for UMP device */ |
3 | |
4 | #include <linux/init.h> |
5 | #include <linux/slab.h> |
6 | #include <linux/errno.h> |
7 | #include <linux/mutex.h> |
8 | #include <linux/string.h> |
9 | #include <linux/module.h> |
10 | #include <asm/byteorder.h> |
11 | #include <sound/core.h> |
12 | #include <sound/ump.h> |
13 | #include <sound/seq_kernel.h> |
14 | #include <sound/seq_device.h> |
15 | #include "seq_clientmgr.h" |
16 | #include "seq_system.h" |
17 | |
18 | struct seq_ump_client; |
19 | struct seq_ump_group; |
20 | |
21 | enum { |
22 | STR_IN = SNDRV_RAWMIDI_STREAM_INPUT, |
23 | STR_OUT = SNDRV_RAWMIDI_STREAM_OUTPUT |
24 | }; |
25 | |
26 | /* object per UMP group; corresponding to a sequencer port */ |
27 | struct seq_ump_group { |
28 | int group; /* group index (0-based) */ |
29 | unsigned int dir_bits; /* directions */ |
30 | bool active; /* activeness */ |
31 | char name[64]; /* seq port name */ |
32 | }; |
33 | |
34 | /* context for UMP input parsing, per EP */ |
35 | struct seq_ump_input_buffer { |
36 | unsigned char len; /* total length in words */ |
37 | unsigned char pending; /* pending words */ |
38 | unsigned char type; /* parsed UMP packet type */ |
39 | unsigned char group; /* parsed UMP packet group */ |
40 | u32 buf[4]; /* incoming UMP packet */ |
41 | }; |
42 | |
43 | /* sequencer client, per UMP EP (rawmidi) */ |
44 | struct seq_ump_client { |
45 | struct snd_ump_endpoint *ump; /* assigned endpoint */ |
46 | int seq_client; /* sequencer client id */ |
47 | int opened[2]; /* current opens for each direction */ |
48 | struct snd_rawmidi_file out_rfile; /* rawmidi for output */ |
49 | struct seq_ump_input_buffer input; /* input parser context */ |
50 | struct seq_ump_group groups[SNDRV_UMP_MAX_GROUPS]; /* table of groups */ |
51 | void *ump_info[SNDRV_UMP_MAX_BLOCKS + 1]; /* shadow of seq client ump_info */ |
52 | struct work_struct group_notify_work; /* FB change notification */ |
53 | }; |
54 | |
55 | /* number of 32bit words for each UMP message type */ |
56 | static unsigned char ump_packet_words[0x10] = { |
57 | 1, 1, 1, 2, 2, 4, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4 |
58 | }; |
59 | |
60 | /* conversion between UMP group and seq port; |
61 | * assume the port number is equal with UMP group number (1-based) |
62 | */ |
63 | static unsigned char ump_group_to_seq_port(unsigned char group) |
64 | { |
65 | return group + 1; |
66 | } |
67 | |
68 | /* process the incoming rawmidi stream */ |
69 | static void seq_ump_input_receive(struct snd_ump_endpoint *ump, |
70 | const u32 *val, int words) |
71 | { |
72 | struct seq_ump_client *client = ump->seq_client; |
73 | struct snd_seq_ump_event ev = {}; |
74 | |
75 | if (!client->opened[STR_IN]) |
76 | return; |
77 | |
78 | if (ump_is_groupless_msg(ump_message_type(*val))) |
79 | ev.source.port = 0; /* UMP EP port */ |
80 | else |
81 | ev.source.port = ump_group_to_seq_port(group: ump_message_group(data: *val)); |
82 | ev.dest.client = SNDRV_SEQ_ADDRESS_SUBSCRIBERS; |
83 | ev.flags = SNDRV_SEQ_EVENT_UMP; |
84 | memcpy(ev.ump, val, words << 2); |
85 | snd_seq_kernel_client_dispatch(client: client->seq_client, |
86 | ev: (struct snd_seq_event *)&ev, |
87 | atomic: true, hop: 0); |
88 | } |
89 | |
90 | /* process an input sequencer event; only deal with UMP types */ |
91 | static int seq_ump_process_event(struct snd_seq_event *ev, int direct, |
92 | void *private_data, int atomic, int hop) |
93 | { |
94 | struct seq_ump_client *client = private_data; |
95 | struct snd_rawmidi_substream *substream; |
96 | struct snd_seq_ump_event *ump_ev; |
97 | unsigned char type; |
98 | int len; |
99 | |
100 | substream = client->out_rfile.output; |
101 | if (!substream) |
102 | return -ENODEV; |
103 | if (!snd_seq_ev_is_ump(ev)) |
104 | return 0; /* invalid event, skip */ |
105 | ump_ev = (struct snd_seq_ump_event *)ev; |
106 | type = ump_message_type(data: ump_ev->ump[0]); |
107 | len = ump_packet_words[type]; |
108 | if (len > 4) |
109 | return 0; // invalid - skip |
110 | snd_rawmidi_kernel_write(substream, buf: ev->data.raw8.d, count: len << 2); |
111 | return 0; |
112 | } |
113 | |
114 | /* open the rawmidi */ |
115 | static int seq_ump_client_open(struct seq_ump_client *client, int dir) |
116 | { |
117 | struct snd_ump_endpoint *ump = client->ump; |
118 | int err; |
119 | |
120 | guard(mutex)(T: &ump->open_mutex); |
121 | if (dir == STR_OUT && !client->opened[dir]) { |
122 | err = snd_rawmidi_kernel_open(rmidi: &ump->core, subdevice: 0, |
123 | SNDRV_RAWMIDI_LFLG_OUTPUT | |
124 | SNDRV_RAWMIDI_LFLG_APPEND, |
125 | rfile: &client->out_rfile); |
126 | if (err < 0) |
127 | return err; |
128 | } |
129 | client->opened[dir]++; |
130 | return 0; |
131 | } |
132 | |
133 | /* close the rawmidi */ |
134 | static int seq_ump_client_close(struct seq_ump_client *client, int dir) |
135 | { |
136 | struct snd_ump_endpoint *ump = client->ump; |
137 | |
138 | guard(mutex)(T: &ump->open_mutex); |
139 | if (!--client->opened[dir]) |
140 | if (dir == STR_OUT) |
141 | snd_rawmidi_kernel_release(rfile: &client->out_rfile); |
142 | return 0; |
143 | } |
144 | |
145 | /* sequencer subscription ops for each client */ |
146 | static int seq_ump_subscribe(void *pdata, struct snd_seq_port_subscribe *info) |
147 | { |
148 | struct seq_ump_client *client = pdata; |
149 | |
150 | return seq_ump_client_open(client, dir: STR_IN); |
151 | } |
152 | |
153 | static int seq_ump_unsubscribe(void *pdata, struct snd_seq_port_subscribe *info) |
154 | { |
155 | struct seq_ump_client *client = pdata; |
156 | |
157 | return seq_ump_client_close(client, dir: STR_IN); |
158 | } |
159 | |
160 | static int seq_ump_use(void *pdata, struct snd_seq_port_subscribe *info) |
161 | { |
162 | struct seq_ump_client *client = pdata; |
163 | |
164 | return seq_ump_client_open(client, dir: STR_OUT); |
165 | } |
166 | |
167 | static int seq_ump_unuse(void *pdata, struct snd_seq_port_subscribe *info) |
168 | { |
169 | struct seq_ump_client *client = pdata; |
170 | |
171 | return seq_ump_client_close(client, dir: STR_OUT); |
172 | } |
173 | |
174 | /* fill port_info from the given UMP EP and group info */ |
175 | static void fill_port_info(struct snd_seq_port_info *port, |
176 | struct seq_ump_client *client, |
177 | struct seq_ump_group *group) |
178 | { |
179 | unsigned int rawmidi_info = client->ump->core.info_flags; |
180 | |
181 | port->addr.client = client->seq_client; |
182 | port->addr.port = ump_group_to_seq_port(group: group->group); |
183 | port->capability = 0; |
184 | if (rawmidi_info & SNDRV_RAWMIDI_INFO_OUTPUT) |
185 | port->capability |= SNDRV_SEQ_PORT_CAP_WRITE | |
186 | SNDRV_SEQ_PORT_CAP_SYNC_WRITE | |
187 | SNDRV_SEQ_PORT_CAP_SUBS_WRITE; |
188 | if (rawmidi_info & SNDRV_RAWMIDI_INFO_INPUT) |
189 | port->capability |= SNDRV_SEQ_PORT_CAP_READ | |
190 | SNDRV_SEQ_PORT_CAP_SYNC_READ | |
191 | SNDRV_SEQ_PORT_CAP_SUBS_READ; |
192 | if (rawmidi_info & SNDRV_RAWMIDI_INFO_DUPLEX) |
193 | port->capability |= SNDRV_SEQ_PORT_CAP_DUPLEX; |
194 | if (group->dir_bits & (1 << STR_IN)) |
195 | port->direction |= SNDRV_SEQ_PORT_DIR_INPUT; |
196 | if (group->dir_bits & (1 << STR_OUT)) |
197 | port->direction |= SNDRV_SEQ_PORT_DIR_OUTPUT; |
198 | port->ump_group = group->group + 1; |
199 | if (!group->active) |
200 | port->capability |= SNDRV_SEQ_PORT_CAP_INACTIVE; |
201 | port->type = SNDRV_SEQ_PORT_TYPE_MIDI_GENERIC | |
202 | SNDRV_SEQ_PORT_TYPE_MIDI_UMP | |
203 | SNDRV_SEQ_PORT_TYPE_HARDWARE | |
204 | SNDRV_SEQ_PORT_TYPE_PORT; |
205 | port->midi_channels = 16; |
206 | if (*group->name) |
207 | snprintf(buf: port->name, size: sizeof(port->name), fmt: "Group %d (%.53s)" , |
208 | group->group + 1, group->name); |
209 | else |
210 | sprintf(buf: port->name, fmt: "Group %d" , group->group + 1); |
211 | } |
212 | |
213 | /* create a new sequencer port per UMP group */ |
214 | static int seq_ump_group_init(struct seq_ump_client *client, int group_index) |
215 | { |
216 | struct seq_ump_group *group = &client->groups[group_index]; |
217 | struct snd_seq_port_info *port __free(kfree) = NULL; |
218 | struct snd_seq_port_callback pcallbacks; |
219 | |
220 | port = kzalloc(size: sizeof(*port), GFP_KERNEL); |
221 | if (!port) |
222 | return -ENOMEM; |
223 | |
224 | fill_port_info(port, client, group); |
225 | port->flags = SNDRV_SEQ_PORT_FLG_GIVEN_PORT; |
226 | memset(&pcallbacks, 0, sizeof(pcallbacks)); |
227 | pcallbacks.owner = THIS_MODULE; |
228 | pcallbacks.private_data = client; |
229 | pcallbacks.subscribe = seq_ump_subscribe; |
230 | pcallbacks.unsubscribe = seq_ump_unsubscribe; |
231 | pcallbacks.use = seq_ump_use; |
232 | pcallbacks.unuse = seq_ump_unuse; |
233 | pcallbacks.event_input = seq_ump_process_event; |
234 | port->kernel = &pcallbacks; |
235 | return snd_seq_kernel_client_ctl(client: client->seq_client, |
236 | SNDRV_SEQ_IOCTL_CREATE_PORT, |
237 | arg: port); |
238 | } |
239 | |
240 | /* update the sequencer ports; called from notify_fb_change callback */ |
241 | static void update_port_infos(struct seq_ump_client *client) |
242 | { |
243 | struct snd_seq_port_info *old __free(kfree) = NULL; |
244 | struct snd_seq_port_info *new __free(kfree) = NULL; |
245 | int i, err; |
246 | |
247 | old = kzalloc(size: sizeof(*old), GFP_KERNEL); |
248 | new = kzalloc(size: sizeof(*new), GFP_KERNEL); |
249 | if (!old || !new) |
250 | return; |
251 | |
252 | for (i = 0; i < SNDRV_UMP_MAX_GROUPS; i++) { |
253 | old->addr.client = client->seq_client; |
254 | old->addr.port = i; |
255 | err = snd_seq_kernel_client_ctl(client: client->seq_client, |
256 | SNDRV_SEQ_IOCTL_GET_PORT_INFO, |
257 | arg: old); |
258 | if (err < 0) |
259 | return; |
260 | fill_port_info(port: new, client, group: &client->groups[i]); |
261 | if (old->capability == new->capability && |
262 | !strcmp(old->name, new->name)) |
263 | continue; |
264 | err = snd_seq_kernel_client_ctl(client: client->seq_client, |
265 | SNDRV_SEQ_IOCTL_SET_PORT_INFO, |
266 | arg: new); |
267 | if (err < 0) |
268 | return; |
269 | /* notify to system port */ |
270 | snd_seq_system_client_ev_port_change(client->seq_client, i); |
271 | } |
272 | } |
273 | |
274 | /* update dir_bits and active flag for all groups in the client */ |
275 | static void update_group_attrs(struct seq_ump_client *client) |
276 | { |
277 | struct snd_ump_block *fb; |
278 | struct seq_ump_group *group; |
279 | int i; |
280 | |
281 | for (i = 0; i < SNDRV_UMP_MAX_GROUPS; i++) { |
282 | group = &client->groups[i]; |
283 | *group->name = 0; |
284 | group->dir_bits = 0; |
285 | group->active = 0; |
286 | group->group = i; |
287 | } |
288 | |
289 | list_for_each_entry(fb, &client->ump->block_list, list) { |
290 | if (fb->info.first_group + fb->info.num_groups > SNDRV_UMP_MAX_GROUPS) |
291 | break; |
292 | group = &client->groups[fb->info.first_group]; |
293 | for (i = 0; i < fb->info.num_groups; i++, group++) { |
294 | if (fb->info.active) |
295 | group->active = 1; |
296 | switch (fb->info.direction) { |
297 | case SNDRV_UMP_DIR_INPUT: |
298 | group->dir_bits |= (1 << STR_IN); |
299 | break; |
300 | case SNDRV_UMP_DIR_OUTPUT: |
301 | group->dir_bits |= (1 << STR_OUT); |
302 | break; |
303 | case SNDRV_UMP_DIR_BIDIRECTION: |
304 | group->dir_bits |= (1 << STR_OUT) | (1 << STR_IN); |
305 | break; |
306 | } |
307 | if (!*fb->info.name) |
308 | continue; |
309 | if (!*group->name) { |
310 | /* store the first matching name */ |
311 | strscpy(group->name, fb->info.name, |
312 | sizeof(group->name)); |
313 | } else { |
314 | /* when overlapping, concat names */ |
315 | strlcat(p: group->name, q: ", " , avail: sizeof(group->name)); |
316 | strlcat(p: group->name, q: fb->info.name, |
317 | avail: sizeof(group->name)); |
318 | } |
319 | } |
320 | } |
321 | } |
322 | |
323 | /* create a UMP Endpoint port */ |
324 | static int create_ump_endpoint_port(struct seq_ump_client *client) |
325 | { |
326 | struct snd_seq_port_info *port __free(kfree) = NULL; |
327 | struct snd_seq_port_callback pcallbacks; |
328 | unsigned int rawmidi_info = client->ump->core.info_flags; |
329 | int err; |
330 | |
331 | port = kzalloc(size: sizeof(*port), GFP_KERNEL); |
332 | if (!port) |
333 | return -ENOMEM; |
334 | |
335 | port->addr.client = client->seq_client; |
336 | port->addr.port = 0; /* fixed */ |
337 | port->flags = SNDRV_SEQ_PORT_FLG_GIVEN_PORT; |
338 | port->capability = SNDRV_SEQ_PORT_CAP_UMP_ENDPOINT; |
339 | if (rawmidi_info & SNDRV_RAWMIDI_INFO_INPUT) { |
340 | port->capability |= SNDRV_SEQ_PORT_CAP_READ | |
341 | SNDRV_SEQ_PORT_CAP_SYNC_READ | |
342 | SNDRV_SEQ_PORT_CAP_SUBS_READ; |
343 | port->direction |= SNDRV_SEQ_PORT_DIR_INPUT; |
344 | } |
345 | if (rawmidi_info & SNDRV_RAWMIDI_INFO_OUTPUT) { |
346 | port->capability |= SNDRV_SEQ_PORT_CAP_WRITE | |
347 | SNDRV_SEQ_PORT_CAP_SYNC_WRITE | |
348 | SNDRV_SEQ_PORT_CAP_SUBS_WRITE; |
349 | port->direction |= SNDRV_SEQ_PORT_DIR_OUTPUT; |
350 | } |
351 | if (rawmidi_info & SNDRV_RAWMIDI_INFO_DUPLEX) |
352 | port->capability |= SNDRV_SEQ_PORT_CAP_DUPLEX; |
353 | port->ump_group = 0; /* no associated group, no conversion */ |
354 | port->type = SNDRV_SEQ_PORT_TYPE_MIDI_UMP | |
355 | SNDRV_SEQ_PORT_TYPE_HARDWARE | |
356 | SNDRV_SEQ_PORT_TYPE_PORT; |
357 | port->midi_channels = 16; |
358 | strcpy(p: port->name, q: "MIDI 2.0" ); |
359 | memset(&pcallbacks, 0, sizeof(pcallbacks)); |
360 | pcallbacks.owner = THIS_MODULE; |
361 | pcallbacks.private_data = client; |
362 | if (rawmidi_info & SNDRV_RAWMIDI_INFO_INPUT) { |
363 | pcallbacks.subscribe = seq_ump_subscribe; |
364 | pcallbacks.unsubscribe = seq_ump_unsubscribe; |
365 | } |
366 | if (rawmidi_info & SNDRV_RAWMIDI_INFO_OUTPUT) { |
367 | pcallbacks.use = seq_ump_use; |
368 | pcallbacks.unuse = seq_ump_unuse; |
369 | pcallbacks.event_input = seq_ump_process_event; |
370 | } |
371 | port->kernel = &pcallbacks; |
372 | err = snd_seq_kernel_client_ctl(client: client->seq_client, |
373 | SNDRV_SEQ_IOCTL_CREATE_PORT, |
374 | arg: port); |
375 | return err; |
376 | } |
377 | |
378 | /* release the client resources */ |
379 | static void seq_ump_client_free(struct seq_ump_client *client) |
380 | { |
381 | cancel_work_sync(work: &client->group_notify_work); |
382 | |
383 | if (client->seq_client >= 0) |
384 | snd_seq_delete_kernel_client(client: client->seq_client); |
385 | |
386 | client->ump->seq_ops = NULL; |
387 | client->ump->seq_client = NULL; |
388 | |
389 | kfree(objp: client); |
390 | } |
391 | |
392 | /* update the MIDI version for the given client */ |
393 | static void setup_client_midi_version(struct seq_ump_client *client) |
394 | { |
395 | struct snd_seq_client *cptr; |
396 | |
397 | cptr = snd_seq_kernel_client_get(client: client->seq_client); |
398 | if (!cptr) |
399 | return; |
400 | if (client->ump->info.protocol & SNDRV_UMP_EP_INFO_PROTO_MIDI2) |
401 | cptr->midi_version = SNDRV_SEQ_CLIENT_UMP_MIDI_2_0; |
402 | else |
403 | cptr->midi_version = SNDRV_SEQ_CLIENT_UMP_MIDI_1_0; |
404 | snd_seq_kernel_client_put(cptr); |
405 | } |
406 | |
407 | /* set up client's group_filter bitmap */ |
408 | static void setup_client_group_filter(struct seq_ump_client *client) |
409 | { |
410 | struct snd_seq_client *cptr; |
411 | unsigned int filter; |
412 | int p; |
413 | |
414 | cptr = snd_seq_kernel_client_get(client: client->seq_client); |
415 | if (!cptr) |
416 | return; |
417 | filter = ~(1U << 0); /* always allow groupless messages */ |
418 | for (p = 0; p < SNDRV_UMP_MAX_GROUPS; p++) { |
419 | if (client->groups[p].active) |
420 | filter &= ~(1U << (p + 1)); |
421 | } |
422 | cptr->group_filter = filter; |
423 | snd_seq_kernel_client_put(cptr); |
424 | } |
425 | |
426 | /* UMP group change notification */ |
427 | static void handle_group_notify(struct work_struct *work) |
428 | { |
429 | struct seq_ump_client *client = |
430 | container_of(work, struct seq_ump_client, group_notify_work); |
431 | |
432 | update_group_attrs(client); |
433 | update_port_infos(client); |
434 | setup_client_group_filter(client); |
435 | } |
436 | |
437 | /* UMP FB change notification */ |
438 | static int seq_ump_notify_fb_change(struct snd_ump_endpoint *ump, |
439 | struct snd_ump_block *fb) |
440 | { |
441 | struct seq_ump_client *client = ump->seq_client; |
442 | |
443 | if (!client) |
444 | return -ENODEV; |
445 | schedule_work(work: &client->group_notify_work); |
446 | return 0; |
447 | } |
448 | |
449 | /* UMP protocol change notification; just update the midi_version field */ |
450 | static int seq_ump_switch_protocol(struct snd_ump_endpoint *ump) |
451 | { |
452 | if (!ump->seq_client) |
453 | return -ENODEV; |
454 | setup_client_midi_version(ump->seq_client); |
455 | return 0; |
456 | } |
457 | |
458 | static const struct snd_seq_ump_ops seq_ump_ops = { |
459 | .input_receive = seq_ump_input_receive, |
460 | .notify_fb_change = seq_ump_notify_fb_change, |
461 | .switch_protocol = seq_ump_switch_protocol, |
462 | }; |
463 | |
464 | /* create a sequencer client and ports for the given UMP endpoint */ |
465 | static int snd_seq_ump_probe(struct device *_dev) |
466 | { |
467 | struct snd_seq_device *dev = to_seq_dev(_dev); |
468 | struct snd_ump_endpoint *ump = dev->private_data; |
469 | struct snd_card *card = dev->card; |
470 | struct seq_ump_client *client; |
471 | struct snd_ump_block *fb; |
472 | struct snd_seq_client *cptr; |
473 | int p, err; |
474 | |
475 | client = kzalloc(size: sizeof(*client), GFP_KERNEL); |
476 | if (!client) |
477 | return -ENOMEM; |
478 | |
479 | INIT_WORK(&client->group_notify_work, handle_group_notify); |
480 | client->ump = ump; |
481 | |
482 | client->seq_client = |
483 | snd_seq_create_kernel_client(card, client_index: ump->core.device, |
484 | name_fmt: ump->core.name); |
485 | if (client->seq_client < 0) { |
486 | err = client->seq_client; |
487 | goto error; |
488 | } |
489 | |
490 | client->ump_info[0] = &ump->info; |
491 | list_for_each_entry(fb, &ump->block_list, list) |
492 | client->ump_info[fb->info.block_id + 1] = &fb->info; |
493 | |
494 | setup_client_midi_version(client); |
495 | update_group_attrs(client); |
496 | |
497 | for (p = 0; p < SNDRV_UMP_MAX_GROUPS; p++) { |
498 | err = seq_ump_group_init(client, group_index: p); |
499 | if (err < 0) |
500 | goto error; |
501 | } |
502 | |
503 | setup_client_group_filter(client); |
504 | |
505 | err = create_ump_endpoint_port(client); |
506 | if (err < 0) |
507 | goto error; |
508 | |
509 | cptr = snd_seq_kernel_client_get(client: client->seq_client); |
510 | if (!cptr) { |
511 | err = -EINVAL; |
512 | goto error; |
513 | } |
514 | cptr->ump_info = client->ump_info; |
515 | snd_seq_kernel_client_put(cptr); |
516 | |
517 | ump->seq_client = client; |
518 | ump->seq_ops = &seq_ump_ops; |
519 | return 0; |
520 | |
521 | error: |
522 | seq_ump_client_free(client); |
523 | return err; |
524 | } |
525 | |
526 | /* remove a sequencer client */ |
527 | static int snd_seq_ump_remove(struct device *_dev) |
528 | { |
529 | struct snd_seq_device *dev = to_seq_dev(_dev); |
530 | struct snd_ump_endpoint *ump = dev->private_data; |
531 | |
532 | if (ump->seq_client) |
533 | seq_ump_client_free(client: ump->seq_client); |
534 | return 0; |
535 | } |
536 | |
537 | static struct snd_seq_driver seq_ump_driver = { |
538 | .driver = { |
539 | .name = KBUILD_MODNAME, |
540 | .probe = snd_seq_ump_probe, |
541 | .remove = snd_seq_ump_remove, |
542 | }, |
543 | .id = SNDRV_SEQ_DEV_ID_UMP, |
544 | .argsize = 0, |
545 | }; |
546 | |
547 | module_snd_seq_driver(seq_ump_driver); |
548 | |
549 | MODULE_DESCRIPTION("ALSA sequencer client for UMP rawmidi" ); |
550 | MODULE_LICENSE("GPL" ); |
551 | |