From 79da488132bb21da98a3697ff4ab8a5ea9eb795d Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Mon, 18 Aug 2025 16:28:35 -0600 Subject: [PATCH] Reimplement MIDI file import to retain metadata in the files written Previosuly, all meta data was thrown away. Now we retain it, so that MIDI regions can ask about their source file's tempo & meter. Significant engineering rework of how this all works, relying on recently introduced API and API changes in SMF, SMFSource etc. --- libs/ardour/import.cc | 338 ++++++++++++++++++++++++++++++++---------- 1 file changed, 260 insertions(+), 78 deletions(-) diff --git a/libs/ardour/import.cc b/libs/ardour/import.cc index 833df4440d..79646d514f 100644 --- a/libs/ardour/import.cc +++ b/libs/ardour/import.cc @@ -363,61 +363,132 @@ write_audio_data_to_new_files (ImportableSource* source, ImportStatus& status, } static void -write_midi_data_to_new_files (Evoral::SMF* source, ImportStatus& status, - vector >& newfiles, - bool split_midi_channels) +write_midi_type0_data_to_one_file (Evoral::SMF* source, ImportStatus& status, size_t n, size_t nfiles, std::shared_ptr smfs, bool split_midi_channels, int channel) { - uint32_t buf_size = 4; - uint8_t* buf = (uint8_t*) malloc (buf_size); + uint32_t bufsize = 4; + uint8_t* buf = (uint8_t*) malloc (bufsize); + bool had_meta = false; + Evoral::event_id_t ignored_note_id; /* imported files either don't have noted IDs or we ignore them */ - status.progress = 0.0f; - - bool type0 = source->smf_format()==0; - - int total_files = newfiles.size(); + Source::WriterLock target_lock (smfs->mutex()); + smfs->mark_streaming_write_started (target_lock); + smfs->drop_model (target_lock); try { - vector >::iterator s = newfiles.begin(); - int cur_chan = 0; + source->seek_to_start(); - for (int i = 0; i < total_files; ++i) { + uint64_t t = 0; + uint32_t delta_t = 0; + uint32_t size = 0; + uint32_t written = 0; - int cur_track = i+1; //first Track of a type-1 file is metadata only. Start importing sourcefiles at Track index 1 + while (!status.cancel) { - if (split_midi_channels) { //if splitting channels we will need to fill 16x sources. empties will be disposed-of later - cur_track = 1 + (int) floor((float)i/16.f); //calculate the Track needed for this sourcefile (offset by 1) + size = bufsize; + + int ret = source->read_event (&delta_t, &size, &buf, &ignored_note_id); + + if (size > bufsize) { + bufsize = size; } - std::shared_ptr smfs = std::dynamic_pointer_cast (*s); - if (!smfs) { - continue; //should never happen. The calling code should provide exactly the number of tracks&channels we need + if (ret < 0) { // EOT + break; } - Source::WriterLock source_lock(smfs->mutex()); + t += delta_t; - smfs->drop_model (source_lock); - if (type0) { - source->seek_to_start (); - } else { - source->seek_to_track (cur_track); + /* if requested by user, each sourcefile gets only a single channel's data */ + + if (split_midi_channels) { + uint8_t type = buf[0] & 0xf0; + uint8_t chan = buf[0] & 0x0f; + if (type >= 0x80 && type <= 0xE0) { + if (chan != channel) { + continue; + } + } } + smfs->append_event_beats ( + target_lock, + Evoral::Event( + Evoral::MIDI_EVENT, + Temporal::Beats::ticks_at_rate(t, source->ppqn()), + size, + buf)); + + written++; + + if (status.progress < 0.99) { + status.progress += 0.01; + } + } + + if (had_meta || written) { + + /* we wrote something */ + + smfs->mark_streaming_write_completed (target_lock, timecnt_t (source->duration())); + + /* the streaming write that we've just finished + * only wrote data to the SMF object, which is + * ultimately an on-disk data structure. So now + * we pull the data back from disk to build our + * in-memory MidiModel version. + */ + + smfs->load_model (target_lock, true); + + } else { + info << string_compose (_("No usable MIDI data found for file %1 of %2"), n, nfiles) << endmsg; + } + + } catch (exception& e) { + error << string_compose (_("MIDI file could not be written (best guess: %1)"), e.what()) << endmsg; + } + + free (buf); + +} + +static void +write_midi_type1_data_to_one_file (Evoral::SMF* source, ImportStatus& status, std::shared_ptr smfs, + int track, bool split_midi_channels, int channel) +{ + uint32_t bufsize = 4; + uint8_t* buf = (uint8_t*) malloc (bufsize); + bool had_meta = false; + Evoral::event_id_t ignored_note_id; /* imported files either don't have noted IDs or we ignore them */ + + Source::WriterLock target_lock (smfs->mutex()); + smfs->mark_streaming_write_started (target_lock); + smfs->drop_model (target_lock); + + try { + + /* Check track number is legal. Remember, track 0 is metadata, so the number of + * real tracks is one less than the number of tracks reported via libsmf. + */ + + if (track >= source->num_tracks() - 1) { + return; + } + + /* Get metadata first */ + + if (source->seek_to_track (1) == 0) { /* type 1 has metadata in track 1 */ + uint64_t t = 0; uint32_t delta_t = 0; uint32_t size = 0; - bool first = true; while (!status.cancel) { - gint note_id_ignored; // imported files either don't have NoteID's or we ignore them. - size = buf_size; + size = bufsize; - int ret = source->read_event (&delta_t, &size, &buf, ¬e_id_ignored); - - if (size > buf_size) { - buf_size = size; - } + int ret = source->read_event (&delta_t, &size, &buf, &ignored_note_id); if (ret < 0) { // EOT break; @@ -425,76 +496,187 @@ write_midi_data_to_new_files (Evoral::SMF* source, ImportStatus& status, t += delta_t; - if (ret == 0) { // Meta + if (size == 0) { + /* meta event that is not for us */ continue; } - /* if requested by user, each sourcefile gets only a single channel's data */ - if (split_midi_channels) { - uint8_t type = buf[0] & 0xf0; - uint8_t chan = buf[0] & 0x0f; - if (type >= 0x80 && type <= 0xE0) { - if (chan != cur_chan) { - continue; - } - } + if (size > bufsize) { + bufsize = size; } - if (first) { - smfs->mark_streaming_write_started (source_lock); - first = false; - } + if (ret == 0) { // meta event - smfs->append_event_beats( - source_lock, - Evoral::Event( - Evoral::MIDI_EVENT, - Temporal::Beats::ticks_at_rate(t, source->ppqn()), - size, - buf)); + had_meta = true; + + smfs->append_event_beats ( + target_lock, + Evoral::Event( + Evoral::MIDI_EVENT, + Temporal::Beats::ticks_at_rate(t, source->ppqn()), + size, + buf), true); /* allow meta-events */ + } if (status.progress < 0.99) { status.progress += 0.01; } } - if (!first) { + if (had_meta) { + smfs->end_track (target_lock); + } + } - /* we wrote something */ + uint32_t written = 0; - smfs->mark_streaming_write_completed (source_lock, timecnt_t (source->duration())); + if (source->seek_to_track (track+2) == 0) { /* type 1 has metadata in track 1, so the nth real track is track n+2 */ - /* the streaming write that we've just finished - * only wrote data to the SMF object, which is - * ultimately an on-disk data structure. So now - * we pull the data back from disk to build our - * in-memory MidiModel version. - */ + uint64_t t = 0; + uint32_t delta_t = 0; + uint32_t size = 0; - smfs->load_model (source_lock, true); + while (!status.cancel) { + gint note_id_ignored; // imported files either don't have NoteID's or we ignore them. - if (status.cancel) { + size = bufsize; + + int ret = source->read_event (&delta_t, &size, &buf, ¬e_id_ignored); + + if (ret < 0) { // EOT break; } - } else { - info << string_compose (_("Track %1 of %2 contained no usable MIDI data"), i, total_files) << endmsg; - } + t += delta_t; - ++s; // next source + if (size == 0) { + /* meta event, not for us */ + continue; + } - ++cur_chan; - if (cur_chan > 15) { - cur_chan=0; + if (size > bufsize) { + bufsize = size; + } + + if (ret > 0) { // non-meta event + + /* if requested by user, each sourcefile gets only a single channel's data */ + + if (split_midi_channels) { + uint8_t type = buf[0] & 0xf0; + uint8_t chan = buf[0] & 0x0f; + if (type >= 0x80 && type <= 0xE0) { + if (chan != channel) { + continue; + } + } + } + smfs->append_event_beats ( + target_lock, + Evoral::Event( + Evoral::MIDI_EVENT, + Temporal::Beats::ticks_at_rate(t, source->ppqn()), + size, + buf)); + + written++; + } + + if (status.progress < 0.99) { + status.progress += 0.01; + } } + } else { + std::cerr << "could not seek to " << track + 2 << std::endl; + } + + if (had_meta || written) { + + /* we wrote something */ + + smfs->mark_streaming_write_completed (target_lock, timecnt_t (source->duration())); + + /* the streaming write that we've just finished + * only wrote data to the SMF object, which is + * ultimately an on-disk data structure. So now + * we pull the data back from disk to build our + * in-memory MidiModel version. + */ + + smfs->load_model (target_lock, true); + + } else { + info << string_compose (_("Track %1 contained no usable MIDI data"), track) << endmsg; } } catch (exception& e) { error << string_compose (_("MIDI file could not be written (best guess: %1)"), e.what()) << endmsg; } - if (buf) { - free (buf); + free (buf); +} + +static void +write_midi_data_to_new_files (Evoral::SMF* source, ImportStatus& status, + vector >& newsrcs, + bool split_midi_channels) +{ + int track; + int channel; + + status.progress = 0.0f; + size_t nfiles = newsrcs.size(); + size_t n = 0; + + switch (source->smf_format()) { + case 0: + channel = 0; + + for (auto & newsrc : newsrcs) { + std::shared_ptr smfs = std::dynamic_pointer_cast (newsrc); + assert (smfs); + + write_midi_type0_data_to_one_file (source, status, n, nfiles, smfs, split_midi_channels, channel); + + if (split_midi_channels) { + channel = (channel + 1) % 16; + } + + if (status.cancel) { + break; + } + + ++n; + } + break; + + case 1: + track = 0; + channel = 0; + + for (auto & newsrc : newsrcs) { + std::shared_ptr smfs = std::dynamic_pointer_cast (newsrc); + assert (smfs); + + write_midi_type1_data_to_one_file (source, status, smfs, track, split_midi_channels, channel); + + track = (track + 1) % 16; + + if (split_midi_channels) { + channel = (channel + 1) % 16; + } + + if (status.cancel) { + break; + } + + ++n; + } + break; + + default: + error << string_compose (_("MIDI file has unsupported SMF format type %1"), source->smf_format()) << endmsg; + return; } } @@ -535,8 +717,8 @@ Session::deinterlace_midi_region (std::shared_ptr mr) /* create new file paths for 16 potential channels of midi data */ vector smf_names; - for (int i = 0; i<16; i++) { - smf_names.push_back(string_compose("-ch%1", i+1)); + for (int i = 0; i < 16; i++) { + smf_names.push_back (string_compose ("-ch%1", i+1)); } vector new_paths = get_paths_for_new_sources (false, source_path, 16, smf_names, true); @@ -654,8 +836,8 @@ Session::import_files (ImportStatus& status) /* Type0: we should prepare filenames for up to 16 channels in the file; we will throw out the empty ones later */ if (status.split_midi_channels) { num_channels = 16; - for (uint32_t i = 0; i