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 const bool old_percussive = bbt_source->model()->percussive();
63 bbt_source->model()->set_percussive (false);
65 source->mark_streaming_midi_write_started (source_lock, bbt_source->model()->note_mode());
67 TempoMap& map (source->session().tempo_map());
69 for (Evoral::Sequence<MidiModel::TimeType>::const_iterator i = bbt_source->model()->begin(MidiModel::TimeType(), true); i != bbt_source->model()->end(); ++i) {
70 const double new_time = map.quarter_note_at_beat ((*i).time().to_double() + map.beat_at_pulse (session_offset)) - (session_offset * 4.0);
71 Evoral::Event<Evoral::Beats> new_ev (*i, true);
72 new_ev.set_time (Evoral::Beats (new_time));
73 source->append_event_beats (source_lock, new_ev);
76 bbt_source->model()->set_percussive (old_percussive);
77 source->mark_streaming_write_completed (source_lock);
82 boost::shared_ptr<MidiSource>
83 ensure_per_region_source (Session* session, std::string newsrc_path, boost::shared_ptr<MidiRegion> region)
85 boost::shared_ptr<MidiSource> newsrc;
87 /* create a new source if none exists and write corrected events to it.
88 if file exists, assume that it is correct.
90 if (Glib::file_test (newsrc_path, Glib::FILE_TEST_EXISTS)) {
91 Source::Flag flags = Source::Flag (Source::Writable | Source::CanRename);
92 newsrc = boost::dynamic_pointer_cast<MidiSource>(
93 SourceFactory::createExternal(DataType::MIDI, *session,
94 newsrc_path, 1, flags));
96 std::cout << UTILNAME << "An error occurred creating external source from " << newsrc_path << " exiting." << std::endl;
97 session_fail (session);
101 XMLNode* node = new XMLNode (newsrc->get_state());
103 if (node->property ("flags") != 0) {
104 node->property ("flags")->set_value (enum_2_string (flags));
107 newsrc->set_state (*node, PBD::Stateful::loading_state_version);
109 std::cout << UTILNAME << ": Using existing midi source file : " << newsrc_path << std::endl;
110 std::cout << "for region : " << region->name() << std::endl;
113 newsrc = boost::dynamic_pointer_cast<MidiSource>(
114 SourceFactory::createWritable(DataType::MIDI, *session,
115 newsrc_path, false, session->frame_rate()));
118 std::cout << UTILNAME << "An error occurred creating writeable source " << newsrc_path << " exiting." << std::endl;
119 session_fail (session);
122 Source::Lock newsrc_lock (newsrc->mutex());
124 write_bbt_source_to_source (region->midi_source(0), newsrc, newsrc_lock, region->pulse() - (region->start_beats().to_double() / 4.0));
126 std::cout << UTILNAME << ": Created new midi source file " << newsrc_path << std::endl;
127 std::cout << "for region : " << region->name() << std::endl;
134 boost::shared_ptr<MidiSource>
135 ensure_per_source_source (Session* session, std::string newsrc_path, boost::shared_ptr<MidiRegion> region)
137 boost::shared_ptr<MidiSource> newsrc;
139 /* create a new source if none exists and write corrected events to it. */
140 if (Glib::file_test (newsrc_path, Glib::FILE_TEST_EXISTS)) {
141 /* flags are ignored for external MIDI source */
142 Source::Flag flags = Source::Flag (Source::Writable | Source::CanRename);
144 newsrc = boost::dynamic_pointer_cast<MidiSource>(
145 SourceFactory::createExternal(DataType::MIDI, *session,
146 newsrc_path, 1, flags));
149 std::cout << UTILNAME << "An error occurred creating external source from " << newsrc_path << " exiting." << std::endl;
150 session_fail (session);
153 std::cout << UTILNAME << ": Using existing midi source file " << newsrc_path << std::endl;
154 std::cout << "for source : " << region->midi_source(0)->name() << std::endl;
157 newsrc = boost::dynamic_pointer_cast<MidiSource>(
158 SourceFactory::createWritable(DataType::MIDI, *session,
159 newsrc_path, false, session->frame_rate()));
161 std::cout << UTILNAME << "An error occurred creating writeable source " << newsrc_path << " exiting." << std::endl;
162 session_fail (session);
165 Source::Lock newsrc_lock (newsrc->mutex());
167 write_bbt_source_to_source (region->midi_source(0), newsrc, newsrc_lock, region->pulse() - (region->start_beats().to_double() / 4.0));
169 std::cout << UTILNAME << ": Created new midi source file " << newsrc_path << std::endl;
170 std::cout << "for source : " << region->midi_source(0)->name() << std::endl;
178 reset_start_and_length (Session* session, boost::shared_ptr<MidiRegion> region)
180 /* set start_beats & length_beats to quarter note value */
181 TempoMap& map (session->tempo_map());
183 region->set_start_beats (Evoral::Beats ((map.pulse_at_beat (region->beat())
184 - map.pulse_at_beat (region->beat() - region->start_beats().to_double())) * 4.0));
186 region->set_length_beats (Evoral::Beats ((map.pulse_at_beat (region->beat() + region->length_beats().to_double())
187 - map.pulse_at_beat (region->beat())) * 4.0));
189 std::cout << UTILNAME << ": Reset start and length beats for region : " << region->name() << std::endl;
193 apply_one_source_per_region_fix (Session* session)
195 const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
197 if (!region_map.size()) {
201 /* for every midi region, ensure a new source and switch to it. */
202 for (RegionFactory::RegionMap::const_iterator i = region_map.begin(); i != region_map.end(); ++i) {
203 boost::shared_ptr<MidiRegion> mr = 0;
205 if ((mr = boost::dynamic_pointer_cast<MidiRegion>((*i).second)) != 0) {
206 reset_start_and_length (session, mr);
207 string newsrc_filename = mr->name() + "-a54-compat.mid";
208 string newsrc_path = Glib::build_filename (session->session_directory().midi_path(), newsrc_filename);
209 boost::shared_ptr<MidiSource> newsrc = ensure_per_region_source (session, newsrc_path, mr);
210 mr->clobber_sources (newsrc);
218 apply_one_source_per_source_fix (Session* session)
220 const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
222 if (!region_map.size()) {
226 map<PBD::ID, boost::shared_ptr<MidiSource> > old_id_to_new_source;
227 /* for every midi region, ensure a converted source exists. */
228 for (RegionFactory::RegionMap::const_iterator i = region_map.begin(); i != region_map.end(); ++i) {
229 boost::shared_ptr<MidiRegion> mr = 0;
230 map<PBD::ID, boost::shared_ptr<MidiSource> >::iterator src_it;
232 if ((mr = boost::dynamic_pointer_cast<MidiRegion>((*i).second)) != 0) {
233 reset_start_and_length (session, mr);
235 if ((src_it = old_id_to_new_source.find (mr->midi_source()->id())) == old_id_to_new_source.end()) {
236 string newsrc_filename = mr->source()->name() + "-a54-compat.mid";
237 string newsrc_path = Glib::build_filename (session->session_directory().midi_path(), newsrc_filename);
239 boost::shared_ptr<MidiSource> newsrc = ensure_per_source_source (session, newsrc_path, mr);
242 std::cout << UTILNAME << ": an error occurred while creating a new source. exiting" << std::endl;
243 session_fail (session);
246 old_id_to_new_source.insert (make_pair (mr->midi_source()->id(), newsrc));
248 mr->midi_source(0)->set_name (newsrc->name());
253 /* remove new sources from the session. current snapshot is saved.*/
254 std::cout << UTILNAME << ": clearing new sources." << std::endl;
256 for (map<PBD::ID, boost::shared_ptr<MidiSource> >::iterator i = old_id_to_new_source.begin(); i != old_id_to_new_source.end(); ++i) {
257 session->remove_source (boost::weak_ptr<MidiSource> ((*i).second));
263 static void usage (int status) {
264 // help2man compatible format (standard GNU help-text)
265 printf (UTILNAME " - convert an ardour session with 5.0 - 5.3 midi sources to be compatible with 5.4.\n\n");
266 printf ("Usage: " UTILNAME " [ OPTIONS ] <session-dir> <snapshot-name>\n\n");
268 -h, --help display this help and exit\n\
269 -f, --force override detection of affected sessions\n\
270 -o, --output <snapshot-name> output session snapshot name (without file suffix)\n\
271 -V, --version print version information and exit\n\
274 This Ardour-specific utility provides an upgrade path for sessions created or modified with Ardour versions 5.0 - 5.3.\n\
275 It creates a 5.4-compatible snapshot from affected Ardour session files.\n\
276 Affected versions (5.0 - 5.3 inclusive) contain a bug which caused some MIDI region properties and contents\n\
277 to be stored incorrectly (see more below).\n\n\
278 The utility will first determine whether or not a session requires any changes for 5.4 compatibility.\n\
279 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\
280 The first is to write a new MIDI source file for every existing MIDI source in the supplied snapshot.\n\
281 In the second approach, each MIDI region have its source converted and placed in the session midifiles directory\n\
282 as a new source (one source file per region).\n\
283 The second method is only used if the first approach cannot guarantee that the results would match the input snapshot.\n\n\
284 Both methods update MIDI region properties and save a new snapshot in the supplied session-dir, optionally using a supplied snapshot name (-o).\n\
285 The new snapshot may be used on Ardour-5.4.\n\n\
286 Running this utility will not alter any existing files, but it is recommended that you run it on a backup of the session directory.\n\n\
288 ardour5-headless-chicken -o bantam ~/studio/leghorn leghorn\n\
289 will create a new snapshot file ~/studio/leghorn/bantam.ardour from ~/studio/leghorn/leghorn.ardour\n\
290 Converted midi sources will be created in ~/studio/leghorn/interchange/leghorn/midifiles/\n\
291 If the output option (-o) is omitted, the string \"-a54-compat\" will be appended to the supplied snapshot name.\n\n\
293 If a session from affected versions used MIDI regions and a meter note divisor was set to anything but quarter notes,\n\
294 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\
295 The region start and length offsets would also be stored incorrectly.\n\
296 If a MIDI session only contains quarter note meter divisors, it will be unaffected.\n\
299 printf ("Report bugs to <http://tracker.ardour.org/>\n"
300 "Website: <http://ardour.org/>\n");
304 int main (int argc, char* argv[])
309 const char *optstring = "hfo:r:V";
311 const struct option longopts[] = {
312 { "help", 0, 0, 'h' },
313 { "force", 0, 0, 'f' },
314 { "output", 1, 0, 'o' },
315 { "version", 0, 0, 'V' },
319 while (EOF != (c = getopt_long (argc, argv,
320 optstring, longopts, (int *) 0))) {
329 if (outfile.empty()) {
335 printf ("ardour-utils version %s\n\n", VERSIONSTRING);
336 printf ("Copyright (C) GPL 2015 Robin Gareus <robin@gareus.org>\n");
345 usage (EXIT_FAILURE);
350 if (optind + 2 > argc) {
351 usage (EXIT_FAILURE);
354 SessionDirectory* session_dir = new SessionDirectory (argv[optind]);
355 string snapshot_name (argv[optind+1]);
356 string statefile_suffix (X_(".ardour"));
357 string pending_suffix (X_(".pending"));
361 std::string xmlpath(argv[optind]);
362 string out_snapshot_name;
364 if (!outfile.empty()) {
365 string file_test_path = Glib::build_filename (argv[optind], outfile + statefile_suffix);
366 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
367 std::cout << UTILNAME << ": session file " << file_test_path << " already exists!" << std::endl;
368 ::exit (EXIT_FAILURE);
370 out_snapshot_name = outfile;
372 string file_test_path = Glib::build_filename (argv[optind], snapshot_name + "-a54-compat" + statefile_suffix);
373 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
374 std::cout << UTILNAME << ": session file " << file_test_path << " already exists!" << std::endl;
375 ::exit (EXIT_FAILURE);
377 out_snapshot_name = snapshot_name + "-a54-compat";
380 xmlpath = Glib::build_filename (xmlpath, legalize_for_path (snapshot_name) + pending_suffix);
382 if (Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
384 /* there is pending state from a crashed capture attempt */
385 std::cout << UTILNAME << ": There seems to be pending state for snapshot : " << snapshot_name << std::endl;
389 xmlpath = Glib::build_filename (argv[optind], argv[optind+1]);
391 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
392 xmlpath = Glib::build_filename (argv[optind], legalize_for_path (argv[optind+1]) + ".ardour");
393 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
394 std::cout << UTILNAME << ": session file " << xmlpath << " doesn't exist!" << std::endl;
395 ::exit (EXIT_FAILURE);
399 state_tree = new XMLTree;
401 bool writable = PBD::exists_and_writable (xmlpath) && PBD::exists_and_writable(Glib::path_get_dirname(xmlpath));
404 std::cout << UTILNAME << ": Error : The session directory must exist and be writable." << std::endl;
408 if (!state_tree->read (xmlpath)) {
409 std::cout << UTILNAME << ": Could not understand session file " << xmlpath << std::endl;
412 ::exit (EXIT_FAILURE);
415 XMLNode const & root (*state_tree->root());
417 if (root.name() != X_("Session")) {
418 std::cout << UTILNAME << ": Session file " << xmlpath<< " is not a session" << std::endl;
421 ::exit (EXIT_FAILURE);
424 XMLProperty const * prop;
426 if ((prop = root.property ("version")) == 0) {
427 /* no version implies very old version of Ardour */
428 std::cout << UTILNAME << ": The session " << snapshot_name << " has no version or is too old to be affected. exiting." << std::endl;
429 ::exit (EXIT_FAILURE);
431 if (prop->value().find ('.') != string::npos) {
432 /* old school version format */
433 std::cout << UTILNAME << ": The session " << snapshot_name << " is too old to be affected. exiting." << std::endl;
434 ::exit (EXIT_FAILURE);
436 PBD::Stateful::loading_state_version = atoi (prop->value().c_str());
440 std::cout << UTILNAME << ": Checking snapshot : " << snapshot_name << " in directory : " << session_dir->root_path() << std::endl;
442 bool midi_regions_use_bbt_beats = false;
444 if (PBD::Stateful::loading_state_version == 3002 && writable) {
446 if ((child = find_named_node (root, "ProgramVersion")) != 0) {
447 if ((prop = child->property ("modified-with")) != 0) {
448 std::string modified_with = prop->value ();
450 const double modified_with_version = atof (modified_with.substr ( modified_with.find(" ", 0) + 1, string::npos).c_str());
451 const int modified_with_revision = atoi (modified_with.substr (modified_with.find("-", 0) + 1, string::npos).c_str());
453 if (modified_with_version <= 5.3 && !(modified_with_version == 5.3 && modified_with_revision >= 42)) {
454 midi_regions_use_bbt_beats = true;
461 bool all_metrum_divisors_are_quarters = true;
462 list<double> divisor_list;
464 if ((tm_node = find_named_node (root, "TempoMap")) != 0) {
466 XMLNodeConstIterator niter;
467 metrum = tm_node->children();
468 for (niter = metrum.begin(); niter != metrum.end(); ++niter) {
469 XMLNode* child = *niter;
471 if (child->name() == MeterSection::xml_state_node_name && (prop = child->property ("note-type")) != 0) {
474 if (sscanf (prop->value().c_str(), "%lf", ¬e_type) ==1) {
476 if (note_type != 4.0) {
477 all_metrum_divisors_are_quarters = false;
479 divisor_list.push_back (note_type);
484 std::cout << UTILNAME << ": Session file " << xmlpath << " has no TempoMap node. exiting." << std::endl;
485 ::exit (EXIT_FAILURE);
488 if (all_metrum_divisors_are_quarters && !force) {
489 std::cout << UTILNAME << ": The session " << snapshot_name << " is clear for use in 5.4 (all divisors are quarters). Use -f to override." << std::endl;
490 ::exit (EXIT_FAILURE);
493 /* check for multiple note divisors. if there is only one, we can create one file per source. */
494 bool one_source_file_per_source = false;
495 divisor_list.unique();
497 if (divisor_list.size() == 1) {
498 std::cout << UTILNAME << ": Snapshot " << snapshot_name << " will be converted using one new file per source." << std::endl;
499 std::cout << "To continue with per-source conversion press enter s. q to quit." << std::endl;
502 std::cout << "[s/q]" << std::endl;
505 getline (std::cin, input);
517 one_source_file_per_source = true;
520 std::cout << UTILNAME << ": Snapshot " << snapshot_name << " contains multiple meter note divisors." << std::endl;
521 std::cout << "per-region source conversion guarantees that the output snapshot will be identical to the original," << std::endl;
522 std::cout << "however regions in the new snapshot will no longer share sources." << std::endl;
523 std::cout << "In many (but not all) cases per-source conversion will work equally well." << std::endl;
524 std::cout << "It is recommended that you test a snapshot created with the per-source method before using per-region conversion." << std::endl;
525 std::cout << "To continue with per-region conversion enter r. For per-source conversion, enter s. q to quit." << std::endl;
528 std::cout << "[r/s/q]" << std::endl;
531 getline (std::cin, input);
534 one_source_file_per_source = true;
549 if (midi_regions_use_bbt_beats || force) {
552 std::cout << UTILNAME << ": Forced update of snapshot : " << snapshot_name << std::endl;
555 SessionUtils::init();
558 std::cout << UTILNAME << ": Loading snapshot." << std::endl;
560 s = SessionUtils::load_session (argv[optind], argv[optind+1]);
562 /* save new snapshot and prevent alteration of the original by switching to it.
563 we know these files don't yet exist.
565 if (s->save_state (out_snapshot_name, false, true)) {
566 std::cout << UTILNAME << ": Could not save new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << std::endl;
571 std::cout << UTILNAME << ": Saved new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << std::endl;
573 if (one_source_file_per_source) {
574 std::cout << UTILNAME << ": Will create one MIDI file per source." << std::endl;
576 if (!apply_one_source_per_source_fix (s)) {
577 std::cout << UTILNAME << ": The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting." << std::endl;
581 std::cout << UTILNAME << ": Will create one MIDI file per midi region." << std::endl;
583 if (!apply_one_source_per_region_fix (s)) {
584 std::cout << UTILNAME << ": The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting." << std::endl;
588 if (s->save_state (out_snapshot_name, false, true)) {
589 std::cout << UTILNAME << ": Could not save snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << std::endl;
592 std::cout << UTILNAME << ": Saved new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << std::endl;
595 SessionUtils::unload_session(s);
596 SessionUtils::cleanup();
597 std::cout << UTILNAME << ": Snapshot " << out_snapshot_name << " is ready for use in 5.4" << std::endl;
599 std::cout << UTILNAME << ": The snapshot " << snapshot_name << " doesn't require any change for use in 5.4. Use -f to override." << std::endl;
600 ::exit (EXIT_FAILURE);