diff --git a/src/tools/meson.build b/src/tools/meson.build index 8651bd3b1..300a9cb4c 100644 --- a/src/tools/meson.build +++ b/src/tools/meson.build @@ -5,7 +5,7 @@ tools_sources = [ [ 'pw-dot', [ 'pw-dot.c' ] ], [ 'pw-dump', [ 'pw-dump.c' ] ], [ 'pw-profiler', [ 'pw-profiler.c' ] ], - [ 'pw-mididump', [ 'pw-mididump.c', 'midifile.c', 'midievent.c' ] ], + [ 'pw-mididump', [ 'pw-mididump.c', 'midifile.c', 'midievent.c', 'midiclip.c' ] ], [ 'pw-metadata', [ 'pw-metadata.c' ] ], [ 'pw-loopback', [ 'pw-loopback.c' ] ], [ 'pw-link', [ 'pw-link.c' ] ], @@ -48,6 +48,7 @@ if get_option('pw-cat').allowed() and sndfile_dep.found() pwcat_sources = [ 'pw-cat.c', 'midifile.c', + 'midiclip.c', 'midievent.c', 'dfffile.c', 'dsffile.c', diff --git a/src/tools/midiclip.c b/src/tools/midiclip.c new file mode 100644 index 000000000..4d7741d44 --- /dev/null +++ b/src/tools/midiclip.c @@ -0,0 +1,327 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "midiclip.h" + +#define DEFAULT_BPM 120 +#define SEC_AS_10NS 100000000.0 +#define MINUTE_10NS 6000000000 /* in 10ns units */ +#define DEFAULT_TEMPO MINUTE_10NS/DEFAULT_BPM + +struct midi_clip { + int mode; + FILE *file; + bool close; + int64_t count; + + uint8_t data[16]; + + uint32_t next[4]; + int num; + + bool pass_all; + struct midi_clip_info info; + uint32_t tempo; + + int64_t tick; + int64_t tick_start; + double tick_sec; +}; + +static int read_header(struct midi_clip *mc) +{ + uint8_t data[8]; + + if (fread(data, sizeof(data), 1, mc->file) != 1 || + memcmp(data, "SMF2CLIP", 4) != 0) + return -EINVAL; + return 0; +} + +static inline int read_word(struct midi_clip *mc, uint32_t *val) +{ + uint32_t v; + if (fread(&v, 4, 1, mc->file) != 1) + return 0; + *val = be32toh(v); + return 1; +} + +static inline int read_ump(struct midi_clip *mc) +{ + int i, num; + mc->num = 0; + if (read_word(mc, &mc->next[0]) != 1) + return 0; + num = spa_ump_message_size(mc->next[0]>>28); + for (i = 1; i < num; i++) { + if (read_word(mc, &mc->next[i]) != 1) + return 0; + } + return mc->num = num; +} + +static int next_packet(struct midi_clip *mc) +{ + while (read_ump(mc) > 0) { + uint8_t type = mc->next[0] >> 28; + + switch (type) { + case 0x0: /* utility */ + switch ((mc->next[0] >> 20) & 0xf) { + case 0x3: /* DCTPQ */ + mc->info.division = (mc->next[0] & 0xffff); + break; + case 0x4: /* DC */ + mc->tick += (mc->next[0] & 0xfffff); + break; + } + break; + case 0x2: /* midi 1.0 */ + case 0x3: /* sysex 7bits */ + case 0x4: /* midi 2.0 */ + return mc->num; + case 0xd: /* flex data */ + if (((mc->next[0] >> 8) & 0xff) == 0 && + (mc->next[0] & 0xff) == 0) + mc->tempo = mc->next[1]; + break; + case 0xf: /* stream */ + break; + default: + break; + } + if (mc->pass_all) + return mc->num; + } + return 0; +} + +static int open_read(struct midi_clip *mc, const char *filename, struct midi_clip_info *info) +{ + int res; + + if (strcmp(filename, "-") != 0) { + if ((mc->file = fopen(filename, "r")) == NULL) { + res = -errno; + goto exit; + } + mc->close = true; + } else { + mc->file = stdin; + mc->close = false; + } + + if ((res = read_header(mc)) < 0) + goto exit_close; + + mc->tempo = DEFAULT_TEMPO; + mc->tick = 0; + mc->mode = 1; + + next_packet(mc); + *info = mc->info; + return 0; + +exit_close: + if (mc->close) + fclose(mc->file); +exit: + return res; +} + +static inline int write_n(FILE *file, const void *buf, int count) +{ + return fwrite(buf, 1, count, file) == (size_t)count ? count : -errno; +} + +static inline int write_be32(FILE *file, uint32_t val) +{ + uint8_t buf[4] = { val >> 24, val >> 16, val >> 8, val }; + return write_n(file, buf, 4); +} + +#define CHECK_RES(expr) if ((res = (expr)) < 0) return res + +static int write_headers(struct midi_clip *mc) +{ + int res; + CHECK_RES(write_n(mc->file, "SMF2CLIP", 8)); + + /* DC 0 */ + CHECK_RES(write_be32(mc->file, 0x00400000)); + /* DCTPQ division */ + CHECK_RES(write_be32(mc->file, 0x00300000 | mc->info.division)); + /* tempo */ + CHECK_RES(write_be32(mc->file, 0xd0100000)); + CHECK_RES(write_be32(mc->file, mc->tempo)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + /* start */ + CHECK_RES(write_be32(mc->file, 0xf0200000)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + + return 0; +} + +static int open_write(struct midi_clip *mc, const char *filename, struct midi_clip_info *info) +{ + int res; + + if (info->format != 0) + return -EINVAL; + if (info->division == 0) + info->division = 96; + + if (strcmp(filename, "-") != 0) { + if ((mc->file = fopen(filename, "w")) == NULL) { + res = -errno; + goto exit; + } + mc->close = true; + } else { + mc->file = stdout; + mc->close = false; + } + mc->mode = 2; + mc->tempo = DEFAULT_TEMPO; + mc->info = *info; + + res = write_headers(mc); +exit: + return res; +} + +struct midi_clip * +midi_clip_open(const char *filename, const char *mode, struct midi_clip_info *info) +{ + int res; + struct midi_clip *mc; + + mc = calloc(1, sizeof(struct midi_clip)); + if (mc == NULL) + return NULL; + + if (spa_streq(mode, "r")) { + if ((res = open_read(mc, filename, info)) < 0) + goto exit_free; + } else if (spa_streq(mode, "w")) { + if ((res = open_write(mc, filename, info)) < 0) + goto exit_free; + } else { + res = -EINVAL; + goto exit_free; + } + return mc; + +exit_free: + free(mc); + errno = -res; + return NULL; +} + +int midi_clip_close(struct midi_clip *mc) +{ + int res; + + if (mc->mode == 2) { + CHECK_RES(write_be32(mc->file, 0xf0210000)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + } else if (mc->mode != 1) + return -EINVAL; + + if (mc->close) + fclose(mc->file); + free(mc); + return 0; +} + +int midi_clip_next_time(struct midi_clip *mc, double *sec) +{ + if (mc->num <= 0) + return 0; + + if (mc->info.division == 0) + *sec = 0.0; + else + *sec = mc->tick_sec + ((mc->tick - mc->tick_start) * (double)mc->tempo) / + (SEC_AS_10NS * mc->info.division); + return 1; +} + +int midi_clip_read_event(struct midi_clip *mc, struct midi_event *event) +{ + if (midi_clip_next_time(mc, &event->sec) != 1) + return 0; + event->track = 0; + event->type = MIDI_EVENT_TYPE_UMP; + event->data = mc->data; + event->size = mc->num * 4; + memcpy(mc->data, mc->next, event->size); + + next_packet(mc); + return 1; +} + +int midi_clip_write_event(struct midi_clip *mc, const struct midi_event *event) +{ + uint32_t tick; + void *data; + size_t size; + int res, i, ump_size; + int32_t diff; + uint32_t ump[4], *ump_data; + uint64_t state = 0; + + spa_return_val_if_fail(event != NULL, -EINVAL); + spa_return_val_if_fail(mc != NULL, -EINVAL); + spa_return_val_if_fail(event->track == 0, -EINVAL); + spa_return_val_if_fail(event->size > 1, -EINVAL); + + data = event->data; + size = event->size; + + tick = (uint32_t)(event->sec * (SEC_AS_10NS * mc->info.division) / (double)mc->tempo); + + diff = mc->count++ == 0 ? 0 : tick - mc->tick; + if (diff > 0 || mc->count == 1) + CHECK_RES(write_be32(mc->file, 0x00400000 | diff)); + mc->tick = tick; + + while (size > 0) { + switch (event->type) { + case MIDI_EVENT_TYPE_UMP: + ump_data = data; + ump_size = size; + size = 0; + break; + case MIDI_EVENT_TYPE_MIDI1: + ump_size = spa_ump_from_midi((uint8_t**)&data, &size, + ump, sizeof(ump), event->track, &state); + if (ump_size <= 0) + return ump_size; + ump_data = ump; + break; + default: + return -EINVAL; + } + for (i = 0; i < ump_size/4; i++) + CHECK_RES(write_be32(mc->file, ump_data[i])); + } + return 0; +} diff --git a/src/tools/midiclip.h b/src/tools/midiclip.h new file mode 100644 index 000000000..8c02a0b00 --- /dev/null +++ b/src/tools/midiclip.h @@ -0,0 +1,27 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2025 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +#include "midievent.h" + +struct midi_clip; + +struct midi_clip_info { + uint16_t format; + uint16_t division; +}; + +struct midi_clip * +midi_clip_open(const char *filename, const char *mode, struct midi_clip_info *info); + +int midi_clip_close(struct midi_clip *mc); + +int midi_clip_next_time(struct midi_clip *mc, double *sec); + +int midi_clip_read_event(struct midi_clip *mc, struct midi_event *event); + +int midi_clip_write_event(struct midi_clip *mc, const struct midi_event *event); diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c index d017b2316..a9cd4460f 100644 --- a/src/tools/pw-cat.c +++ b/src/tools/pw-cat.c @@ -43,6 +43,7 @@ #endif #include "midifile.h" +#include "midiclip.h" #include "dfffile.h" #include "dsffile.h" @@ -104,6 +105,7 @@ struct data { #define TYPE_ENCODED 3 #endif #define TYPE_SYSEX 4 +#define TYPE_MIDI2 5 int data_type; bool raw; const char *remote_name; @@ -147,6 +149,10 @@ struct data { #define MIDI_FORCE_MIDI1 2 int force_type; } midi; + struct { + struct midi_clip *file; + struct midi_clip_info info; + } clip; struct { struct dsf_file *file; struct dsf_file_info info; @@ -1063,6 +1069,7 @@ static const struct option long_options[] = { { "raw", no_argument, NULL, 'a' }, { "force-midi", required_argument, NULL, 'M' }, { "sample-count", required_argument, NULL, 'n' }, + { "midi-clip", no_argument, NULL, 'c' }, { NULL, 0, NULL, 0 } }; @@ -1127,6 +1134,7 @@ static void show_usage(const char *name, bool is_error) " -o, --encoded Encoded mode\n" #endif " -s, --sysex SysEx mode\n" + " -c, --midi-clip MIDI clip mode\n" "\n"), fp); } } @@ -1297,6 +1305,143 @@ static int setup_midifile(struct data *data) return 0; } +static int clip_play(struct data *d, void *src, unsigned int n_frames, bool *null_frame) +{ + int res; + struct spa_pod_builder b; + struct spa_pod_frame f; + uint32_t first_frame, last_frame; + bool have_data = false; + + spa_zero(b); + spa_pod_builder_init(&b, src, n_frames); + + spa_pod_builder_push_sequence(&b, &f, 0); + + first_frame = d->clock_time; + last_frame = first_frame + d->position->clock.duration; + d->clock_time = last_frame; + + while (1) { + uint32_t frame; + struct midi_event ev; + uint64_t state = 0; + size_t size; + + res = midi_clip_next_time(d->clip.file, &ev.sec); + if (res <= 0) { + if (have_data) + break; + return res; + } + + frame = (uint32_t)(ev.sec * d->position->clock.rate.denom); + if (frame < first_frame) + frame = 0; + else if (frame < last_frame) + frame -= first_frame; + else + break; + + midi_clip_read_event(d->clip.file, &ev); + + if (d->verbose) + midi_event_dump(stderr, &ev); + + size = ev.size; + + if (d->midi.force_type == MIDI_FORCE_MIDI1) { + const uint32_t *data = (const uint32_t*)ev.data; + while (size > 0) { + uint8_t ev[16]; + int ev_size = spa_ump_to_midi(&data, &size, + ev, sizeof(ev), &state); + if (ev_size <= 0) + break; + + spa_pod_builder_control(&b, frame, SPA_CONTROL_Midi); + spa_pod_builder_bytes(&b, ev, ev_size); + } + } else { + spa_pod_builder_control(&b, frame, SPA_CONTROL_UMP); + spa_pod_builder_bytes(&b, ev.data, ev.size); + } + have_data = true; + } + spa_pod_builder_pop(&b, &f); + + return b.state.offset; +} + +static int clip_record(struct data *d, void *src, unsigned int n_frames, bool *null_frame) +{ + struct spa_pod_parser parser; + struct spa_pod_frame frame; + struct spa_pod_sequence seq; + const void *seq_body, *c_body; + struct spa_pod_control c; + uint32_t offset; + + offset = d->clock_time; + d->clock_time += d->position->clock.duration; + + spa_pod_parser_init_from_data(&parser, src, n_frames, 0, n_frames); + + if (spa_pod_parser_push_sequence_body(&parser, &frame, &seq, &seq_body) < 0) + return 0; + + while (spa_pod_parser_get_control_body(&parser, &c, &c_body) >= 0) { + struct midi_event ev; + + switch (c.type) { + case SPA_CONTROL_UMP: + ev.type = MIDI_EVENT_TYPE_UMP; + break; + case SPA_CONTROL_Midi: + ev.type = MIDI_EVENT_TYPE_MIDI1; + break; + default: + continue; + } + ev.track = 0; + ev.sec = (offset + c.offset) / (float) d->position->clock.rate.denom; + ev.data = (uint8_t*)c_body; + ev.size = c.value.size; + + if (d->verbose) + midi_event_dump(stderr, &ev); + + midi_clip_write_event(d->clip.file, &ev); + } + return 0; +} + +static int setup_midiclip(struct data *data) +{ + if (data->mode == mode_record) { + spa_zero(data->clip.info); + data->clip.info.format = 0; + } + + data->clip.file = midi_clip_open(data->filename, + data->mode == mode_playback ? "r" : "w", + &data->clip.info); + if (data->clip.file == NULL) { + fprintf(stderr, "midiclip: can't read midi file '%s': %m\n", data->filename); + return -errno; + } + + if (data->verbose) + fprintf(stderr, "midifile: opened file \"%s\" format %08x div:%d\n", + data->filename, + data->clip.info.format, data->clip.info.division); + + data->fill = data->mode == mode_playback ? clip_play : clip_record; + data->stride = 1; + + return 0; +} + static int sysex_play(struct data *d, void *dst, unsigned int n_frames, bool *null_frame) { struct spa_pod_builder b; @@ -1881,9 +2026,9 @@ int main(int argc, char *argv[]) } #ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION - while ((c = getopt_long(argc, argv, "hvprmdosR:q:P:aM:n:", long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "hvprmdosR:q:P:aM:n:c", long_options, NULL)) != -1) { #else - while ((c = getopt_long(argc, argv, "hvprmdsR:q:P:aM:n:", long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "hvprmdsR:q:P:aM:n:c", long_options, NULL)) != -1) { #endif switch (c) { @@ -2019,6 +2164,9 @@ int main(int argc, char *argv[]) case 'n': data.sample_limit = strtoull(optarg, NULL, 10); break; + case 'c': + data.data_type = TYPE_MIDI2; + break; default: goto error_usage; } @@ -2032,6 +2180,7 @@ int main(int argc, char *argv[]) if (!data.media_type) { switch (data.data_type) { case TYPE_MIDI: + case TYPE_MIDI2: case TYPE_SYSEX: data.media_type = DEFAULT_MIDI_MEDIA_TYPE; break; @@ -2112,6 +2261,9 @@ int main(int argc, char *argv[]) case TYPE_MIDI: ret = setup_midifile(&data); break; + case TYPE_MIDI2: + ret = setup_midiclip(&data); + break; case TYPE_DSD: ret = setup_dsdfile(&data); break; @@ -2185,6 +2337,7 @@ int main(int argc, char *argv[]) break; } case TYPE_MIDI: + case TYPE_MIDI2: case TYPE_SYSEX: params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, @@ -2307,6 +2460,8 @@ error_no_main_loop: sf_close(data.file); if (data.midi.file) midi_file_close(data.midi.file); + if (data.clip.file) + midi_clip_close(data.clip.file); if (data.dsf.file) dsf_file_close(data.dsf.file); if (data.dff.file) diff --git a/src/tools/pw-mididump.c b/src/tools/pw-mididump.c index f7ba5fcba..277784bb1 100644 --- a/src/tools/pw-mididump.c +++ b/src/tools/pw-mididump.c @@ -19,6 +19,7 @@ #include #include "midifile.h" +#include "midiclip.h" struct data; @@ -35,6 +36,29 @@ struct data { bool opt_midi1; }; + +static int dump_clip(const char *filename) +{ + struct midi_clip *file; + struct midi_clip_info info; + struct midi_event ev; + + file = midi_clip_open(filename, "r", &info); + if (file == NULL) { + fprintf(stderr, "error opening %s: %m\n", filename); + return -1; + } + + printf("opened %s format:%u division:%u\n", filename, info.format, info.division); + + while (midi_clip_read_event(file, &ev) == 1) + midi_event_dump(stdout, &ev); + + midi_clip_close(file); + + return 0; +} + static int dump_file(const char *filename) { struct midi_file *file; @@ -43,15 +67,14 @@ static int dump_file(const char *filename) file = midi_file_open(filename, "r", &info); if (file == NULL) { - fprintf(stderr, "error opening %s: %m\n", filename); - return -1; + return dump_clip(filename); } printf("opened %s format:%u ntracks:%u division:%u\n", filename, info.format, info.ntracks, info.division); - while (midi_file_read_event(file, &ev) == 1) { + while (midi_file_read_event(file, &ev) == 1) midi_event_dump(stdout, &ev); - } + midi_file_close(file); return 0;