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 clone_bbt_source_to_source (boost::shared_ptr<MidiSource> bbt_source, boost::shared_ptr<MidiSource> source,
51 const Glib::Threads::Mutex::Lock& source_lock, const double session_offset)
53 const bool old_percussive = bbt_source->model()->percussive();
55 bbt_source->model()->set_percussive (false);
57 source->mark_streaming_midi_write_started (source_lock, bbt_source->model()->note_mode());
59 TempoMap& map (source->session().tempo_map());
61 for (Evoral::Sequence<MidiModel::TimeType>::const_iterator i = bbt_source->model()->begin(MidiModel::TimeType(), true); i != bbt_source->model()->end(); ++i) {
62 const double new_time = map.quarter_note_at_beat ((*i).time().to_double() + map.beat_at_pulse (session_offset)) - (session_offset * 4.0);
63 Evoral::Event<Evoral::Beats> new_ev (*i, true);
64 new_ev.set_time (Evoral::Beats (new_time));
65 source->append_event_beats (source_lock, new_ev);
68 bbt_source->model()->set_percussive (old_percussive);
69 source->mark_streaming_write_completed (source_lock);
74 boost::shared_ptr<MidiSource>
75 ensure_qn_source (Session* session, std::string path, boost::shared_ptr<MidiRegion> region, bool one_file_per_source)
77 boost::shared_ptr<MidiSource> newsrc;
78 string newsrc_filename;
80 if (one_file_per_source) {
81 newsrc_filename = region->source()->name() + "-a54-compat.mid";
83 newsrc_filename = region->name() + "-a54-compat.mid";
86 string newsrc_path = Glib::build_filename (path, newsrc_filename);
88 /* create a new source if none exists and write corrected events to it.
89 if file exists, assume that it is correct.
91 if (Glib::file_test (newsrc_path, Glib::FILE_TEST_EXISTS)) {
92 Source::Flag flags = Source::Flag (Source::Writable | Source::CanRename);
93 newsrc = boost::dynamic_pointer_cast<MidiSource>(
94 SourceFactory::createExternal(DataType::MIDI, *session,
95 newsrc_path, 1, flags));
97 XMLNode* node = new XMLNode (newsrc->get_state());
99 if (node->property ("flags") != 0) {
100 node->property ("flags")->set_value (enum_2_string (flags));
103 newsrc->set_state (*node, PBD::Stateful::loading_state_version);
105 std::cout << UTILNAME << ": Using existing midi source file : " << newsrc_path << std::endl;
106 std::cout << "for region : " << region->name() << std::endl;
109 newsrc = boost::dynamic_pointer_cast<MidiSource>(
110 SourceFactory::createWritable(DataType::MIDI, *session,
111 newsrc_path, false, session->frame_rate()));
112 Source::Lock newsrc_lock (newsrc->mutex());
114 clone_bbt_source_to_source (region->midi_source(0), newsrc, newsrc_lock, region->pulse() - (region->start_beats().to_double() / 4.0));
116 std::cout << UTILNAME << ": Created new midi source file " << newsrc_path << std::endl;
117 std::cout << "for region : " << region->name() << std::endl;
124 reset_start_and_length (Session* session, boost::shared_ptr<MidiRegion> region)
126 /* set start_beats & length_beats to quarter note value */
127 TempoMap& map (session->tempo_map());
129 region->set_start_beats (Evoral::Beats ((map.pulse_at_beat (region->beat())
130 - map.pulse_at_beat (region->beat() - region->start_beats().to_double())) * 4.0));
132 region->set_length_beats (Evoral::Beats ((map.pulse_at_beat (region->beat() + region->length_beats().to_double())
133 - map.pulse_at_beat (region->beat())) * 4.0));
135 std::cout << UTILNAME << ": Reset start and length beats for region : " << region->name() << std::endl;
139 write_one_source_per_region (Session* session)
141 const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
143 if (!region_map.size()) {
147 /* for every midi region, ensure a new source and switch to it. */
148 for (RegionFactory::RegionMap::const_iterator i = region_map.begin(); i != region_map.end(); ++i) {
149 boost::shared_ptr<MidiRegion> mr = 0;
151 if ((mr = boost::dynamic_pointer_cast<MidiRegion>((*i).second)) != 0) {
152 reset_start_and_length (session, mr);
153 boost::shared_ptr<MidiSource> newsrc = ensure_qn_source (session, session->session_directory().midi_path(), mr, false);
155 mr->clobber_sources (newsrc);
163 write_one_source_per_source (Session* session)
165 const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
167 if (!region_map.size()) {
171 map<PBD::ID, boost::shared_ptr<MidiSource> > old_id_to_new_source;
172 /* for every midi source, ensure a new source and switch to it. */
173 for (RegionFactory::RegionMap::const_iterator i = region_map.begin(); i != region_map.end(); ++i) {
174 boost::shared_ptr<MidiRegion> mr = 0;
175 map<PBD::ID, boost::shared_ptr<MidiSource> >::iterator src_it;
177 if ((mr = boost::dynamic_pointer_cast<MidiRegion>((*i).second)) != 0) {
178 reset_start_and_length (session, mr);
180 if ((src_it = old_id_to_new_source.find (mr->source()->id())) != old_id_to_new_source.end()) {
181 mr->clobber_sources ((*src_it).second);
183 boost::shared_ptr<MidiSource> newsrc = ensure_qn_source (session, session->session_directory().midi_path(), mr, true);
184 old_id_to_new_source.insert (make_pair (mr->source()->id(), newsrc));
185 mr->clobber_sources (newsrc);
193 static void usage (int status) {
194 // help2man compatible format (standard GNU help-text)
195 printf (UTILNAME " - convert an ardour session with 5.0 - 5.3 midi sources to be compatible with 5.4.\n\n");
196 printf ("Usage: " UTILNAME " [ OPTIONS ] <session-dir> <session/snapshot-name>\n\n");
198 -h, --help display this help and exit\n\
199 -f, --force override detection of affected sessions\n\
200 -o, --output <file> output session snapshot name (without file suffix)\n\
201 -V, --version print version information and exit\n\
204 This Ardour-specific utility provides an upgrade path for sessions created or modified with Ardour versions 5.0 - 5.3.\n\
205 It creates a 5.4-compatible snapshot from affected Ardour session files.\n\
206 Affected versions (5.0 - 5.3 inclusive) contain a bug which caused some MIDI region properties and contents\n\
207 to be stored incorrectly (see more below).\n\n\
208 The utility will first determine whether or not a session requires any changes for 5.4 compatibility.\n\
209 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\
210 The first is to write a new MIDI source file for every existing MIDI source in the supplied snapshot.\n\
211 In the second approach, each MIDI region have its source converted and placed in the session midifiles directory\n\
212 as a new source (one source file per region).\n\
213 The second method is only used if the first approach cannot guarantee that the results would match the input snapshot.\n\n\
214 Both methods update MIDI region properties and save a new snapshot in the supplied session-dir, optionally using a supplied snapshot name (-o).\n\
215 The new snapshot may be used on Ardour-5.4.\n\n\
216 Running this utility will not alter any existing files, but it is recommended that you backup the session directory before use.\n\n\
218 ardour5-headless-chicken -o bantam ~/studio/leghorn leghorn\n\
219 will create a new snapshot file ~/studio/leghorn/bantam.ardour from ~/studio/leghorn/leghorn.ardour\n\
220 Converted midi sources will be created in ~/studio/leghorn/interchange/leghorn/midifiles/\n\
221 If the output option (-o) is omitted, the string \"-a54-compat\" will be appended to the supplied snapshot name.\n\n\
223 If a session from affected versions used MIDI regions and a meter note divisor was set to anything but quarter notes,\n\
224 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\
225 The region start and length offsets would also be stored incorrectly.\n\
226 If a MIDI session only contains quarter note meter divisors, it will be unaffected.\n\
229 printf ("Report bugs to <http://tracker.ardour.org/>\n"
230 "Website: <http://ardour.org/>\n");
234 int main (int argc, char* argv[])
239 const char *optstring = "hfo:r:V";
241 const struct option longopts[] = {
242 { "help", 0, 0, 'h' },
243 { "force", 0, 0, 'f' },
244 { "output", 1, 0, 'o' },
245 { "version", 0, 0, 'V' },
249 while (EOF != (c = getopt_long (argc, argv,
250 optstring, longopts, (int *) 0))) {
262 printf ("ardour-utils version %s\n\n", VERSIONSTRING);
263 printf ("Copyright (C) GPL 2015 Robin Gareus <robin@gareus.org>\n");
272 usage (EXIT_FAILURE);
277 if (optind + 2 > argc) {
278 usage (EXIT_FAILURE);
280 std::cout << UTILNAME << ": hello" << std::endl;
282 SessionDirectory* session_dir = new SessionDirectory (argv[optind]);
283 std::string snapshot_name (argv[optind+1]);
284 std::string statefile_suffix (X_(".ardour"));
285 std::string pending_suffix (X_(".pending"));
289 std::string xmlpath(argv[optind]);
291 if (!outfile.empty ()) {
292 string file_test_path = Glib::build_filename (argv[optind], outfile + statefile_suffix);
293 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
294 std::cout << UTILNAME << ": session file " << file_test_path << " already exists!" << std::endl;
298 string file_test_path = Glib::build_filename (argv[optind], snapshot_name + "-a54-compat" + statefile_suffix);
299 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
300 std::cout << UTILNAME << ": session file " << file_test_path << "already exists!" << std::endl;
305 xmlpath = Glib::build_filename (xmlpath, legalize_for_path (snapshot_name) + pending_suffix);
307 if (Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
309 /* there is pending state from a crashed capture attempt */
310 std::cout << UTILNAME << ": There seems to be pending state for snapshot : " << snapshot_name << std::endl;
314 xmlpath = Glib::build_filename (argv[optind], argv[optind+1]);
316 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
317 xmlpath = Glib::build_filename (argv[optind], legalize_for_path (argv[optind+1]) + ".ardour");
318 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
319 std::cout << UTILNAME << ": session file " << xmlpath << " doesn't exist!" << std::endl;
324 state_tree = new XMLTree;
326 bool writable = PBD::exists_and_writable (xmlpath) && PBD::exists_and_writable(Glib::path_get_dirname(xmlpath));
329 std::cout << UTILNAME << ": Error : The session directory must exist and be writable." << std::endl;
333 if (!state_tree->read (xmlpath)) {
334 std::cout << UTILNAME << ": Could not understand session file " << xmlpath << std::endl;
340 XMLNode const & root (*state_tree->root());
342 if (root.name() != X_("Session")) {
343 std::cout << UTILNAME << ": Session file " << xmlpath<< " is not a session" << std::endl;
349 XMLProperty const * prop;
351 if ((prop = root.property ("version")) == 0) {
352 /* no version implies very old version of Ardour */
353 std::cout << UTILNAME << ": The session " << argv[optind+1] << " has no version or is too old to be affected. exiting." << std::endl;
356 if (prop->value().find ('.') != string::npos) {
357 /* old school version format */
358 std::cout << UTILNAME << ": The session " << argv[optind+1] << " is too old to be affected. exiting." << std::endl;
361 PBD::Stateful::loading_state_version = atoi (prop->value().c_str());
365 std::cout << UTILNAME << ": Checking snapshot : " << snapshot_name << " in directory : " << session_dir->root_path() << std::endl;
367 bool midi_regions_use_bbt_beats = false;
369 if (PBD::Stateful::loading_state_version == 3002 && writable) {
371 if ((child = find_named_node (root, "ProgramVersion")) != 0) {
372 if ((prop = child->property ("modified-with")) != 0) {
373 std::string modified_with = prop->value ();
375 const double modified_with_version = atof (modified_with.substr ( modified_with.find(" ", 0) + 1, string::npos).c_str());
376 const int modified_with_revision = atoi (modified_with.substr (modified_with.find("-", 0) + 1, string::npos).c_str());
378 if (modified_with_version <= 5.3 && !(modified_with_version == 5.3 && modified_with_revision >= 42)) {
379 midi_regions_use_bbt_beats = true;
386 bool all_metrum_divisors_are_quarters = true;
387 list<double> divisor_list;
389 if ((tm_node = find_named_node (root, "TempoMap")) != 0) {
391 XMLNodeConstIterator niter;
392 metrum = tm_node->children();
393 for (niter = metrum.begin(); niter != metrum.end(); ++niter) {
394 XMLNode* child = *niter;
396 if (child->name() == MeterSection::xml_state_node_name && (prop = child->property ("divisions-per-bar")) != 0) {
397 double divisions_per_bar;
399 if (sscanf (prop->value().c_str(), "%lf", &divisions_per_bar) ==1) {
401 if (divisions_per_bar != 4.0) {
402 all_metrum_divisors_are_quarters = false;
403 divisor_list.push_back (divisions_per_bar);
409 std::cout << UTILNAME << ": Session file " << xmlpath << " has no TempoMap node. exiting." << std::endl;
413 if (all_metrum_divisors_are_quarters && !force) {
414 std::cout << UTILNAME << ": The session " << argv[optind+1] << " is clear for use in 5.4 (all divisors are quarters). Use -f to override." << std::endl;
418 /* check for multiple note divisors. if there is only one, we can create one file per source. */
419 bool new_source_file_per_source = false;
420 divisor_list.unique();
422 if (divisor_list.size() == 1) {
423 new_source_file_per_source = true;
426 if (midi_regions_use_bbt_beats || force) {
428 std::cout << UTILNAME << ": Forced update of snapshot : " << argv[optind+1] << std::endl;
431 SessionUtils::init();
434 std::cout << UTILNAME << ": Loading snapshot." << std::endl;
436 s = SessionUtils::load_session (argv[optind], argv[optind+1]);
437 if (new_source_file_per_source) {
438 std::cout << UTILNAME << ": Will create one MIDI file per source." << std::endl;
440 if (!write_one_source_per_source (s)) {
441 std::cout << UTILNAME << ": The snapshot " << argv[optind+1] << " is clear for use in 5.4 (no midi regions). exiting." << std::endl;
442 SessionUtils::unload_session(s);
443 SessionUtils::cleanup();
447 std::cout << UTILNAME << ": Will create one MIDI file per midi region." << std::endl;
449 if (!write_one_source_per_region (s)) {
450 std::cout << UTILNAME << ": The snapshot " << argv[optind+1] << " is clear for use in 5.4 (no midi regions). exiting." << std::endl;
451 SessionUtils::unload_session(s);
452 SessionUtils::cleanup();
456 /* we've already checked that these don't exist */
457 if (outfile.empty ()) {
458 s->save_state (snapshot_name + "-a54-compat");
459 std::cout << UTILNAME << ": Saved new snapshot: " << snapshot_name + "-a54-compat" << " in " << session_dir << std::endl;
462 s->save_state (outfile);
463 std::cout << UTILNAME << ": Saved new snapshot: " << outfile.c_str() << " in " << session_dir << std::endl;
466 SessionUtils::unload_session(s);
467 SessionUtils::cleanup();
468 std::cout << UTILNAME << ": Finished." << std::endl;
470 std::cout << UTILNAME << ": The snapshot " << argv[optind+1] << " doesn't require any change for use in 5.4. Use -f to override." << std::endl;