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_quarter_note (session_offset * 4.0)) - (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 << ":" << endl
99 << " An error occurred creating external source from " << newsrc_path << " exiting." << endl;
100 session_fail (session);
104 XMLNode* node = new XMLNode (newsrc->get_state());
106 if (node->property ("flags") != 0) {
107 node->property ("flags")->set_value (enum_2_string (flags));
110 newsrc->set_state (*node, PBD::Stateful::loading_state_version);
114 cout << UTILNAME << ":" << endl
115 << " Using existing midi source file" << endl
116 << " " << newsrc_path << endl
117 << " for region " << region->name() << endl;
120 newsrc = boost::dynamic_pointer_cast<MidiSource>(
121 SourceFactory::createWritable(DataType::MIDI, *session,
122 newsrc_path, false, session->frame_rate()));
125 cout << UTILNAME << ":" << endl
126 << " An error occurred creating writeable source " << newsrc_path << " exiting." << endl;
127 session_fail (session);
130 if (!newsrc->empty()) {
131 cout << UTILNAME << ":" << endl
132 << " An error occurred/ " << newsrc->name() << " is not empty. exiting." << endl;
133 session_fail (session);
136 Source::Lock newsrc_lock (newsrc->mutex());
138 write_bbt_source_to_source (region->midi_source(0), newsrc, newsrc_lock, (region->quarter_note() - region->start_beats()) / 4.0);
140 cout << UTILNAME << ":" << endl
141 << " Created new midi source file" << endl
142 << " " << newsrc_path << endl
143 << " for region " << region->name() << endl;
149 boost::shared_ptr<MidiSource>
150 ensure_per_source_source (Session* session, boost::shared_ptr<MidiRegion> region, string newsrc_path)
152 boost::shared_ptr<MidiSource> newsrc;
154 /* create a new source if none exists and write corrected events to it. */
155 if (Glib::file_test (newsrc_path, Glib::FILE_TEST_EXISTS)) {
156 /* flags are ignored for external MIDI source */
157 Source::Flag flags = Source::Flag (Source::Writable | Source::CanRename);
159 newsrc = boost::dynamic_pointer_cast<MidiSource>(
160 SourceFactory::createExternal(DataType::MIDI, *session,
161 newsrc_path, 1, flags));
164 cout << UTILNAME << ":" << endl
165 << " An error occurred creating external source from " << newsrc_path << " exiting." << endl;
166 session_fail (session);
169 cout << UTILNAME << ":" << endl
170 << " Using existing midi source file" << endl
171 << " " << newsrc_path << endl
172 << " for source " << region->midi_source(0)->name() << endl;
175 newsrc = boost::dynamic_pointer_cast<MidiSource>(
176 SourceFactory::createWritable(DataType::MIDI, *session,
177 newsrc_path, false, session->frame_rate()));
179 cout << UTILNAME << ":" << endl
180 <<" An error occurred creating writeable source " << newsrc_path << " exiting." << endl;
181 session_fail (session);
184 if (!newsrc->empty()) {
185 cout << UTILNAME << ":" << endl
186 << " An error occurred/ " << newsrc->name() << " is not empty. exiting." << endl;
187 session_fail (session);
190 Source::Lock newsrc_lock (newsrc->mutex());
192 write_bbt_source_to_source (region->midi_source(0), newsrc, newsrc_lock, (region->quarter_note() - region->start_beats()) / 4.0);
194 cout << UTILNAME << ":" << endl
195 << " Created new midi source file" << endl
196 << " " << newsrc_path << endl
197 << " for source " << region->midi_source(0)->name() << endl;
205 reset_start (Session* session, boost::shared_ptr<MidiRegion> region)
207 /* set start_beats to quarter note value from incorrect bbt*/
208 TempoMap& tmap (session->tempo_map());
209 double new_start_qn = tmap.quarter_note_at_beat (region->beat()) - tmap.quarter_note_at_beat (region->beat() - region->start_beats());
211 /* force a change to start and start_beats */
212 PositionLockStyle old_pls = region->position_lock_style();
213 region->set_position_lock_style (AudioTime);
214 region->set_start (tmap.frame_at_quarter_note (region->quarter_note()) - tmap.frame_at_quarter_note (region->quarter_note() - new_start_qn) + 1);
215 region->set_start (tmap.frame_at_quarter_note (region->quarter_note()) - tmap.frame_at_quarter_note (region->quarter_note() - new_start_qn));
216 region->set_position_lock_style (old_pls);
221 reset_length (Session* session, boost::shared_ptr<MidiRegion> region)
223 /* set length_beats to quarter note value */
224 TempoMap& tmap (session->tempo_map());
225 double new_length_qn = tmap.quarter_note_at_beat (region->beat() + region->length_beats())
226 - tmap.quarter_note_at_beat (region->beat());
228 /* force a change to length and length_beats */
229 PositionLockStyle old_pls = region->position_lock_style();
230 region->set_position_lock_style (AudioTime);
231 region->set_length (tmap.frame_at_quarter_note (region->quarter_note() + new_length_qn) + 1 - region->position(), 0);
232 region->set_length (tmap.frame_at_quarter_note (region->quarter_note() + new_length_qn)- region->position(), 0);
233 region->set_position_lock_style (old_pls);
237 apply_one_source_per_region_fix (Session* session)
239 const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
241 if (!region_map.size()) {
245 list<boost::shared_ptr<MidiSource> > old_source_list;
247 /* set start and length for every midi region. ensure a new converted source exists and switch to it. */
248 for (RegionFactory::RegionMap::const_iterator i = region_map.begin(); i != region_map.end(); ++i) {
249 boost::shared_ptr<MidiRegion> mr;
251 if ((mr = boost::dynamic_pointer_cast<MidiRegion>((*i).second)) != 0) {
253 if (!mr->midi_source()->writable()) {
254 /* we know the midi dir is writable, so this region is external. leave it alone*/
255 cout << mr->source()->name() << "is not writable. skipping." << endl;
259 old_source_list.push_back (mr->midi_source());
261 reset_start (session, mr);
262 reset_length (session, mr);
264 string newsrc_filename = mr->name() + "-a54-compat.mid";
265 string newsrc_path = Glib::build_filename (session->session_directory().midi_path(), newsrc_filename);
267 boost::shared_ptr<MidiSource> newsrc = ensure_per_region_source (session, mr, newsrc_path);
269 mr->clobber_sources (newsrc);
273 old_source_list.unique();
275 /* remove old sources from the session. current snapshot is saved.*/
276 cout << UTILNAME << ":" << endl
277 << " clearing old sources." << endl;
279 for (list<boost::shared_ptr<MidiSource> >::iterator i = old_source_list.begin(); i != old_source_list.end(); ++i) {
280 session->remove_source (boost::weak_ptr<MidiSource> (*i));
287 apply_one_source_per_source_fix (Session* session)
289 const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
291 if (!region_map.size()) {
295 map<PBD::ID, boost::shared_ptr<MidiSource> > old_source_to_new;
296 /* reset every midi region's start and length. ensure its corrected source exists. */
297 for (RegionFactory::RegionMap::const_iterator i = region_map.begin(); i != region_map.end(); ++i) {
298 boost::shared_ptr<MidiRegion> mr;
299 map<PBD::ID, boost::shared_ptr<MidiSource> >::iterator src_it;
301 if ((mr = boost::dynamic_pointer_cast<MidiRegion>((*i).second)) != 0) {
303 if (!mr->midi_source()->writable()) {
304 cout << mr->source()->name() << "is not writable. skipping." << endl;
308 reset_start (session, mr);
309 reset_length (session, mr);
311 if ((src_it = old_source_to_new.find (mr->midi_source()->id())) == old_source_to_new.end()) {
312 string newsrc_filename = mr->source()->name() + "-a54-compat.mid";
313 string newsrc_path = Glib::build_filename (session->session_directory().midi_path(), newsrc_filename);
315 boost::shared_ptr<MidiSource> newsrc = ensure_per_source_source (session, mr, newsrc_path);
317 old_source_to_new.insert (make_pair (mr->midi_source()->id(), newsrc));
319 mr->midi_source(0)->set_name (newsrc->name());
324 /* remove new sources from the session. current snapshot is saved.*/
325 cout << UTILNAME << ":" << endl
326 << " clearing new sources." << endl;
328 for (map<PBD::ID, boost::shared_ptr<MidiSource> >::iterator i = old_source_to_new.begin(); i != old_source_to_new.end(); ++i) {
329 session->remove_source (boost::weak_ptr<MidiSource> ((*i).second));
335 static void usage (int status) {
336 // help2man compatible format (standard GNU help-text)
337 printf (UTILNAME " - convert an ardour session with 5.0 - 5.3 midi sources to be compatible with 5.4.\n\n");
338 printf ("Usage: " UTILNAME " [ OPTIONS ] <session-dir> <snapshot-name>\n\n");
340 -h, --help display this help and exit\n\
341 -f, --force override detection of affected sessions\n\
342 -o, --output <snapshot-name> output session snapshot name (without file suffix)\n\
343 -V, --version print version information and exit\n\
346 This Ardour-specific utility provides an upgrade path for sessions created or\n\
347 modified with Ardour versions 5.0 - 5.3.\n\
348 It creates a 5.4-compatible snapshot from affected Ardour session files.\n\
349 Affected versions (5.0 - 5.3 inclusive) contain a bug which caused some\n\
350 MIDI region properties and contents to be stored incorrectly\n\
351 (see more below).\n\n\
352 The utility will first determine whether or not a session requires any\n\
353 changes for 5.4 compatibility.\n\
354 If a session is determined to be affected by the bug, the program will take\n\
355 one of two approaches to correcting the problem.\n\n\
356 The first is to write a new MIDI source file for every existing MIDI source\n\
357 in the supplied snapshot.\n\
358 In the second approach, each MIDI region have its source converted and placed\n\
359 in the session midifiles directory as a new source\n\
360 (one source file per region).\n\
361 The second method is only offered if the first approach cannot logically ensure\n\
362 that the results would match the input snapshot.\n\
363 Using the first method even if the second method is offered\n\
364 will usually match the input exactly\n\
365 (partly due to a characteristic of the bug).\n\n\
366 Both methods update MIDI region properties and save a new snapshot in the\n\
367 supplied session-dir, optionally using a supplied snapshot name (-o).\n\
368 The new snapshot may be used on Ardour-5.4.\n\n\
369 Running this utility should not alter any existing files,\n\
370 but it is recommended that you run it on a backup of the session directory.\n\n\
372 ardour5-fix_bbtppq -o bantam ~/studio/leghorn leghorn\n\
373 will create a new snapshot file ~/studio/leghorn/bantam.ardour from\n\
374 ~/studio/leghorn/leghorn.ardour\n\
375 Converted midi sources will be created in\n\
376 ~/studio/leghorn/interchange/leghorn/midifiles/\n\
377 If the output option (-o) is omitted, the string \"-a54-compat\"\n\
378 will be appended to the supplied snapshot name.\n\n\
380 If a session from affected versions used MIDI regions and a meter note divisor\n\
381 was set to anything but quarter notes, the source smf files would contain events\n\
382 at a PPQN value derived from BBT beats (using meter note divisor)\n\
383 rather than quarter-note beats.\n\
384 The region start and length offsets would also be stored incorrectly.\n\
385 If a MIDI session only contains quarter note meter divisors, it will be unaffected.\n\
388 printf ("Report bugs to <http://tracker.ardour.org/>\n"
389 "Website: <http://ardour.org/>\n");
393 int main (int argc, char* argv[])
398 const char *optstring = "hfo:r:V";
400 const struct option longopts[] = {
401 { "help", 0, 0, 'h' },
402 { "force", 0, 0, 'f' },
403 { "output", 1, 0, 'o' },
404 { "version", 0, 0, 'V' },
408 while (EOF != (c = getopt_long (argc, argv,
409 optstring, longopts, (int *) 0))) {
418 if (outfile.empty()) {
424 printf ("ardour-utils version %s\n\n", VERSIONSTRING);
425 printf ("Copyright (C) GPL 2015 Robin Gareus <robin@gareus.org>\n");
434 usage (EXIT_FAILURE);
439 if (optind + 2 > argc) {
440 usage (EXIT_FAILURE);
443 SessionDirectory* session_dir = new SessionDirectory (argv[optind]);
444 string snapshot_name (argv[optind+1]);
445 string statefile_suffix (X_(".ardour"));
446 string pending_suffix (X_(".pending"));
450 string xmlpath(argv[optind]);
451 string out_snapshot_name;
453 if (!outfile.empty()) {
454 string file_test_path = Glib::build_filename (argv[optind], outfile + statefile_suffix);
455 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
456 cout << UTILNAME << ":" << endl
457 << " session file " << file_test_path << " already exists!" << endl;
458 ::exit (EXIT_FAILURE);
460 out_snapshot_name = outfile;
462 string file_test_path = Glib::build_filename (argv[optind], snapshot_name + "-a54-compat" + statefile_suffix);
463 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
464 cout << UTILNAME << ":" << endl
465 << " session file " << file_test_path << " already exists!" << endl;
466 ::exit (EXIT_FAILURE);
468 out_snapshot_name = snapshot_name + "-a54-compat";
471 xmlpath = Glib::build_filename (xmlpath, legalize_for_path (snapshot_name) + pending_suffix);
473 if (Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
475 /* there is pending state from a crashed capture attempt */
476 cout << UTILNAME << ":" << endl
477 << " There seems to be pending state for snapshot : " << snapshot_name << endl;
481 xmlpath = Glib::build_filename (argv[optind], argv[optind+1]);
483 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
484 xmlpath = Glib::build_filename (argv[optind], legalize_for_path (argv[optind+1]) + ".ardour");
485 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
486 cout << UTILNAME << ":" << endl
487 << " session file " << xmlpath << " doesn't exist!" << endl;
488 ::exit (EXIT_FAILURE);
492 state_tree = new XMLTree;
494 bool writable = PBD::exists_and_writable (xmlpath) && PBD::exists_and_writable(Glib::path_get_dirname(xmlpath));
497 cout << UTILNAME << ":" << endl
498 << " Error : The session directory must exist and be writable." << endl;
502 if (!PBD::exists_and_writable (Glib::path_get_dirname (session_dir->midi_path()))) {
503 cout << UTILNAME << ":" << endl
504 << " Error : The session midi directory " << session_dir->midi_path() << " must be writable. exiting." << endl;
505 ::exit (EXIT_FAILURE);
508 if (!state_tree->read (xmlpath)) {
509 cout << UTILNAME << ":" << endl
510 << " Could not understand session file " << xmlpath << endl;
513 ::exit (EXIT_FAILURE);
516 XMLNode const & root (*state_tree->root());
518 if (root.name() != X_("Session")) {
519 cout << UTILNAME << ":" << endl
520 << " Session file " << xmlpath<< " is not a session" << endl;
523 ::exit (EXIT_FAILURE);
526 XMLProperty const * prop;
528 if ((prop = root.property ("version")) == 0) {
529 /* no version implies very old version of Ardour */
530 cout << UTILNAME << ":" << endl
531 << " The session " << snapshot_name << " has no version or is too old to be affected. exiting." << endl;
532 ::exit (EXIT_FAILURE);
534 if (prop->value().find ('.') != string::npos) {
535 /* old school version format */
536 cout << UTILNAME << ":" << endl
537 << " The session " << snapshot_name << " is too old to be affected. exiting." << endl;
538 ::exit (EXIT_FAILURE);
540 PBD::Stateful::loading_state_version = atoi (prop->value().c_str());
544 cout << UTILNAME << ":" << endl
545 << " Checking snapshot : " << snapshot_name << " in directory : " << session_dir->root_path() << endl;
547 bool midi_regions_use_bbt_beats = false;
549 if (PBD::Stateful::loading_state_version == 3002 && writable) {
551 if ((child = find_named_node (root, "ProgramVersion")) != 0) {
552 if ((prop = child->property ("modified-with")) != 0) {
553 string modified_with = prop->value ();
555 const double modified_with_version = atof (modified_with.substr ( modified_with.find(" ", 0) + 1, string::npos).c_str());
556 const int modified_with_revision = atoi (modified_with.substr (modified_with.find("-", 0) + 1, string::npos).c_str());
558 if (modified_with_version <= 5.3 && !(modified_with_version == 5.3 && modified_with_revision >= 42)) {
559 midi_regions_use_bbt_beats = true;
566 bool all_metrum_divisors_are_quarters = true;
567 list<double> divisor_list;
569 if ((tm_node = find_named_node (root, "TempoMap")) != 0) {
571 XMLNodeConstIterator niter;
572 metrum = tm_node->children();
573 for (niter = metrum.begin(); niter != metrum.end(); ++niter) {
574 XMLNode* child = *niter;
576 if (child->name() == MeterSection::xml_state_node_name && (prop = child->property ("note-type")) != 0) {
579 if (sscanf (prop->value().c_str(), "%lf", ¬e_type) ==1) {
581 if (note_type != 4.0) {
582 all_metrum_divisors_are_quarters = false;
585 divisor_list.push_back (note_type);
590 cout << UTILNAME << ":" << endl
591 << " Session file " << xmlpath << " has no TempoMap node. exiting." << endl;
592 ::exit (EXIT_FAILURE);
595 if (all_metrum_divisors_are_quarters && !force) {
596 cout << UTILNAME << ":" << endl
597 << " The session " << snapshot_name << " is clear for use in 5.4 (all divisors are quarters). Use -f to override." << endl;
598 ::exit (EXIT_FAILURE);
601 /* check for multiple note divisors. if there is only one, we can create one file per source. */
602 bool one_source_file_per_source = false;
603 divisor_list.unique();
605 if (divisor_list.size() == 1) {
606 cout << endl << UTILNAME << ":" << endl
607 << " Snapshot " << snapshot_name << " will be converted using one new file per source." << endl
608 << " To continue with per-source conversion enter s. q to quit." << endl;
611 cout << " [s/q]" << endl;
614 getline (cin, input);
626 one_source_file_per_source = true;
629 cout << endl << UTILNAME << ":" << endl
630 << " Snapshot " << snapshot_name << " contains multiple meter note divisors." << endl
631 << " Per-region source conversion ensures that the output snapshot will be identical to the original," << endl
632 << " however regions in the new snapshot will no longer share sources." << endl << endl
633 << " In many (but not all) cases per-source conversion will work equally well." << endl
634 << " It is recommended that you test a snapshot created with the per-source method before using per-region conversion." << endl << endl
635 << " To continue with per-region conversion enter r. For per-source conversion, enter s. q to quit." << endl;
638 cout << " [r/s/q]" << endl;
641 getline (cin, input);
644 one_source_file_per_source = true;
659 if (midi_regions_use_bbt_beats || force) {
662 cout << UTILNAME << ":" << endl
663 << " Forced update of snapshot : " << snapshot_name << endl;
666 SessionUtils::init();
669 cout << UTILNAME << ":" << endl
670 << " Loading snapshot " << snapshot_name << endl;
672 s = SessionUtils::load_session (argv[optind], argv[optind+1]);
674 /* save new snapshot and prevent alteration of the original by switching to it.
675 we know these files don't yet exist.
677 if (s->save_state (out_snapshot_name, false, true)) {
678 cout << UTILNAME << ":" << endl
679 << " Could not save new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
684 cout << UTILNAME << ":" << endl
685 << " Saved new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
687 if (one_source_file_per_source) {
688 cout << UTILNAME << ":" << endl
689 << " Will create one MIDI file per source." << endl;
691 if (!apply_one_source_per_source_fix (s)) {
692 cout << UTILNAME << ":" << endl
693 << " The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting." << endl;
697 cout << UTILNAME << ":" << endl
698 << " Will create one MIDI file per midi region." << endl;
700 if (!apply_one_source_per_region_fix (s)) {
701 cout << UTILNAME << ":" << endl
702 << " The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting." << endl;
706 if (s->save_state (out_snapshot_name, false, true)) {
707 cout << UTILNAME << ":" << endl
708 << " Could not save snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
711 cout << UTILNAME << ":" << endl
712 << " Saved new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
715 SessionUtils::unload_session(s);
716 SessionUtils::cleanup();
717 cout << UTILNAME << ":" << endl
718 << " Snapshot " << out_snapshot_name << " is ready for use in 5.4" << endl;
720 cout << UTILNAME << ":" << endl
721 << " The snapshot " << snapshot_name << " doesn't require any change for use in 5.4. Use -f to override." << endl;
722 ::exit (EXIT_FAILURE);