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());
62 const bool old_percussive = bbt_source->model()->percussive();
64 bbt_source->model()->set_percussive (false);
66 source->mark_streaming_midi_write_started (source_lock, bbt_source->model()->note_mode());
68 TempoMap& map (source->session().tempo_map());
70 for (Evoral::Sequence<MidiModel::TimeType>::const_iterator i = bbt_source->model()->begin(MidiModel::TimeType(), true); i != bbt_source->model()->end(); ++i) {
71 const double new_time = map.quarter_note_at_beat ((*i).time().to_double() + map.beat_at_pulse (session_offset)) - (session_offset * 4.0);
72 Evoral::Event<Evoral::Beats> new_ev (*i, true);
73 new_ev.set_time (Evoral::Beats (new_time));
74 source->append_event_beats (source_lock, new_ev);
77 bbt_source->model()->set_percussive (old_percussive);
78 source->mark_streaming_write_completed (source_lock);
79 source->set_timeline_position (bbt_source->timeline_position());
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 (Session* session, boost::shared_ptr<MidiRegion> region)
192 /* set start_beats to quarter note value from incorrect bbt*/
193 TempoMap& tmap (session->tempo_map());
194 double new_start_qn = (tmap.pulse_at_beat (region->beat()) - tmap.pulse_at_beat (region->beat() - region->start_beats().to_double())) * 4.0;
196 /* force a change to start and start_beats */
197 PositionLockStyle old_pls = region->position_lock_style();
198 region->set_position_lock_style (AudioTime);
199 region->set_start (tmap.frame_at_quarter_note (new_start_qn) + 1);
200 region->set_start (tmap.frame_at_quarter_note (new_start_qn));
201 region->set_position_lock_style (old_pls);
206 reset_length (Session* session, boost::shared_ptr<MidiRegion> region)
208 /* set start_beats & length_beats to quarter note value */
209 TempoMap& tmap (session->tempo_map());
210 double new_length_qn = (tmap.pulse_at_beat (region->beat() + region->length_beats().to_double())
211 - tmap.pulse_at_beat (region->beat())) * 4.0;
213 /* force a change to length and length_beats */
214 PositionLockStyle old_pls = region->position_lock_style();
215 region->set_position_lock_style (AudioTime);
216 region->set_length (tmap.frame_at_quarter_note (new_length_qn) + 1, 0);
217 region->set_length (tmap.frame_at_quarter_note (new_length_qn), 0);
218 region->set_position_lock_style (old_pls);
222 apply_one_source_per_region_fix (Session* session)
224 const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
226 if (!region_map.size()) {
230 /* for every midi region, ensure a new source and switch to it. */
231 for (RegionFactory::RegionMap::const_iterator i = region_map.begin(); i != region_map.end(); ++i) {
232 boost::shared_ptr<MidiRegion> mr;
234 if ((mr = boost::dynamic_pointer_cast<MidiRegion>((*i).second)) != 0) {
236 if (!mr->midi_source()->writable()) {
237 /* we know the midi dir is writable, so this region is external. leave it alone*/
238 cout << mr->source()->name() << "is not writable. skipping." << endl;
242 reset_start (session, mr);
243 reset_length (session, mr);
245 string newsrc_filename = mr->name() + "-a54-compat.mid";
246 string newsrc_path = Glib::build_filename (session->session_directory().midi_path(), newsrc_filename);
247 boost::shared_ptr<MidiSource> newsrc = ensure_per_region_source (session, mr, newsrc_path);
249 mr->clobber_sources (newsrc);
257 apply_one_source_per_source_fix (Session* session)
259 const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
261 if (!region_map.size()) {
265 map<PBD::ID, boost::shared_ptr<MidiSource> > old_source_to_new;
266 /* for every midi region, ensure a converted source exists. */
267 for (RegionFactory::RegionMap::const_iterator i = region_map.begin(); i != region_map.end(); ++i) {
268 boost::shared_ptr<MidiRegion> mr;
269 map<PBD::ID, boost::shared_ptr<MidiSource> >::iterator src_it;
271 if ((mr = boost::dynamic_pointer_cast<MidiRegion>((*i).second)) != 0) {
273 if (!mr->midi_source()->writable()) {
274 cout << mr->source()->name() << "is not writable. skipping." << endl;
278 reset_start (session, mr);
279 reset_length (session, mr);
281 if ((src_it = old_source_to_new.find (mr->midi_source()->id())) == old_source_to_new.end()) {
282 string newsrc_filename = mr->source()->name() + "-a54-compat.mid";
283 string newsrc_path = Glib::build_filename (session->session_directory().midi_path(), newsrc_filename);
285 boost::shared_ptr<MidiSource> newsrc = ensure_per_source_source (session, mr, newsrc_path);
287 old_source_to_new.insert (make_pair (mr->midi_source()->id(), newsrc));
289 mr->midi_source(0)->set_name (newsrc->name());
294 /* remove new sources from the session. current snapshot is saved.*/
295 cout << UTILNAME << ": clearing new sources." << endl;
297 for (map<PBD::ID, boost::shared_ptr<MidiSource> >::iterator i = old_source_to_new.begin(); i != old_source_to_new.end(); ++i) {
298 session->remove_source (boost::weak_ptr<MidiSource> ((*i).second));
304 static void usage (int status) {
305 // help2man compatible format (standard GNU help-text)
306 printf (UTILNAME " - convert an ardour session with 5.0 - 5.3 midi sources to be compatible with 5.4.\n\n");
307 printf ("Usage: " UTILNAME " [ OPTIONS ] <session-dir> <snapshot-name>\n\n");
309 -h, --help display this help and exit\n\
310 -f, --force override detection of affected sessions\n\
311 -o, --output <snapshot-name> output session snapshot name (without file suffix)\n\
312 -V, --version print version information and exit\n\
315 This Ardour-specific utility provides an upgrade path for sessions created or modified with Ardour versions 5.0 - 5.3.\n\
316 It creates a 5.4-compatible snapshot from affected Ardour session files.\n\
317 Affected versions (5.0 - 5.3 inclusive) contain a bug which caused some MIDI region properties and contents\n\
318 to be stored incorrectly (see more below).\n\n\
319 The utility will first determine whether or not a session requires any changes for 5.4 compatibility.\n\
320 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\
321 The first is to write a new MIDI source file for every existing MIDI source in the supplied snapshot.\n\
322 In the second approach, each MIDI region have its source converted and placed in the session midifiles directory\n\
323 as a new source (one source file per region).\n\
324 The second method is only offered if the first approach cannot logically ensure that the results would match the input snapshot.\n\
325 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\
326 Both methods update MIDI region properties and save a new snapshot in the supplied session-dir, optionally using a supplied snapshot name (-o).\n\
327 The new snapshot may be used on Ardour-5.4.\n\n\
328 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\
330 ardour5-headless-chicken -o bantam ~/studio/leghorn leghorn\n\
331 will create a new snapshot file ~/studio/leghorn/bantam.ardour from ~/studio/leghorn/leghorn.ardour\n\
332 Converted midi sources will be created in ~/studio/leghorn/interchange/leghorn/midifiles/\n\
333 If the output option (-o) is omitted, the string \"-a54-compat\" will be appended to the supplied snapshot name.\n\n\
335 If a session from affected versions used MIDI regions and a meter note divisor was set to anything but quarter notes,\n\
336 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\
337 The region start and length offsets would also be stored incorrectly.\n\
338 If a MIDI session only contains quarter note meter divisors, it will be unaffected.\n\
341 printf ("Report bugs to <http://tracker.ardour.org/>\n"
342 "Website: <http://ardour.org/>\n");
346 int main (int argc, char* argv[])
351 const char *optstring = "hfo:r:V";
353 const struct option longopts[] = {
354 { "help", 0, 0, 'h' },
355 { "force", 0, 0, 'f' },
356 { "output", 1, 0, 'o' },
357 { "version", 0, 0, 'V' },
361 while (EOF != (c = getopt_long (argc, argv,
362 optstring, longopts, (int *) 0))) {
371 if (outfile.empty()) {
377 printf ("ardour-utils version %s\n\n", VERSIONSTRING);
378 printf ("Copyright (C) GPL 2015 Robin Gareus <robin@gareus.org>\n");
387 usage (EXIT_FAILURE);
392 if (optind + 2 > argc) {
393 usage (EXIT_FAILURE);
396 SessionDirectory* session_dir = new SessionDirectory (argv[optind]);
397 string snapshot_name (argv[optind+1]);
398 string statefile_suffix (X_(".ardour"));
399 string pending_suffix (X_(".pending"));
403 string xmlpath(argv[optind]);
404 string out_snapshot_name;
406 if (!outfile.empty()) {
407 string file_test_path = Glib::build_filename (argv[optind], outfile + statefile_suffix);
408 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
409 cout << UTILNAME << ": session file " << file_test_path << " already exists!" << endl;
410 ::exit (EXIT_FAILURE);
412 out_snapshot_name = outfile;
414 string file_test_path = Glib::build_filename (argv[optind], snapshot_name + "-a54-compat" + statefile_suffix);
415 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
416 cout << UTILNAME << ": session file " << file_test_path << " already exists!" << endl;
417 ::exit (EXIT_FAILURE);
419 out_snapshot_name = snapshot_name + "-a54-compat";
422 xmlpath = Glib::build_filename (xmlpath, legalize_for_path (snapshot_name) + pending_suffix);
424 if (Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
426 /* there is pending state from a crashed capture attempt */
427 cout << UTILNAME << ": There seems to be pending state for snapshot : " << snapshot_name << endl;
431 xmlpath = Glib::build_filename (argv[optind], argv[optind+1]);
433 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
434 xmlpath = Glib::build_filename (argv[optind], legalize_for_path (argv[optind+1]) + ".ardour");
435 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
436 cout << UTILNAME << ": session file " << xmlpath << " doesn't exist!" << endl;
437 ::exit (EXIT_FAILURE);
441 state_tree = new XMLTree;
443 bool writable = PBD::exists_and_writable (xmlpath) && PBD::exists_and_writable(Glib::path_get_dirname(xmlpath));
446 cout << UTILNAME << ": Error : The session directory must exist and be writable." << endl;
450 if (!PBD::exists_and_writable (Glib::path_get_dirname (session_dir->midi_path()))) {
451 cout << UTILNAME << ": Error : The session midi directory " << session_dir->midi_path() << " must be writable. exiting." << endl;
452 ::exit (EXIT_FAILURE);
455 if (!state_tree->read (xmlpath)) {
456 cout << UTILNAME << ": Could not understand session file " << xmlpath << endl;
459 ::exit (EXIT_FAILURE);
462 XMLNode const & root (*state_tree->root());
464 if (root.name() != X_("Session")) {
465 cout << UTILNAME << ": Session file " << xmlpath<< " is not a session" << endl;
468 ::exit (EXIT_FAILURE);
471 XMLProperty const * prop;
473 if ((prop = root.property ("version")) == 0) {
474 /* no version implies very old version of Ardour */
475 cout << UTILNAME << ": The session " << snapshot_name << " has no version or is too old to be affected. exiting." << endl;
476 ::exit (EXIT_FAILURE);
478 if (prop->value().find ('.') != string::npos) {
479 /* old school version format */
480 cout << UTILNAME << ": The session " << snapshot_name << " is too old to be affected. exiting." << endl;
481 ::exit (EXIT_FAILURE);
483 PBD::Stateful::loading_state_version = atoi (prop->value().c_str());
487 cout << UTILNAME << ": Checking snapshot : " << snapshot_name << " in directory : " << session_dir->root_path() << endl;
489 bool midi_regions_use_bbt_beats = false;
491 if (PBD::Stateful::loading_state_version == 3002 && writable) {
493 if ((child = find_named_node (root, "ProgramVersion")) != 0) {
494 if ((prop = child->property ("modified-with")) != 0) {
495 string modified_with = prop->value ();
497 const double modified_with_version = atof (modified_with.substr ( modified_with.find(" ", 0) + 1, string::npos).c_str());
498 const int modified_with_revision = atoi (modified_with.substr (modified_with.find("-", 0) + 1, string::npos).c_str());
500 if (modified_with_version <= 5.3 && !(modified_with_version == 5.3 && modified_with_revision >= 42)) {
501 midi_regions_use_bbt_beats = true;
508 bool all_metrum_divisors_are_quarters = true;
509 list<double> divisor_list;
511 if ((tm_node = find_named_node (root, "TempoMap")) != 0) {
513 XMLNodeConstIterator niter;
514 metrum = tm_node->children();
515 for (niter = metrum.begin(); niter != metrum.end(); ++niter) {
516 XMLNode* child = *niter;
518 if (child->name() == MeterSection::xml_state_node_name && (prop = child->property ("note-type")) != 0) {
521 if (sscanf (prop->value().c_str(), "%lf", ¬e_type) ==1) {
523 if (note_type != 4.0) {
524 all_metrum_divisors_are_quarters = false;
527 divisor_list.push_back (note_type);
532 cout << UTILNAME << ": Session file " << xmlpath << " has no TempoMap node. exiting." << endl;
533 ::exit (EXIT_FAILURE);
536 if (all_metrum_divisors_are_quarters && !force) {
537 cout << UTILNAME << ": The session " << snapshot_name << " is clear for use in 5.4 (all divisors are quarters). Use -f to override." << endl;
538 ::exit (EXIT_FAILURE);
541 /* check for multiple note divisors. if there is only one, we can create one file per source. */
542 bool one_source_file_per_source = false;
543 divisor_list.unique();
545 if (divisor_list.size() == 1) {
546 cout << UTILNAME << ": Snapshot " << snapshot_name << " will be converted using one new file per source." << endl;
547 cout << "To continue with per-source conversion enter s. q to quit." << endl;
550 cout << "[s/q]" << endl;
553 getline (cin, input);
565 one_source_file_per_source = true;
568 cout << UTILNAME << ": Snapshot " << snapshot_name << " contains multiple meter note divisors." << endl;
569 cout << "per-region source conversion ensures that the output snapshot will be identical to the original," << endl;
570 cout << "however regions in the new snapshot will no longer share sources." << endl;
571 cout << "In many (but not all) cases per-source conversion will work equally well." << endl;
572 cout << "It is recommended that you test a snapshot created with the per-source method before using per-region conversion." << endl;
573 cout << "To continue with per-region conversion enter r. For per-source conversion, enter s. q to quit." << endl;
576 cout << "[r/s/q]" << endl;
579 getline (cin, input);
582 one_source_file_per_source = true;
597 if (midi_regions_use_bbt_beats || force) {
600 cout << UTILNAME << ": Forced update of snapshot : " << snapshot_name << endl;
603 SessionUtils::init();
606 cout << UTILNAME << ": Loading snapshot." << endl;
608 s = SessionUtils::load_session (argv[optind], argv[optind+1]);
610 /* save new snapshot and prevent alteration of the original by switching to it.
611 we know these files don't yet exist.
613 if (s->save_state (out_snapshot_name, false, true)) {
614 cout << UTILNAME << ": Could not save new 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;
621 if (one_source_file_per_source) {
622 cout << UTILNAME << ": Will create one MIDI file per source." << endl;
624 if (!apply_one_source_per_source_fix (s)) {
625 cout << UTILNAME << ": The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting." << endl;
629 cout << UTILNAME << ": Will create one MIDI file per midi region." << endl;
631 if (!apply_one_source_per_region_fix (s)) {
632 cout << UTILNAME << ": The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting." << endl;
636 if (s->save_state (out_snapshot_name, false, true)) {
637 cout << UTILNAME << ": Could not save snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
640 cout << UTILNAME << ": Saved new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
643 SessionUtils::unload_session(s);
644 SessionUtils::cleanup();
645 cout << UTILNAME << ": Snapshot " << out_snapshot_name << " is ready for use in 5.4" << endl;
647 cout << UTILNAME << ": The snapshot " << snapshot_name << " doesn't require any change for use in 5.4. Use -f to override." << endl;
648 ::exit (EXIT_FAILURE);