Add headless-chicken session utility.
[ardour.git] / session_utils / headless-chicken.cc
1 /*
2     Copyright (C) 2000-2006 Paul Davis
3
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.
8
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.
13
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.
17 */
18
19 #include <iostream>
20 #include <cstdlib>
21 #include <getopt.h>
22
23 #include <glibmm.h>
24
25 #include "pbd/file_utils.h"
26 #include "pbd/i18n.h"
27 #include "pbd/stateful.h"
28
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"
39
40 #include "evoral/Note.hpp"
41 #include "evoral/Sequence.hpp"
42
43 #include "common.h"
44
45 using namespace std;
46 using namespace ARDOUR;
47 using namespace SessionUtils;
48
49 bool
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)
52 {
53         const bool old_percussive = bbt_source->model()->percussive();
54
55         bbt_source->model()->set_percussive (false);
56
57         source->mark_streaming_midi_write_started (source_lock, bbt_source->model()->note_mode());
58
59         TempoMap& map (source->session().tempo_map());
60
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);
66         }
67
68         bbt_source->model()->set_percussive (old_percussive);
69         source->mark_streaming_write_completed (source_lock);
70
71         return true;
72 }
73
74 boost::shared_ptr<MidiSource>
75 ensure_qn_source (Session* session, std::string path, boost::shared_ptr<MidiRegion> region, bool one_file_per_source)
76 {
77         boost::shared_ptr<MidiSource> newsrc;
78         string newsrc_filename;
79
80         if (one_file_per_source) {
81                 newsrc_filename = region->source()->name() +  "-a54-compat.mid";
82         } else {
83                 newsrc_filename = region->name() +  "-a54-compat.mid";
84         }
85
86         string newsrc_path = Glib::build_filename (path, newsrc_filename);
87
88         /* create a new source if none exists and write corrected events to it.
89            if file exists, assume that it is correct.
90         */
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));
96                 /* hack flags */
97                 XMLNode* node = new XMLNode (newsrc->get_state());
98
99                 if (node->property ("flags") != 0) {
100                         node->property ("flags")->set_value (enum_2_string (flags));
101                 }
102
103                 newsrc->set_state (*node, PBD::Stateful::loading_state_version);
104
105                 std::cout << UTILNAME << ": Using existing midi source file : " << newsrc_path << std::endl;
106                 std::cout << "for region : " << region->name() << std::endl;
107
108         } else {
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());
113
114                 clone_bbt_source_to_source (region->midi_source(0), newsrc, newsrc_lock, region->pulse() - (region->start_beats().to_double() / 4.0));
115
116                 std::cout << UTILNAME << ": Created new midi source file " << newsrc_path << std::endl;
117                 std::cout << "for region : " <<  region->name() << std::endl;
118         }
119
120         return newsrc;
121 }
122
123 void
124 reset_start_and_length (Session* session, boost::shared_ptr<MidiRegion> region)
125 {
126         /* set start_beats & length_beats to quarter note value */
127         TempoMap& map (session->tempo_map());
128
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));
131
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));
134
135         std::cout << UTILNAME << ": Reset start and length beats for region : " << region->name() << std::endl;
136 }
137
138 bool
139 write_one_source_per_region (Session* session)
140 {
141         const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
142
143         if (!region_map.size()) {
144                 return false;
145         }
146
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;
150
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);
154
155                         mr->clobber_sources (newsrc);
156                 }
157         }
158
159         return true;
160 }
161
162 bool
163 write_one_source_per_source (Session* session)
164 {
165         const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
166
167         if (!region_map.size()) {
168                 return false;
169         }
170
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;
176
177                 if ((mr = boost::dynamic_pointer_cast<MidiRegion>((*i).second)) != 0) {
178                         reset_start_and_length (session, mr);
179
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);
182                         } else {
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);
186                         }
187                 }
188         }
189
190         return true;
191 }
192
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");
197         printf ("Options:\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\
202 \n");
203         printf ("\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\
217 EXAMPLE:\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\
222 About the Bug\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\
227 \n");
228
229         printf ("Report bugs to <http://tracker.ardour.org/>\n"
230                 "Website: <http://ardour.org/>\n");
231         ::exit (status);
232 }
233
234 int main (int argc, char* argv[])
235 {
236         std::string outfile;
237         bool force = false;
238
239         const char *optstring = "hfo:r:V";
240
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' },
246         };
247
248         int c = 0;
249         while (EOF != (c = getopt_long (argc, argv,
250                                         optstring, longopts, (int *) 0))) {
251                 switch (c) {
252
253                 case 'f':
254                         force = true;
255                         break;
256
257                 case 'o':
258                         outfile = optarg;
259                         break;
260
261                 case 'V':
262                         printf ("ardour-utils version %s\n\n", VERSIONSTRING);
263                         printf ("Copyright (C) GPL 2015 Robin Gareus <robin@gareus.org>\n");
264                         exit (0);
265                         break;
266
267                 case 'h':
268                         usage (0);
269                         break;
270
271                 default:
272                         usage (EXIT_FAILURE);
273                         break;
274                 }
275         }
276
277         if (optind + 2 > argc) {
278                 usage (EXIT_FAILURE);
279         }
280         std::cout << UTILNAME << ": hello" << std::endl;
281
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"));
286
287         XMLTree* state_tree;
288
289         std::string xmlpath(argv[optind]);
290
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;
295                         return EXIT_FAILURE;
296                 }
297         } else {
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;
301                         return EXIT_FAILURE;
302                 }
303         }
304
305         xmlpath = Glib::build_filename (xmlpath, legalize_for_path (snapshot_name) + pending_suffix);
306
307         if (Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
308
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;
311
312         }
313
314         xmlpath = Glib::build_filename (argv[optind], argv[optind+1]);
315
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;
320                         return EXIT_FAILURE;
321                 }
322         }
323
324         state_tree = new XMLTree;
325
326         bool writable = PBD::exists_and_writable (xmlpath) && PBD::exists_and_writable(Glib::path_get_dirname(xmlpath));
327
328         if (!writable) {
329                 std::cout << UTILNAME << ": Error : The session directory must exist and be writable." << std::endl;
330                 return -1;
331         }
332
333         if (!state_tree->read (xmlpath)) {
334                 std::cout << UTILNAME << ": Could not understand session file " << xmlpath << std::endl;
335                 delete state_tree;
336                 state_tree = 0;
337                 return EXIT_FAILURE;
338         }
339
340         XMLNode const & root (*state_tree->root());
341
342         if (root.name() != X_("Session")) {
343                 std::cout << UTILNAME << ": Session file " << xmlpath<< " is not a session" << std::endl;
344                 delete state_tree;
345                 state_tree = 0;
346                 return EXIT_FAILURE;
347         }
348
349         XMLProperty const * prop;
350
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;
354                 return EXIT_FAILURE;
355         } else {
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;
359                         return EXIT_FAILURE;
360                 } else {
361                         PBD::Stateful::loading_state_version = atoi (prop->value().c_str());
362                 }
363         }
364
365         std::cout <<  UTILNAME << ": Checking snapshot : " << snapshot_name << " in directory : " << session_dir->root_path() << std::endl;
366
367         bool midi_regions_use_bbt_beats = false;
368
369         if (PBD::Stateful::loading_state_version == 3002 && writable) {
370                 XMLNode* child;
371                 if ((child = find_named_node (root, "ProgramVersion")) != 0) {
372                         if ((prop = child->property ("modified-with")) != 0) {
373                                 std::string modified_with = prop->value ();
374
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());
377
378                                 if (modified_with_version <= 5.3 && !(modified_with_version == 5.3 && modified_with_revision >= 42)) {
379                                         midi_regions_use_bbt_beats = true;
380                                 }
381                         }
382                 }
383         }
384
385         XMLNode* tm_node;
386         bool all_metrum_divisors_are_quarters = true;
387         list<double> divisor_list;
388
389         if ((tm_node = find_named_node (root, "TempoMap")) != 0) {
390                 XMLNodeList metrum;
391                 XMLNodeConstIterator niter;
392                 metrum = tm_node->children();
393                 for (niter = metrum.begin(); niter != metrum.end(); ++niter) {
394                         XMLNode* child = *niter;
395
396                         if (child->name() == MeterSection::xml_state_node_name && (prop = child->property ("divisions-per-bar")) != 0) {
397                                 double divisions_per_bar;
398
399                                 if (sscanf (prop->value().c_str(), "%lf", &divisions_per_bar) ==1) {
400
401                                         if (divisions_per_bar != 4.0) {
402                                                 all_metrum_divisors_are_quarters = false;
403                                                 divisor_list.push_back (divisions_per_bar);
404                                         }
405                                 }
406                         }
407                 }
408         } else {
409                 std::cout << UTILNAME << ": Session file " <<  xmlpath << " has no TempoMap node. exiting." << std::endl;
410                 return EXIT_FAILURE;
411         }
412
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;
415                 return EXIT_FAILURE;
416         }
417
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();
421
422         if (divisor_list.size() == 1) {
423                 new_source_file_per_source = true;
424         }
425
426         if (midi_regions_use_bbt_beats || force) {
427                 if (force) {
428                         std::cout << UTILNAME << ": Forced update of snapshot : " << argv[optind+1] << std::endl;
429                 }
430
431                 SessionUtils::init();
432                 Session* s = 0;
433
434                 std::cout <<  UTILNAME << ": Loading snapshot." << std::endl;
435
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;
439
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();
444                                 return EXIT_FAILURE;
445                         }
446                 } else {
447                         std::cout << UTILNAME << ": Will create one MIDI file per midi region." << std::endl;
448
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();
453                                 return EXIT_FAILURE;
454                         }
455                 }
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;
460
461                 } else {
462                         s->save_state (outfile);
463                         std::cout << UTILNAME << ": Saved new snapshot: " << outfile.c_str() << " in " << session_dir << std::endl;
464                 }
465
466                 SessionUtils::unload_session(s);
467                 SessionUtils::cleanup();
468                 std::cout << UTILNAME << ": Finished." << std::endl;
469         } else {
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;
471         }
472
473         return 0;
474 }