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