fix up escaping of strings in TOC files (suggested by andreas ruge)
[ardour.git] / libs / ardour / export_handler.cc
1 /*
2     Copyright (C) 2008-2009 Paul Davis
3     Author: Sakari Bergen
4
5     This program is free software; you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation; either version 2 of the License, or
8     (at your option) any later version.
9
10     This program is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14
15     You should have received a copy of the GNU General Public License
16     along with this program; if not, write to the Free Software
17     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18
19 */
20
21 #include "ardour/export_handler.h"
22
23 #include <glibmm.h>
24
25 #include "pbd/convert.h"
26 #include "pbd/filesystem.h"
27
28 #include "ardour/ardour.h"
29 #include "ardour/configuration.h"
30 #include "ardour/export_graph_builder.h"
31 #include "ardour/export_timespan.h"
32 #include "ardour/export_channel_configuration.h"
33 #include "ardour/export_status.h"
34 #include "ardour/export_format_specification.h"
35 #include "ardour/export_filename.h"
36 #include "ardour/export_failed.h"
37
38 #include "i18n.h"
39
40 using namespace std;
41 using namespace PBD;
42
43 namespace ARDOUR
44 {
45
46 /*** ExportElementFactory ***/
47
48 ExportElementFactory::ExportElementFactory (Session & session) :
49   session (session)
50 {
51
52 }
53
54 ExportElementFactory::~ExportElementFactory ()
55 {
56
57 }
58
59 ExportTimespanPtr
60 ExportElementFactory::add_timespan ()
61 {
62         return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
63 }
64
65 ExportChannelConfigPtr
66 ExportElementFactory::add_channel_config ()
67 {
68         return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
69 }
70
71 ExportFormatSpecPtr
72 ExportElementFactory::add_format ()
73 {
74         return ExportFormatSpecPtr (new ExportFormatSpecification (session));
75 }
76
77 ExportFormatSpecPtr
78 ExportElementFactory::add_format (XMLNode const & state)
79 {
80         return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
81 }
82
83 ExportFormatSpecPtr
84 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
85 {
86         return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
87 }
88
89 ExportFilenamePtr
90 ExportElementFactory::add_filename ()
91 {
92         return ExportFilenamePtr (new ExportFilename (session));
93 }
94
95 ExportFilenamePtr
96 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
97 {
98         return ExportFilenamePtr (new ExportFilename (*other));
99 }
100
101 /*** ExportHandler ***/
102
103 ExportHandler::ExportHandler (Session & session)
104   : ExportElementFactory (session)
105   , session (session)
106   , graph_builder (new ExportGraphBuilder (session))
107   , export_status (session.get_export_status ())
108   , realtime (false)
109   , normalizing (false)
110   , cue_tracknum (0)
111   , cue_indexnum (0)
112 {
113 }
114
115 ExportHandler::~ExportHandler ()
116 {
117         // TODO remove files that were written but not finsihed
118 }
119
120 bool
121 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
122                                   ExportFormatSpecPtr format, ExportFilenamePtr filename,
123                                   BroadcastInfoPtr broadcast_info)
124 {
125         FileSpec spec (channel_config, format, filename, broadcast_info);
126         ConfigPair pair (timespan, spec);
127         config_map.insert (pair);
128
129         return true;
130 }
131
132 void
133 ExportHandler::do_export (bool rt)
134 {
135         /* Count timespans */
136
137         export_status->init();
138         std::set<ExportTimespanPtr> timespan_set;
139         for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
140                 timespan_set.insert (it->first);
141                 export_status->total_frames += it->first->get_length();
142         }
143         export_status->total_timespans = timespan_set.size();
144
145         /* Start export */
146
147         realtime = rt;
148         start_timespan ();
149 }
150
151 void
152 ExportHandler::start_timespan ()
153 {
154         export_status->timespan++;
155
156         if (config_map.empty()) {
157                 // freewheeling has to be stopped from outside the process cycle
158                 export_status->running = false;
159                 return;
160         }
161
162         current_timespan = config_map.begin()->first;
163
164         /* Register file configurations to graph builder */
165
166         timespan_bounds = config_map.equal_range (current_timespan);
167         graph_builder->reset ();
168         graph_builder->set_current_timespan (current_timespan);
169         for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
170                 // Filenames can be shared across timespans
171                 FileSpec & spec = it->second;
172                 spec.filename->set_timespan (it->first);
173                 graph_builder->add_config (spec);
174         }
175
176         /* start export */
177
178         normalizing = false;
179         session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
180         process_position = current_timespan->get_start();
181         session.start_audio_export (process_position, realtime);
182 }
183
184 int
185 ExportHandler::process (framecnt_t frames)
186 {
187         if (!export_status->running) {
188                 return 0;
189         } else if (normalizing) {
190                 return process_normalize ();
191         } else {
192                 return process_timespan (frames);
193         }
194 }
195
196 int
197 ExportHandler::process_timespan (framecnt_t frames)
198 {
199         /* update position */
200
201         framecnt_t frames_to_read = 0;
202         framepos_t const end = current_timespan->get_end();
203
204         bool const last_cycle = (process_position + frames >= end);
205
206         if (last_cycle) {
207                 frames_to_read = end - process_position;
208                 export_status->stop = true;
209                 normalizing = true;
210         } else {
211                 frames_to_read = frames;
212         }
213
214         process_position += frames_to_read;
215         export_status->processed_frames += frames_to_read;
216         export_status->progress = (float) export_status->processed_frames / export_status->total_frames;
217
218         /* Do actual processing */
219
220         return graph_builder->process (frames_to_read, last_cycle);
221 }
222
223 int
224 ExportHandler::process_normalize ()
225 {
226         if (graph_builder->process_normalize ()) {
227                 finish_timespan ();
228                 export_status->normalizing = false;
229         } else {
230                 export_status->normalizing = true;
231         }
232
233         return 0;
234 }
235
236 void
237 ExportHandler::finish_timespan ()
238 {
239         while (config_map.begin() != timespan_bounds.second) {
240
241                 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
242
243                 if (fmt->with_cue()) {
244                         export_cd_marker_file (current_timespan, fmt, config_map.begin()->second.filename->get_path(fmt), CDMarkerCUE);
245                 } 
246
247                 if (fmt->with_toc()) {
248                         export_cd_marker_file (current_timespan, fmt, config_map.begin()->second.filename->get_path(fmt), CDMarkerTOC);
249                 }
250
251                 config_map.erase (config_map.begin());
252         }
253
254         start_timespan ();
255 }
256
257 /*** CD Marker sutff ***/
258
259 struct LocationSortByStart {
260     bool operator() (Location *a, Location *b) {
261             return a->start() < b->start();
262     }
263 };
264
265 void
266 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
267                                       std::string filename, CDMarkerFormat format)
268 {
269         string filepath = get_cd_marker_filename(filename, format);
270
271         void (ExportHandler::*header_func) (CDMarkerStatus &);
272         void (ExportHandler::*track_func) (CDMarkerStatus &);
273         void (ExportHandler::*index_func) (CDMarkerStatus &);
274
275         switch (format) {
276           case CDMarkerTOC:
277                 header_func = &ExportHandler::write_toc_header;
278                 track_func = &ExportHandler::write_track_info_toc;
279                 index_func = &ExportHandler::write_index_info_toc;
280                 break;
281           case CDMarkerCUE:
282                 header_func = &ExportHandler::write_cue_header;
283                 track_func = &ExportHandler::write_track_info_cue;
284                 index_func = &ExportHandler::write_index_info_cue;
285                 break;
286           default:
287                 return;
288         }
289
290         CDMarkerStatus status (filepath, timespan, file_format, filename);
291
292         if (!status.out) {
293                 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
294                 return;
295         }
296
297         (this->*header_func) (status);
298
299         /* Get locations and sort */
300
301         Locations::LocationList const & locations (session.locations()->list());
302         Locations::LocationList::const_iterator i;
303         Locations::LocationList temp;
304
305         for (i = locations.begin(); i != locations.end(); ++i) {
306                 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
307                         temp.push_back (*i);
308                 }
309         }
310
311         if (temp.empty()) {
312                 // TODO One index marker for whole thing
313                 return;
314         }
315
316         LocationSortByStart cmp;
317         temp.sort (cmp);
318         Locations::LocationList::const_iterator nexti;
319
320         /* Start actual marker stuff */
321
322         framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
323         status.track_position = last_start_time - timespan->get_start();
324
325         for (i = temp.begin(); i != temp.end(); ++i) {
326
327                 status.marker = *i;
328
329                 if ((*i)->start() < last_end_time) {
330                         if ((*i)->is_mark()) {
331                                 /* Index within track */
332
333                                 status.index_position = (*i)->start() - timespan->get_start();
334                                 (this->*index_func) (status);
335                         }
336
337                         continue;
338                 }
339
340                 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
341
342                 status.track_position = last_end_time - timespan->get_start();
343                 status.track_start_frame = (*i)->start() - timespan->get_start();  // everything before this is the pregap
344                 status.track_duration = 0;
345
346                 if ((*i)->is_mark()) {
347                         // a mark track location needs to look ahead to the next marker's start to determine length
348                         nexti = i;
349                         ++nexti;
350
351                         if (nexti != temp.end()) {
352                                 status.track_duration = (*nexti)->start() - last_end_time;
353
354                                 last_start_time = (*i)->start();
355                                 last_end_time = (*nexti)->start();
356                         } else {
357                                 // this was the last marker, use timespan end
358                                 status.track_duration = timespan->get_end() - last_end_time;
359
360                                 last_start_time = (*i)->start();
361                                 last_end_time = timespan->get_end();
362                         }
363                 } else {
364                         // range
365                         status.track_duration = (*i)->end() - last_end_time;
366
367                         last_start_time = (*i)->start();
368                         last_end_time = (*i)->end();
369                 }
370
371                 (this->*track_func) (status);
372         }
373 }
374
375 string
376 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
377 {
378         /* do not strip file suffix because there may be more than one format, 
379            and we do not want the CD marker file from one format to overwrite
380            another (e.g. foo.wav.cue > foo.aiff.cue)
381         */
382
383         switch (format) {
384           case CDMarkerTOC:
385                 return filename + ".toc";
386           case CDMarkerCUE:
387                 return filename + ".cue";
388           default:
389                 return filename + ".marker"; // Should not be reached when actually creating a file
390         }
391 }
392
393 void
394 ExportHandler::write_cue_header (CDMarkerStatus & status)
395 {
396         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
397
398         status.out << "REM Cue file generated by Ardour" << endl;
399         status.out << "TITLE \"" << title << "\"" << endl;
400
401         /*  The cue sheet syntax has originally five file types:
402                 WAVE     : 44.1 kHz, 16 Bit (little endian)
403                 AIFF     : 44.1 kHz, 16 Bit (big endian)
404                 BINARY   : 44.1 kHz, 16 Bit (little endian)
405                 MOTOROLA : 44.1 kHz, 16 Bit (big endian)
406                 MP3
407
408                 We want to use cue sheets not only as CD images but also as general playlyist
409                 format, thus for WAVE and AIFF we don't care if it's really 44.1 kHz/16 Bit, the
410                 soundfile's header shows it anyway.  But for the raw formats, i.e. BINARY
411                 and MOTOROLA we do care, because no header would tell us about a different format.
412
413                 For all other formats we just make up our own file type.  MP3 is not supported
414                 at the moment.
415         */
416
417         status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
418         if (!status.format->format_name().compare ("WAV")) {
419                 status.out  << "WAVE";
420         } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
421                    status.format->sample_format() == ExportFormatBase::SF_16 &&
422                    status.format->sample_rate() == ExportFormatBase::SR_44_1) {
423                 // Format is RAW 16bit 44.1kHz
424                 if (status.format->endianness() == ExportFormatBase::E_Little) {
425                         status.out << "BINARY";
426                 } else {
427                         status.out << "MOTOROLA";
428                 }
429         } else {
430                 // AIFF should return "AIFF"
431                 status.out << status.format->format_name();
432         }
433         status.out << endl;
434 }
435
436 void
437 ExportHandler::write_toc_header (CDMarkerStatus & status)
438 {
439         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
440
441         status.out << "CD_DA" << endl;
442         status.out << "CD_TEXT {" << endl << "  LANGUAGE_MAP {" << endl << "    0 : EN" << endl << "  }" << endl;
443         status.out << "  LANGUAGE 0 {" << endl << "    TITLE " << toc_escape_string (title) << endl ;
444         status.out << "    PERFORMER \"\"" << endl << "  }" << endl << "}" << endl;
445 }
446
447 void
448 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
449 {
450         gchar buf[18];
451
452         snprintf (buf, sizeof(buf), "  TRACK %02d AUDIO", status.track_number);
453         status.out << buf << endl;
454
455         status.out << "    FLAGS" ;
456         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
457                 status.out << " SCMS ";
458         } else {
459                 status.out << " DCP ";
460         }
461
462         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
463                 status.out << " PRE";
464         }
465         status.out << endl;
466
467         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
468                 status.out << "    ISRC " << status.marker->cd_info["isrc"] << endl;
469
470         }
471         if (status.marker->name() != "") {
472                 status.out << "    TITLE \"" << status.marker->name() << "\"" << endl;
473         }
474
475         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
476                 status.out << "    PERFORMER \"" <<  status.marker->cd_info["performer"] << "\"" << endl;
477         }
478
479         if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
480                 status.out << "    SONGWRITER \"" << status.marker->cd_info["composer"]  << "\"" << endl;
481         }
482
483         if (status.track_position != status.track_start_frame) {
484                 frames_to_cd_frames_string (buf, status.track_position);
485                 status.out << "    INDEX 00" << buf << endl;
486         }
487
488         frames_to_cd_frames_string (buf, status.track_start_frame);
489         status.out << "    INDEX 01" << buf << endl;
490
491         status.index_number = 2;
492         status.track_number++;
493 }
494
495 void
496 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
497 {
498         gchar buf[18];
499
500         status.out << endl << "TRACK AUDIO" << endl;
501
502         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
503                 status.out << "NO ";
504         }
505         status.out << "COPY" << endl;
506
507         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
508                 status.out << "PRE_EMPHASIS" << endl;
509         } else {
510                 status.out << "NO PRE_EMPHASIS" << endl;
511         }
512
513         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
514                 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
515         }
516
517         status.out << "CD_TEXT {" << endl << "  LANGUAGE 0 {" << endl << "     TITLE "
518                    << toc_escape_string (status.marker->name()) << endl;
519         
520         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
521                 status.out << "     PERFORMER " << toc_escape_string (status.marker->cd_info["performer"]);
522         } else {
523                 status.out << "     PERFORMER \"\"";
524         }
525         
526         if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
527                 status.out  << "     COMPOSER " << toc_escape_string (status.marker->cd_info["composer"]) << endl;
528         }
529
530         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
531                 status.out  << "     ISRC \"";
532                 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
533                 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
534                 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
535                 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
536         }
537
538         status.out << "  }" << endl << "}" << endl;
539
540         frames_to_cd_frames_string (buf, status.track_position);
541         status.out << "FILE " << toc_escape_string (status.filename) << ' ' << buf;
542
543         frames_to_cd_frames_string (buf, status.track_duration);
544         status.out << buf << endl;
545
546         frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
547         status.out << "START" << buf << endl;
548 }
549
550 void
551 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
552 {
553         gchar buf[18];
554
555         snprintf (buf, sizeof(buf), "    INDEX %02d", cue_indexnum);
556         status.out << buf;
557         frames_to_cd_frames_string (buf, status.index_position);
558         status.out << buf << endl;
559
560         cue_indexnum++;
561 }
562
563 void
564 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
565 {
566         gchar buf[18];
567
568         frames_to_cd_frames_string (buf, status.index_position - status.track_position);
569         status.out << "INDEX" << buf << endl;
570 }
571
572 void
573 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
574 {
575         framecnt_t remainder;
576         framecnt_t fr = session.nominal_frame_rate();
577         int mins, secs, frames;
578
579         mins = when / (60 * fr);
580         remainder = when - (mins * 60 * fr);
581         secs = remainder / fr;
582         remainder -= secs * fr;
583         frames = remainder / (fr / 75);
584         sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
585 }
586
587 std::string
588 ExportHandler::toc_escape_string (const std::string& txt)
589 {
590         Glib::ustring utxt (txt);
591         Glib::ustring out;
592         char buf[5];
593
594         out = '"';
595
596         for (Glib::ustring::iterator c = utxt.begin(); c != utxt.end(); ++c) {
597
598                 if ((*c) == '"') {
599                         out += "\\\"";
600                 } else if (g_unichar_isprint (*c)) {
601                         out += *c;
602                 } else {
603                         /* this isn't really correct */
604                         snprintf (buf, sizeof (buf), "\\%03o", *c);
605                         out += buf;
606                 }
607         }
608         
609         out += '"';
610
611         return std::string (out);
612 }
613
614 } // namespace ARDOUR