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, 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 cout << UTILNAME << "An error occurred creating external source from " << newsrc_path << " exiting." << 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 cout << UTILNAME << ": Using existing midi source file " << newsrc_path << endl;
110 cout << "for region : " << region->name() << endl;
113 newsrc = boost::dynamic_pointer_cast<MidiSource>(
114 SourceFactory::createWritable(DataType::MIDI, *session,
115 newsrc_path, false, session->frame_rate()));
118 cout << UTILNAME << "An error occurred creating writeable source " << newsrc_path << " exiting." << 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 cout << UTILNAME << ": Created new midi source file " << newsrc_path << endl;
127 cout << "for region : " << region->name() << endl;
134 boost::shared_ptr<MidiSource>
135 ensure_per_source_source (Session* session, 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 cout << UTILNAME << "An error occurred creating external source from " << newsrc_path << " exiting." << endl;
150 session_fail (session);
153 cout << UTILNAME << ": Using existing midi source file " << newsrc_path << endl;
154 cout << "for source : " << region->midi_source(0)->name() << endl;
157 newsrc = boost::dynamic_pointer_cast<MidiSource>(
158 SourceFactory::createWritable(DataType::MIDI, *session,
159 newsrc_path, false, session->frame_rate()));
161 cout << UTILNAME << "An error occurred creating writeable source " << newsrc_path << " exiting." << 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 cout << UTILNAME << ": Created new midi source file " << newsrc_path << endl;
170 cout << "for source : " << region->midi_source(0)->name() << 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 cout << UTILNAME << ": Reset start and length beats for region : " << region->name() << 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);
241 old_id_to_new_source.insert (make_pair (mr->midi_source()->id(), newsrc));
243 mr->midi_source(0)->set_name (newsrc->name());
248 /* remove new sources from the session. current snapshot is saved.*/
249 cout << UTILNAME << ": clearing new sources." << endl;
251 for (map<PBD::ID, boost::shared_ptr<MidiSource> >::iterator i = old_id_to_new_source.begin(); i != old_id_to_new_source.end(); ++i) {
252 session->remove_source (boost::weak_ptr<MidiSource> ((*i).second));
258 static void usage (int status) {
259 // help2man compatible format (standard GNU help-text)
260 printf (UTILNAME " - convert an ardour session with 5.0 - 5.3 midi sources to be compatible with 5.4.\n\n");
261 printf ("Usage: " UTILNAME " [ OPTIONS ] <session-dir> <snapshot-name>\n\n");
263 -h, --help display this help and exit\n\
264 -f, --force override detection of affected sessions\n\
265 -o, --output <snapshot-name> output session snapshot name (without file suffix)\n\
266 -V, --version print version information and exit\n\
269 This Ardour-specific utility provides an upgrade path for sessions created or modified with Ardour versions 5.0 - 5.3.\n\
270 It creates a 5.4-compatible snapshot from affected Ardour session files.\n\
271 Affected versions (5.0 - 5.3 inclusive) contain a bug which caused some MIDI region properties and contents\n\
272 to be stored incorrectly (see more below).\n\n\
273 The utility will first determine whether or not a session requires any changes for 5.4 compatibility.\n\
274 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\
275 The first is to write a new MIDI source file for every existing MIDI source in the supplied snapshot.\n\
276 In the second approach, each MIDI region have its source converted and placed in the session midifiles directory\n\
277 as a new source (one source file per region).\n\
278 The second method is only offered if the first approach cannot logically ensure that the results would match the input snapshot.\n\
279 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\
280 Both methods update MIDI region properties and save a new snapshot in the supplied session-dir, optionally using a supplied snapshot name (-o).\n\
281 The new snapshot may be used on Ardour-5.4.\n\n\
282 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\
284 ardour5-headless-chicken -o bantam ~/studio/leghorn leghorn\n\
285 will create a new snapshot file ~/studio/leghorn/bantam.ardour from ~/studio/leghorn/leghorn.ardour\n\
286 Converted midi sources will be created in ~/studio/leghorn/interchange/leghorn/midifiles/\n\
287 If the output option (-o) is omitted, the string \"-a54-compat\" will be appended to the supplied snapshot name.\n\n\
289 If a session from affected versions used MIDI regions and a meter note divisor was set to anything but quarter notes,\n\
290 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\
291 The region start and length offsets would also be stored incorrectly.\n\
292 If a MIDI session only contains quarter note meter divisors, it will be unaffected.\n\
295 printf ("Report bugs to <http://tracker.ardour.org/>\n"
296 "Website: <http://ardour.org/>\n");
300 int main (int argc, char* argv[])
305 const char *optstring = "hfo:r:V";
307 const struct option longopts[] = {
308 { "help", 0, 0, 'h' },
309 { "force", 0, 0, 'f' },
310 { "output", 1, 0, 'o' },
311 { "version", 0, 0, 'V' },
315 while (EOF != (c = getopt_long (argc, argv,
316 optstring, longopts, (int *) 0))) {
325 if (outfile.empty()) {
331 printf ("ardour-utils version %s\n\n", VERSIONSTRING);
332 printf ("Copyright (C) GPL 2015 Robin Gareus <robin@gareus.org>\n");
341 usage (EXIT_FAILURE);
346 if (optind + 2 > argc) {
347 usage (EXIT_FAILURE);
350 SessionDirectory* session_dir = new SessionDirectory (argv[optind]);
351 string snapshot_name (argv[optind+1]);
352 string statefile_suffix (X_(".ardour"));
353 string pending_suffix (X_(".pending"));
357 string xmlpath(argv[optind]);
358 string out_snapshot_name;
360 if (!outfile.empty()) {
361 string file_test_path = Glib::build_filename (argv[optind], outfile + statefile_suffix);
362 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
363 cout << UTILNAME << ": session file " << file_test_path << " already exists!" << endl;
364 ::exit (EXIT_FAILURE);
366 out_snapshot_name = outfile;
368 string file_test_path = Glib::build_filename (argv[optind], snapshot_name + "-a54-compat" + statefile_suffix);
369 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
370 cout << UTILNAME << ": session file " << file_test_path << " already exists!" << endl;
371 ::exit (EXIT_FAILURE);
373 out_snapshot_name = snapshot_name + "-a54-compat";
376 xmlpath = Glib::build_filename (xmlpath, legalize_for_path (snapshot_name) + pending_suffix);
378 if (Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
380 /* there is pending state from a crashed capture attempt */
381 cout << UTILNAME << ": There seems to be pending state for snapshot : " << snapshot_name << endl;
385 xmlpath = Glib::build_filename (argv[optind], argv[optind+1]);
387 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
388 xmlpath = Glib::build_filename (argv[optind], legalize_for_path (argv[optind+1]) + ".ardour");
389 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
390 cout << UTILNAME << ": session file " << xmlpath << " doesn't exist!" << endl;
391 ::exit (EXIT_FAILURE);
395 state_tree = new XMLTree;
397 bool writable = PBD::exists_and_writable (xmlpath) && PBD::exists_and_writable(Glib::path_get_dirname(xmlpath));
400 cout << UTILNAME << ": Error : The session directory must exist and be writable." << endl;
404 if (!state_tree->read (xmlpath)) {
405 cout << UTILNAME << ": Could not understand session file " << xmlpath << endl;
408 ::exit (EXIT_FAILURE);
411 XMLNode const & root (*state_tree->root());
413 if (root.name() != X_("Session")) {
414 cout << UTILNAME << ": Session file " << xmlpath<< " is not a session" << endl;
417 ::exit (EXIT_FAILURE);
420 XMLProperty const * prop;
422 if ((prop = root.property ("version")) == 0) {
423 /* no version implies very old version of Ardour */
424 cout << UTILNAME << ": The session " << snapshot_name << " has no version or is too old to be affected. exiting." << endl;
425 ::exit (EXIT_FAILURE);
427 if (prop->value().find ('.') != string::npos) {
428 /* old school version format */
429 cout << UTILNAME << ": The session " << snapshot_name << " is too old to be affected. exiting." << endl;
430 ::exit (EXIT_FAILURE);
432 PBD::Stateful::loading_state_version = atoi (prop->value().c_str());
436 cout << UTILNAME << ": Checking snapshot : " << snapshot_name << " in directory : " << session_dir->root_path() << endl;
438 bool midi_regions_use_bbt_beats = false;
440 if (PBD::Stateful::loading_state_version == 3002 && writable) {
442 if ((child = find_named_node (root, "ProgramVersion")) != 0) {
443 if ((prop = child->property ("modified-with")) != 0) {
444 string modified_with = prop->value ();
446 const double modified_with_version = atof (modified_with.substr ( modified_with.find(" ", 0) + 1, string::npos).c_str());
447 const int modified_with_revision = atoi (modified_with.substr (modified_with.find("-", 0) + 1, string::npos).c_str());
449 if (modified_with_version <= 5.3 && !(modified_with_version == 5.3 && modified_with_revision >= 42)) {
450 midi_regions_use_bbt_beats = true;
457 bool all_metrum_divisors_are_quarters = true;
458 list<double> divisor_list;
460 if ((tm_node = find_named_node (root, "TempoMap")) != 0) {
462 XMLNodeConstIterator niter;
463 metrum = tm_node->children();
464 for (niter = metrum.begin(); niter != metrum.end(); ++niter) {
465 XMLNode* child = *niter;
467 if (child->name() == MeterSection::xml_state_node_name && (prop = child->property ("note-type")) != 0) {
470 if (sscanf (prop->value().c_str(), "%lf", ¬e_type) ==1) {
472 if (note_type != 4.0) {
473 all_metrum_divisors_are_quarters = false;
475 divisor_list.push_back (note_type);
480 cout << UTILNAME << ": Session file " << xmlpath << " has no TempoMap node. exiting." << endl;
481 ::exit (EXIT_FAILURE);
484 if (all_metrum_divisors_are_quarters && !force) {
485 cout << UTILNAME << ": The session " << snapshot_name << " is clear for use in 5.4 (all divisors are quarters). Use -f to override." << endl;
486 ::exit (EXIT_FAILURE);
489 /* check for multiple note divisors. if there is only one, we can create one file per source. */
490 bool one_source_file_per_source = false;
491 divisor_list.unique();
493 if (divisor_list.size() == 1) {
494 cout << UTILNAME << ": Snapshot " << snapshot_name << " will be converted using one new file per source." << endl;
495 cout << "To continue with per-source conversion press enter s. q to quit." << endl;
498 cout << "[s/q]" << endl;
501 getline (cin, input);
513 one_source_file_per_source = true;
516 cout << UTILNAME << ": Snapshot " << snapshot_name << " contains multiple meter note divisors." << endl;
517 cout << "per-region source conversion ensures that the output snapshot will be identical to the original," << endl;
518 cout << "however regions in the new snapshot will no longer share sources." << endl;
519 cout << "In many (but not all) cases per-source conversion will work equally well." << endl;
520 cout << "It is recommended that you test a snapshot created with the per-source method before using per-region conversion." << endl;
521 cout << "To continue with per-region conversion enter r. For per-source conversion, enter s. q to quit." << endl;
524 cout << "[r/s/q]" << endl;
527 getline (cin, input);
530 one_source_file_per_source = true;
545 if (midi_regions_use_bbt_beats || force) {
548 cout << UTILNAME << ": Forced update of snapshot : " << snapshot_name << endl;
551 SessionUtils::init();
554 cout << UTILNAME << ": Loading snapshot." << endl;
556 s = SessionUtils::load_session (argv[optind], argv[optind+1]);
558 /* save new snapshot and prevent alteration of the original by switching to it.
559 we know these files don't yet exist.
561 if (s->save_state (out_snapshot_name, false, true)) {
562 cout << UTILNAME << ": Could not save new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
567 cout << UTILNAME << ": Saved new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
569 if (one_source_file_per_source) {
570 cout << UTILNAME << ": Will create one MIDI file per source." << endl;
572 if (!apply_one_source_per_source_fix (s)) {
573 cout << UTILNAME << ": The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting." << endl;
577 cout << UTILNAME << ": Will create one MIDI file per midi region." << endl;
579 if (!apply_one_source_per_region_fix (s)) {
580 cout << UTILNAME << ": The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting." << endl;
584 if (s->save_state (out_snapshot_name, false, true)) {
585 cout << UTILNAME << ": Could not save snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
588 cout << UTILNAME << ": Saved new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
591 SessionUtils::unload_session(s);
592 SessionUtils::cleanup();
593 cout << UTILNAME << ": Snapshot " << out_snapshot_name << " is ready for use in 5.4" << endl;
595 cout << UTILNAME << ": The snapshot " << snapshot_name << " doesn't require any change for use in 5.4. Use -f to override." << endl;
596 ::exit (EXIT_FAILURE);