diff options
-rw-r--r-- | src/tools/meson.build | 1 | ||||
-rw-r--r-- | src/tools/midifile.c | 258 | ||||
-rw-r--r-- | src/tools/midifile.h | 88 | ||||
-rw-r--r-- | src/tools/pw-cat.c | 160 |
4 files changed, 497 insertions, 10 deletions
diff --git a/src/tools/meson.build b/src/tools/meson.build index dcd116ac..dda11315 100644 --- a/src/tools/meson.build +++ b/src/tools/meson.build @@ -28,6 +28,7 @@ if get_option('pw-cat') and sndfile_dep.found() pwcat_sources = [ 'pw-cat.c', + 'midifile.c', ] pwcat_aliases = [ diff --git a/src/tools/midifile.c b/src/tools/midifile.c new file mode 100644 index 00000000..30457c51 --- /dev/null +++ b/src/tools/midifile.c @@ -0,0 +1,258 @@ +/* PipeWire + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <errno.h> + +#include "midifile.h" + +#define DEFAULT_TEMPO 500000 /* 500ms per quarter note (120 BPM) is the default */ + +static inline uint16_t parse_be16(const uint8_t *in) +{ + return (in[0] << 8) | in[1]; +} + +static inline uint32_t parse_be32(const uint8_t *in) +{ + return (in[0] << 24) | (in[1] << 16) | (in[2] << 8) | in[3]; +} + +static int read_mthd(struct midi_file *mf) +{ + uint8_t buffer[14]; + int res; + + if ((res = mf->events->read(mf->data, mf->offset, buffer, 14)) != 14) + return res < 0 ? res : -EIO; + + if (memcmp(buffer, "MThd", 4)) + return -EIO; + + mf->size = parse_be32(buffer + 4); + mf->format = parse_be16(buffer + 8); + mf->ntracks = parse_be16(buffer + 10); + mf->division = parse_be16(buffer + 12); + mf->offset += 14; + return 0; +} + +int midi_file_open(struct midi_file *mf, int mode, + const struct midi_events *events, void *data) +{ + int res; + + spa_zero(*mf); + + mf->events = events; + mf->data = data; + + if ((res = read_mthd(mf)) < 0) + return res; + + spa_list_init(&mf->tracks); + mf->tick = 0; + mf->tempo = DEFAULT_TEMPO; + + return 0; +} + +int midi_file_close(struct midi_file *mf) +{ + return 0; +} + +static int read_mtrk(struct midi_file *mf, struct midi_track *track) +{ + uint8_t buffer[8]; + int res; + + if ((res = mf->events->read(mf->data, mf->offset, buffer, 8)) != 8) + return res < 0 ? res : -EIO; + + if (memcmp(buffer, "MTrk", 4)) + return -EIO; + + track->start = mf->offset+8; + track->offset = 0; + track->size = parse_be32(buffer + 4); + mf->offset += track->size + 8; + return 0; +} + +static int parse_varlen(struct midi_file *mf, struct midi_track *tr, uint32_t *result) +{ + uint32_t value; + uint8_t buffer[1]; + int i, res; + + value = 0; + for (i = 0; i < 4; i++) { + if (tr->offset >= tr->size) { + tr->eof = true; + break; + } + + if ((res = mf->events->read(mf->data, tr->start + tr->offset, buffer, 1)) != 1) + return res < 0 ? res : -EIO; + + tr->offset++; + + value = (value << 7) | ((buffer[0]) & 0x7f); + if ((buffer[0] & 0x80) == 0) + break; + } + *result = value; + return 0; +} + + +static int peek_event(struct midi_file *mf, struct midi_track *tr, struct midi_event *event) +{ + uint8_t buffer[4], status; + uint32_t size = 0, start; + int res; + + if (tr->eof || tr->offset > tr->size) + return -EIO; + + if ((res = mf->events->read(mf->data, tr->start + tr->offset, buffer, 1)) != 1) + return res < 0 ? res : -EIO; + + event->track = tr; + event->tick = tr->tick; + start = event->offset = tr->offset; + + status = buffer[0]; + if ((status & 0x80) == 0) { + status = tr->running_status; + } else { + tr->running_status = status; + event->offset++; + } + + event->status = status; + + tr->offset++; + + if (status < 0xf0) { + size++; + if (status < 0xc0 || status >= 0xe0) + size++; + } else { + if (status == 0xff) { + if ((res = mf->events->read(mf->data, tr->start + tr->offset, buffer, 1)) != 1) + return res < 0 ? res : -EIO; + + tr->offset++; + + if ((res = parse_varlen(mf, tr, &size)) < 0) + return res; + + event->meta = buffer[0]; + event->offset = tr->offset; + + switch (event->meta) { + case 0x2f: + tr->eof = true; + break; + case 0x51: + if (size < 3) + break; + + if ((res = mf->events->read(mf->data, tr->start + tr->offset, buffer, 3)) != 3) + return res < 0 ? res : -EIO; + + mf->tempo = (buffer[0]<<16) | (buffer[1]<<8) | buffer[2]; + break; + } + + } else if (status == 0xf0 || status == 0xf7) { + if ((res = parse_varlen(mf, tr, &size)) < 0) + return res; + event->offset = tr->offset; + } else { + return -EIO; + } + } + tr->offset = start; + + event->offset += tr->start; + event->size = size; + + return 0; +} + +int midi_file_add_track(struct midi_file *mf, struct midi_track *track) +{ + int res; + uint32_t delta_time; + + if ((res = read_mtrk(mf, track)) < 0) + return res; + + if ((res = parse_varlen(mf, track, &delta_time)) < 0) + return res; + + track->tick = delta_time; + spa_list_init(&track->events); + spa_list_append(&mf->tracks, &track->link); + + return 0; + +} +int midi_file_peek_event(struct midi_file *mf, struct midi_event *event) +{ + struct midi_track *tr, *found = NULL; + + spa_list_for_each(tr, &mf->tracks, link) { + if (tr->eof) + continue; + if (found == NULL || tr->tick < found->tick) + found = tr; + } + if (found == NULL) + return -EIO; + + return peek_event(mf, found, event); +} + +int midi_file_consume_event(struct midi_file *mf, struct midi_event *event) +{ + struct midi_track *tr = event->track; + uint32_t delta_time; + int res; + + tr->offset = event->offset - tr->start + event->size; + if ((res = parse_varlen(mf, tr, &delta_time)) < 0) + return res; + tr->tick += delta_time; + return 0; +} + +int midi_file_add_event(struct midi_file *mf, struct midi_track *track, struct midi_event *event) +{ + spa_list_append(&track->events, &event->link); + return 0; +} + diff --git a/src/tools/midifile.h b/src/tools/midifile.h new file mode 100644 index 00000000..8c600383 --- /dev/null +++ b/src/tools/midifile.h @@ -0,0 +1,88 @@ +/* PipeWire + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <stdio.h> + +#include <spa/utils/defs.h> +#include <spa/utils/list.h> + +struct midi_event { + struct spa_list link; + + struct midi_track *track; + + int64_t tick; + uint8_t status; + uint8_t meta; + uint32_t offset; + size_t size; +}; + +struct midi_track { + struct spa_list link; + + uint32_t start; + uint32_t size; + + uint32_t offset; + int64_t tick; + uint8_t running_status; + unsigned int eof:1; + + struct spa_list events; +}; + +struct midi_events { + int (*read) (void *data, size_t offset, void *buf, size_t size); + int (*write) (void *data, size_t offset, void *buf, size_t size); +}; + +struct midi_file { + uint32_t size; + uint16_t format; + uint16_t ntracks; + uint16_t division; + uint32_t tempo; + + struct spa_list tracks; + + uint32_t offset; + int64_t tick; + + const struct midi_events *events; + void *data; +}; + +int midi_file_open(struct midi_file *mf, int mode, + const struct midi_events *events, void *data); +int midi_file_close(struct midi_file *mf); + +int midi_file_add_track(struct midi_file *mf, struct midi_track *track); + +int midi_file_peek_event(struct midi_file *mf, struct midi_event *event); +int midi_file_consume_event(struct midi_file *mf, struct midi_event *event); + +int midi_file_add_event(struct midi_file *mf, struct midi_track *track, struct midi_event *event); + + diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c index d6e80d29..ae361bdf 100644 --- a/src/tools/pw-cat.c +++ b/src/tools/pw-cat.c @@ -47,6 +47,8 @@ #include <pipewire/pipewire.h> #include <pipewire/global.h> +#include "midifile.h" + #define DEFAULT_MEDIA_TYPE "Audio" #define DEFAULT_MEDIA_CATEGORY_PLAYBACK "Playback" #define DEFAULT_MEDIA_CATEGORY_RECORD "Capture" @@ -103,6 +105,7 @@ struct data { enum mode mode; bool verbose; + bool midi; const char *remote_name; const char *media_type; const char *media_category; @@ -137,7 +140,15 @@ struct data { bool targets_listed; struct spa_list targets; + struct spa_io_position *position; bool drained; + uint64_t clock_start; + + struct { + FILE *f; + struct midi_file file; + struct midi_track track[64]; + } md; }; static inline int @@ -730,6 +741,20 @@ on_state_changed(void *userdata, enum pw_stream_state old, } static void +on_io_changed(void *userdata, uint32_t id, void *data, uint32_t size) +{ + struct data *d = userdata; + + switch (id) { + case SPA_IO_Position: + d->position = data; + break; + default: + break; + } +} + +static void on_param_changed(void *userdata, uint32_t id, const struct spa_pod *format) { struct data *data = userdata; @@ -810,6 +835,7 @@ static void on_drained(void *userdata) static const struct pw_stream_events stream_events = { PW_VERSION_STREAM_EVENTS, .state_changed = on_state_changed, + .io_changed = on_io_changed, .param_changed = on_param_changed, .process = on_process, .drained = on_drained @@ -843,6 +869,7 @@ static const struct option long_options[] = { {"record", no_argument, NULL, 'r'}, {"playback", no_argument, NULL, 's'}, + {"midi", no_argument, NULL, 'm'}, {"remote", required_argument, NULL, 'R'}, @@ -915,6 +942,7 @@ static void show_usage(const char *name, bool is_error) fprintf(fp, " -p, --playback Playback mode\n" " -r, --record Recording mode\n" + " -m, --midi Midi mode\n" "\n"); } @@ -923,6 +951,100 @@ static void show_usage(const char *name, bool is_error) "\n"); } +static int midi_read(void *data, size_t offset, void *buf, size_t size) +{ + struct data *d = data; + fseek(d->md.f, offset, SEEK_SET); + return fread(buf, 1, size, d->md.f); +} + +static const struct midi_events midi_events = { + .read = midi_read, +}; + +static int midi_play(struct data *d, void *src, unsigned int n_frames) +{ + int res; + struct midi_event ev; + struct spa_pod_builder b; + struct spa_pod_frame f; + uint8_t buf[4096]; + uint32_t first_frame, last_frame; + + spa_zero(b); + spa_pod_builder_init(&b, src, n_frames); + + spa_pod_builder_push_sequence(&b, &f, 0); + + if (d->clock_start == 0) + d->clock_start = d->position->clock.position; + + first_frame = d->position->clock.position - d->clock_start; + last_frame = first_frame + d->position->clock.duration; + + while (1) { + uint32_t frame; + + res = midi_file_peek_event(&d->md.file, &ev); + if (res < 0) + return res; + + frame = ev.tick * ((double)d->position->clock.rate.denom * d->md.file.tempo) / + (1000000.0 * d->md.file.division); + + if (frame < first_frame) + frame = 0; + else if (frame < last_frame) + frame -= first_frame; + else + break; + + spa_pod_builder_control(&b, frame, SPA_CONTROL_Midi); + buf[0] = ev.status; + if (ev.status == 0xff) + buf[1] = ev.meta; + midi_read(d, ev.offset, ev.status == 0xff ? &buf[2] : &buf[1], ev.size); + spa_pod_builder_bytes(&b, buf, ev.size + 1); + + midi_file_consume_event(&d->md.file, &ev); + } + spa_pod_builder_pop(&b, &f); + + return b.state.offset; +} + +static int setup_midifile(struct data *data) +{ + int res; + uint16_t i; + + data->md.f = fopen(data->filename, + data->mode == mode_playback ? "r" : "w"); + if (data->md.f == NULL) { + fprintf(stderr, "error: failed to open midi file \"%s\": %m\n", + data->filename); + return -errno; + } + + if ((res = midi_file_open(&data->md.file, 0, &midi_events, data)) < 0) + return -errno; + + if (data->verbose) + printf("opened file \"%s\" format %08x ntracks:%d div:%d\n", + data->filename, + data->md.file.format, data->md.file.ntracks, + data->md.file.division); + + for (i = 0; i < data->md.file.ntracks; i++) { + midi_file_add_track(&data->md.file, &data->md.track[i]); + } + data->fill = data->mode == mode_playback ? + midi_play : midi_play; + data->stride = 1; + + return 0; +} + static int setup_sndfile(struct data *data) { SF_INFO info; @@ -1104,7 +1226,7 @@ int main(int argc, char *argv[]) /* initialize list everytime */ spa_list_init(&data.targets); - while ((c = getopt_long(argc, argv, "hvprR:q:", long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "hvprmR:q:", long_options, NULL)) != -1) { switch (c) { @@ -1133,6 +1255,10 @@ int main(int argc, char *argv[]) data.mode = mode_record; break; + case 'm': + data.midi = true; + break; + case 'R': data.remote_name = optarg; break; @@ -1310,7 +1436,11 @@ int main(int argc, char *argv[]) if (!data.list_targets) { struct spa_audio_info_raw info; - ret = setup_sndfile(&data); + if (data.midi) + ret = setup_midifile(&data); + else + ret = setup_sndfile(&data); + if (ret < 0) { fprintf(stderr, "error: open failed: %s\n", spa_strerror(ret)); switch (ret) { @@ -1321,16 +1451,26 @@ int main(int argc, char *argv[]) goto error_usage; } } - info = SPA_AUDIO_INFO_RAW_INIT( - .flags = data.channelmap.n_channels ? 0 : SPA_AUDIO_FLAG_UNPOSITIONED, - .format = data.spa_format, - .rate = data.rate, - .channels = data.channels); - if (data.channelmap.n_channels) - memcpy(info.position, data.channelmap.channels, data.channels * sizeof(int)); + if (!data.midi) { + info = SPA_AUDIO_INFO_RAW_INIT( + .flags = data.channelmap.n_channels ? 0 : SPA_AUDIO_FLAG_UNPOSITIONED, + .format = data.spa_format, + .rate = data.rate, + .channels = data.channels); + + if (data.channelmap.n_channels) + memcpy(info.position, data.channelmap.channels, data.channels * sizeof(int)); - params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info); + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info); + } else { + params[0] = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + + pw_properties_set(data.props, PW_KEY_FORMAT_DSP, "8 bit raw midi"); + } data.stream = pw_stream_new(data.core, prog, data.props); data.props = NULL; |