2 Copyright (C) 2000-2006 Paul Davis
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
25 #include "pbd/file_utils.h"
27 #include "pbd/stateful.h"
29 #include "ardour/region_factory.h"
30 #include "ardour/midi_model.h"
31 #include "ardour/midi_region.h"
32 #include "ardour/midi_source.h"
33 #include "ardour/playlist.h"
34 #include "ardour/region.h"
35 #include "ardour/session_directory.h"
36 #include "ardour/source.h"
37 #include "ardour/source_factory.h"
38 #include "ardour/tempo.h"
40 #include "evoral/Note.hpp"
41 #include "evoral/Sequence.hpp"
46 using namespace ARDOUR;
47 using namespace SessionUtils;
50 session_fail (Session* session)
52 SessionUtils::unload_session(session);
53 SessionUtils::cleanup();
58 write_bbt_source_to_source (boost::shared_ptr<MidiSource> bbt_source, boost::shared_ptr<MidiSource> source,
59 const Glib::Threads::Mutex::Lock& source_lock, const double session_offset)
61 assert (source->empty());
63 const bool old_percussive = bbt_source->model()->percussive();
65 bbt_source->model()->set_percussive (false);
67 source->mark_streaming_midi_write_started (source_lock, bbt_source->model()->note_mode());
69 TempoMap& map (source->session().tempo_map());
71 for (Evoral::Sequence<MidiModel::TimeType>::const_iterator i = bbt_source->model()->begin(MidiModel::TimeType(), true); i != bbt_source->model()->end(); ++i) {
72 const double new_time = map.quarter_note_at_beat ((*i).time().to_double() + map.beat_at_pulse (session_offset)) - (session_offset * 4.0);
73 Evoral::Event<Evoral::Beats> new_ev (*i, true);
74 new_ev.set_time (Evoral::Beats (new_time));
75 source->append_event_beats (source_lock, new_ev);
78 bbt_source->model()->set_percussive (old_percussive);
79 source->mark_streaming_write_completed (source_lock);
84 boost::shared_ptr<MidiSource>
85 ensure_per_region_source (Session* session, boost::shared_ptr<MidiRegion> region, string newsrc_path)
87 boost::shared_ptr<MidiSource> newsrc;
89 /* create a new source if none exists and write corrected events to it.
90 if file exists, assume that it is correct.
92 if (Glib::file_test (newsrc_path, Glib::FILE_TEST_EXISTS)) {
93 Source::Flag flags = Source::Flag (Source::Writable | Source::CanRename);
94 newsrc = boost::dynamic_pointer_cast<MidiSource>(
95 SourceFactory::createExternal(DataType::MIDI, *session,
96 newsrc_path, 1, flags));
98 cout << UTILNAME << "An error occurred creating external source from " << newsrc_path << " exiting." << endl;
99 session_fail (session);
103 XMLNode* node = new XMLNode (newsrc->get_state());
105 if (node->property ("flags") != 0) {
106 node->property ("flags")->set_value (enum_2_string (flags));
109 newsrc->set_state (*node, PBD::Stateful::loading_state_version);
111 cout << UTILNAME << ": Using existing midi source file " << newsrc_path << endl;
112 cout << "for region : " << region->name() << endl;
115 newsrc = boost::dynamic_pointer_cast<MidiSource>(
116 SourceFactory::createWritable(DataType::MIDI, *session,
117 newsrc_path, false, session->frame_rate()));
120 cout << UTILNAME << "An error occurred creating writeable source " << newsrc_path << " exiting." << endl;
121 session_fail (session);
124 if (!newsrc->empty()) {
125 cout << UTILNAME << "An error occurred/ " << newsrc->name() << " is not empty. exiting." << endl;
126 session_fail (session);
129 Source::Lock newsrc_lock (newsrc->mutex());
131 write_bbt_source_to_source (region->midi_source(0), newsrc, newsrc_lock, region->pulse() - (region->start_beats().to_double() / 4.0));
133 cout << UTILNAME << ": Created new midi source file " << newsrc_path << endl;
134 cout << "for region : " << region->name() << endl;
141 boost::shared_ptr<MidiSource>
142 ensure_per_source_source (Session* session, boost::shared_ptr<MidiRegion> region, string newsrc_path)
144 boost::shared_ptr<MidiSource> newsrc;
146 /* create a new source if none exists and write corrected events to it. */
147 if (Glib::file_test (newsrc_path, Glib::FILE_TEST_EXISTS)) {
148 /* flags are ignored for external MIDI source */
149 Source::Flag flags = Source::Flag (Source::Writable | Source::CanRename);
151 newsrc = boost::dynamic_pointer_cast<MidiSource>(
152 SourceFactory::createExternal(DataType::MIDI, *session,
153 newsrc_path, 1, flags));
156 cout << UTILNAME << "An error occurred creating external source from " << newsrc_path << " exiting." << endl;
157 session_fail (session);
160 cout << UTILNAME << ": Using existing midi source file " << newsrc_path << endl;
161 cout << "for source : " << region->midi_source(0)->name() << endl;
164 newsrc = boost::dynamic_pointer_cast<MidiSource>(
165 SourceFactory::createWritable(DataType::MIDI, *session,
166 newsrc_path, false, session->frame_rate()));
168 cout << UTILNAME << "An error occurred creating writeable source " << newsrc_path << " exiting." << endl;
169 session_fail (session);
172 if (!newsrc->empty()) {
173 cout << UTILNAME << "An error occurred/ " << newsrc->name() << " is not empty. exiting." << endl;
174 session_fail (session);
177 Source::Lock newsrc_lock (newsrc->mutex());
179 write_bbt_source_to_source (region->midi_source(0), newsrc, newsrc_lock, region->pulse() - (region->start_beats().to_double() / 4.0));
181 cout << UTILNAME << ": Created new midi source file " << newsrc_path << endl;
182 cout << "for source : " << region->midi_source(0)->name() << endl;
190 reset_start_and_length (Session* session, boost::shared_ptr<MidiRegion> region)
192 /* set start_beats & length_beats to quarter note value */
193 TempoMap& map (session->tempo_map());
195 region->set_start_beats (Evoral::Beats ((map.pulse_at_beat (region->beat())
196 - map.pulse_at_beat (region->beat() - region->start_beats().to_double())) * 4.0));
198 region->set_length_beats (Evoral::Beats ((map.pulse_at_beat (region->beat() + region->length_beats().to_double())
199 - map.pulse_at_beat (region->beat())) * 4.0));
201 cout << UTILNAME << ": Reset start and length beats for region : " << region->name() << endl;
205 apply_one_source_per_region_fix (Session* session)
207 const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
209 if (!region_map.size()) {
213 /* for every midi region, ensure a new source and switch to it. */
214 for (RegionFactory::RegionMap::const_iterator i = region_map.begin(); i != region_map.end(); ++i) {
215 boost::shared_ptr<MidiRegion> mr = 0;
217 if ((mr = boost::dynamic_pointer_cast<MidiRegion>((*i).second)) != 0) {
219 if (!mr->midi_source()->writable()) {
220 /* we know the midi dir is writable, so this region is external. leave it alone*/
221 cout << mr->source()->name() << "is not writable. skipping." << endl;
225 reset_start_and_length (session, mr);
226 string newsrc_filename = mr->name() + "-a54-compat.mid";
227 string newsrc_path = Glib::build_filename (session->session_directory().midi_path(), newsrc_filename);
228 boost::shared_ptr<MidiSource> newsrc = ensure_per_region_source (session, mr, newsrc_path);
229 mr->clobber_sources (newsrc);
237 apply_one_source_per_source_fix (Session* session)
239 const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
241 if (!region_map.size()) {
245 map<PBD::ID, boost::shared_ptr<MidiSource> > old_source_to_new;
246 /* for every midi region, ensure a converted source exists. */
247 for (RegionFactory::RegionMap::const_iterator i = region_map.begin(); i != region_map.end(); ++i) {
248 boost::shared_ptr<MidiRegion> mr = 0;
249 map<PBD::ID, boost::shared_ptr<MidiSource> >::iterator src_it;
251 if ((mr = boost::dynamic_pointer_cast<MidiRegion>((*i).second)) != 0) {
253 if (!mr->midi_source()->writable()) {
254 cout << mr->source()->name() << "is not writable. skipping." << endl;
258 reset_start_and_length (session, mr);
260 if ((src_it = old_source_to_new.find (mr->midi_source()->id())) == old_source_to_new.end()) {
261 string newsrc_filename = mr->source()->name() + "-a54-compat.mid";
262 string newsrc_path = Glib::build_filename (session->session_directory().midi_path(), newsrc_filename);
264 boost::shared_ptr<MidiSource> newsrc = ensure_per_source_source (session, mr, newsrc_path);
266 old_source_to_new.insert (make_pair (mr->midi_source()->id(), newsrc));
268 mr->midi_source(0)->set_name (newsrc->name());
273 /* remove new sources from the session. current snapshot is saved.*/
274 cout << UTILNAME << ": clearing new sources." << endl;
276 for (map<PBD::ID, boost::shared_ptr<MidiSource> >::iterator i = old_source_to_new.begin(); i != old_source_to_new.end(); ++i) {
277 session->remove_source (boost::weak_ptr<MidiSource> ((*i).second));
283 static void usage (int status) {
284 // help2man compatible format (standard GNU help-text)
285 printf (UTILNAME " - convert an ardour session with 5.0 - 5.3 midi sources to be compatible with 5.4.\n\n");
286 printf ("Usage: " UTILNAME " [ OPTIONS ] <session-dir> <snapshot-name>\n\n");
288 -h, --help display this help and exit\n\
289 -f, --force override detection of affected sessions\n\
290 -o, --output <snapshot-name> output session snapshot name (without file suffix)\n\
291 -V, --version print version information and exit\n\
294 This Ardour-specific utility provides an upgrade path for sessions created or modified with Ardour versions 5.0 - 5.3.\n\
295 It creates a 5.4-compatible snapshot from affected Ardour session files.\n\
296 Affected versions (5.0 - 5.3 inclusive) contain a bug which caused some MIDI region properties and contents\n\
297 to be stored incorrectly (see more below).\n\n\
298 The utility will first determine whether or not a session requires any changes for 5.4 compatibility.\n\
299 If a session is determined to be affected by the bug, the program will take one of two approaches to correcting the problem.\n\n\
300 The first is to write a new MIDI source file for every existing MIDI source in the supplied snapshot.\n\
301 In the second approach, each MIDI region have its source converted and placed in the session midifiles directory\n\
302 as a new source (one source file per region).\n\
303 The second method is only offered if the first approach cannot logically ensure that the results would match the input snapshot.\n\
304 Using the first method even if the second method is offered will usually match the input exactly (partly due to a characteristic of the bug).\n\n\
305 Both methods update MIDI region properties and save a new snapshot in the supplied session-dir, optionally using a supplied snapshot name (-o).\n\
306 The new snapshot may be used on Ardour-5.4.\n\n\
307 Running this utility should not alter any existing files, but it is recommended that you run it on a backup of the session directory.\n\n\
309 ardour5-headless-chicken -o bantam ~/studio/leghorn leghorn\n\
310 will create a new snapshot file ~/studio/leghorn/bantam.ardour from ~/studio/leghorn/leghorn.ardour\n\
311 Converted midi sources will be created in ~/studio/leghorn/interchange/leghorn/midifiles/\n\
312 If the output option (-o) is omitted, the string \"-a54-compat\" will be appended to the supplied snapshot name.\n\n\
314 If a session from affected versions used MIDI regions and a meter note divisor was set to anything but quarter notes,\n\
315 the source smf files would contain events at a PPQN value derived from BBT beats (using meter note divisor) rather than quarter-note beatss.\n\
316 The region start and length offsets would also be stored incorrectly.\n\
317 If a MIDI session only contains quarter note meter divisors, it will be unaffected.\n\
320 printf ("Report bugs to <http://tracker.ardour.org/>\n"
321 "Website: <http://ardour.org/>\n");
325 int main (int argc, char* argv[])
330 const char *optstring = "hfo:r:V";
332 const struct option longopts[] = {
333 { "help", 0, 0, 'h' },
334 { "force", 0, 0, 'f' },
335 { "output", 1, 0, 'o' },
336 { "version", 0, 0, 'V' },
340 while (EOF != (c = getopt_long (argc, argv,
341 optstring, longopts, (int *) 0))) {
350 if (outfile.empty()) {
356 printf ("ardour-utils version %s\n\n", VERSIONSTRING);
357 printf ("Copyright (C) GPL 2015 Robin Gareus <robin@gareus.org>\n");
366 usage (EXIT_FAILURE);
371 if (optind + 2 > argc) {
372 usage (EXIT_FAILURE);
375 SessionDirectory* session_dir = new SessionDirectory (argv[optind]);
376 string snapshot_name (argv[optind+1]);
377 string statefile_suffix (X_(".ardour"));
378 string pending_suffix (X_(".pending"));
382 string xmlpath(argv[optind]);
383 string out_snapshot_name;
385 if (!outfile.empty()) {
386 string file_test_path = Glib::build_filename (argv[optind], outfile + statefile_suffix);
387 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
388 cout << UTILNAME << ": session file " << file_test_path << " already exists!" << endl;
389 ::exit (EXIT_FAILURE);
391 out_snapshot_name = outfile;
393 string file_test_path = Glib::build_filename (argv[optind], snapshot_name + "-a54-compat" + statefile_suffix);
394 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
395 cout << UTILNAME << ": session file " << file_test_path << " already exists!" << endl;
396 ::exit (EXIT_FAILURE);
398 out_snapshot_name = snapshot_name + "-a54-compat";
401 xmlpath = Glib::build_filename (xmlpath, legalize_for_path (snapshot_name) + pending_suffix);
403 if (Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
405 /* there is pending state from a crashed capture attempt */
406 cout << UTILNAME << ": There seems to be pending state for snapshot : " << snapshot_name << endl;
410 xmlpath = Glib::build_filename (argv[optind], argv[optind+1]);
412 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
413 xmlpath = Glib::build_filename (argv[optind], legalize_for_path (argv[optind+1]) + ".ardour");
414 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
415 cout << UTILNAME << ": session file " << xmlpath << " doesn't exist!" << endl;
416 ::exit (EXIT_FAILURE);
420 state_tree = new XMLTree;
422 bool writable = PBD::exists_and_writable (xmlpath) && PBD::exists_and_writable(Glib::path_get_dirname(xmlpath));
425 cout << UTILNAME << ": Error : The session directory must exist and be writable." << endl;
429 if (!state_tree->read (xmlpath)) {
430 cout << UTILNAME << ": Could not understand session file " << xmlpath << endl;
433 ::exit (EXIT_FAILURE);
436 XMLNode const & root (*state_tree->root());
438 if (root.name() != X_("Session")) {
439 cout << UTILNAME << ": Session file " << xmlpath<< " is not a session" << endl;
442 ::exit (EXIT_FAILURE);
445 XMLProperty const * prop;
447 if ((prop = root.property ("version")) == 0) {
448 /* no version implies very old version of Ardour */
449 cout << UTILNAME << ": The session " << snapshot_name << " has no version or is too old to be affected. exiting." << endl;
450 ::exit (EXIT_FAILURE);
452 if (prop->value().find ('.') != string::npos) {
453 /* old school version format */
454 cout << UTILNAME << ": The session " << snapshot_name << " is too old to be affected. exiting." << endl;
455 ::exit (EXIT_FAILURE);
457 PBD::Stateful::loading_state_version = atoi (prop->value().c_str());
461 cout << UTILNAME << ": Checking snapshot : " << snapshot_name << " in directory : " << session_dir->root_path() << endl;
463 bool midi_regions_use_bbt_beats = false;
465 if (PBD::Stateful::loading_state_version == 3002 && writable) {
467 if ((child = find_named_node (root, "ProgramVersion")) != 0) {
468 if ((prop = child->property ("modified-with")) != 0) {
469 string modified_with = prop->value ();
471 const double modified_with_version = atof (modified_with.substr ( modified_with.find(" ", 0) + 1, string::npos).c_str());
472 const int modified_with_revision = atoi (modified_with.substr (modified_with.find("-", 0) + 1, string::npos).c_str());
474 if (modified_with_version <= 5.3 && !(modified_with_version == 5.3 && modified_with_revision >= 42)) {
475 midi_regions_use_bbt_beats = true;
482 bool all_metrum_divisors_are_quarters = true;
483 list<double> divisor_list;
485 if ((tm_node = find_named_node (root, "TempoMap")) != 0) {
487 XMLNodeConstIterator niter;
488 metrum = tm_node->children();
489 for (niter = metrum.begin(); niter != metrum.end(); ++niter) {
490 XMLNode* child = *niter;
492 if (child->name() == MeterSection::xml_state_node_name && (prop = child->property ("note-type")) != 0) {
495 if (sscanf (prop->value().c_str(), "%lf", ¬e_type) ==1) {
497 if (note_type != 4.0) {
498 all_metrum_divisors_are_quarters = false;
501 divisor_list.push_back (note_type);
506 cout << UTILNAME << ": Session file " << xmlpath << " has no TempoMap node. exiting." << endl;
507 ::exit (EXIT_FAILURE);
510 if (all_metrum_divisors_are_quarters && !force) {
511 cout << UTILNAME << ": The session " << snapshot_name << " is clear for use in 5.4 (all divisors are quarters). Use -f to override." << endl;
512 ::exit (EXIT_FAILURE);
515 /* check for multiple note divisors. if there is only one, we can create one file per source. */
516 bool one_source_file_per_source = false;
517 divisor_list.unique();
519 if (divisor_list.size() == 1) {
520 cout << UTILNAME << ": Snapshot " << snapshot_name << " will be converted using one new file per source." << endl;
521 cout << "To continue with per-source conversion enter s. q to quit." << endl;
524 cout << "[s/q]" << endl;
527 getline (cin, input);
539 one_source_file_per_source = true;
542 cout << UTILNAME << ": Snapshot " << snapshot_name << " contains multiple meter note divisors." << endl;
543 cout << "per-region source conversion ensures that the output snapshot will be identical to the original," << endl;
544 cout << "however regions in the new snapshot will no longer share sources." << endl;
545 cout << "In many (but not all) cases per-source conversion will work equally well." << endl;
546 cout << "It is recommended that you test a snapshot created with the per-source method before using per-region conversion." << endl;
547 cout << "To continue with per-region conversion enter r. For per-source conversion, enter s. q to quit." << endl;
550 cout << "[r/s/q]" << endl;
553 getline (cin, input);
556 one_source_file_per_source = true;
571 if (midi_regions_use_bbt_beats || force) {
574 cout << UTILNAME << ": Forced update of snapshot : " << snapshot_name << endl;
577 SessionUtils::init();
580 cout << UTILNAME << ": Loading snapshot." << endl;
582 s = SessionUtils::load_session (argv[optind], argv[optind+1]);
584 if (!PBD::exists_and_writable (Glib::path_get_dirname (session_dir->midi_path()))) {
585 cout << UTILNAME << ": the directory " << session_dir->midi_path() << " must be writable. exiting." << endl;
589 /* save new snapshot and prevent alteration of the original by switching to it.
590 we know these files don't yet exist.
592 if (s->save_state (out_snapshot_name, false, true)) {
593 cout << UTILNAME << ": Could not save new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
598 cout << UTILNAME << ": Saved new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
600 if (one_source_file_per_source) {
601 cout << UTILNAME << ": Will create one MIDI file per source." << endl;
603 if (!apply_one_source_per_source_fix (s)) {
604 cout << UTILNAME << ": The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting." << endl;
608 cout << UTILNAME << ": Will create one MIDI file per midi region." << endl;
610 if (!apply_one_source_per_region_fix (s)) {
611 cout << UTILNAME << ": The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting." << endl;
615 if (s->save_state (out_snapshot_name, false, true)) {
616 cout << UTILNAME << ": Could not save snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
619 cout << UTILNAME << ": Saved new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
622 SessionUtils::unload_session(s);
623 SessionUtils::cleanup();
624 cout << UTILNAME << ": Snapshot " << out_snapshot_name << " is ready for use in 5.4" << endl;
626 cout << UTILNAME << ": The snapshot " << snapshot_name << " doesn't require any change for use in 5.4. Use -f to override." << endl;
627 ::exit (EXIT_FAILURE);