2 * Copyright (C) 2008-2013 Sakari Bergen <sakari.bergen@beatwaves.net>
3 * Copyright (C) 2008-2017 Paul Davis <paul@linuxaudiosystems.com>
4 * Copyright (C) 2009-2012 Carl Hetherington <carl@carlh.net>
5 * Copyright (C) 2009-2014 David Robillard <d@drobilla.net>
6 * Copyright (C) 2013-2015 Colin Fletcher <colin.m.fletcher@googlemail.com>
7 * Copyright (C) 2015-2019 Robin Gareus <robin@gareus.org>
8 * Copyright (C) 2015 Johannes Mueller <github@johannes-mueller.org>
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
20 * You should have received a copy of the GNU General Public License along
21 * with this program; if not, write to the Free Software Foundation, Inc.,
22 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25 #include "pbd/gstdio_compat.h"
27 #include <glibmm/convert.h>
29 #include "pbd/convert.h"
31 #include "ardour/audioengine.h"
32 #include "ardour/audiofile_tagger.h"
33 #include "ardour/audio_port.h"
34 #include "ardour/debug.h"
35 #include "ardour/export_graph_builder.h"
36 #include "ardour/export_handler.h"
37 #include "ardour/export_timespan.h"
38 #include "ardour/export_channel_configuration.h"
39 #include "ardour/export_status.h"
40 #include "ardour/export_format_specification.h"
41 #include "ardour/export_filename.h"
42 #include "ardour/soundcloud_upload.h"
43 #include "ardour/system_exec.h"
44 #include "pbd/openuri.h"
45 #include "pbd/basename.h"
46 #include "ardour/session_metadata.h"
56 /*** ExportElementFactory ***/
58 ExportElementFactory::ExportElementFactory (Session & session) :
64 ExportElementFactory::~ExportElementFactory ()
70 ExportElementFactory::add_timespan ()
72 return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.sample_rate()));
75 ExportChannelConfigPtr
76 ExportElementFactory::add_channel_config ()
78 return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
82 ExportElementFactory::add_format ()
84 return ExportFormatSpecPtr (new ExportFormatSpecification (session));
88 ExportElementFactory::add_format (XMLNode const & state)
90 return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
94 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
96 return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
100 ExportElementFactory::add_filename ()
102 return ExportFilenamePtr (new ExportFilename (session));
106 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
108 return ExportFilenamePtr (new ExportFilename (*other));
111 /*** ExportHandler ***/
113 ExportHandler::ExportHandler (Session & session)
114 : ExportElementFactory (session)
116 , graph_builder (new ExportGraphBuilder (session))
117 , export_status (session.get_export_status ())
118 , post_processing (false)
124 ExportHandler::~ExportHandler ()
126 graph_builder->cleanup (export_status->aborted () );
129 /** Add an export to the `to-do' list */
131 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
132 ExportFormatSpecPtr format, ExportFilenamePtr filename,
133 BroadcastInfoPtr broadcast_info)
135 FileSpec spec (channel_config, format, filename, broadcast_info);
136 config_map.insert (make_pair (timespan, spec));
142 ExportHandler::do_export ()
144 /* Count timespans */
146 export_status->init();
147 std::set<ExportTimespanPtr> timespan_set;
148 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
149 bool new_timespan = timespan_set.insert (it->first).second;
151 export_status->total_samples += it->first->get_length();
154 export_status->total_timespans = timespan_set.size();
156 if (export_status->total_timespans > 1) {
157 // always include timespan if there's more than one.
158 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
159 FileSpec & spec = it->second;
160 spec.filename->include_timespan = true;
166 Glib::Threads::Mutex::Lock l (export_status->lock());
171 ExportHandler::start_timespan ()
173 export_status->timespan++;
175 if (config_map.empty()) {
176 // freewheeling has to be stopped from outside the process cycle
177 export_status->set_running (false);
181 /* finish_timespan pops the config_map entry that has been done, so
182 this is the timespan to do this time
184 current_timespan = config_map.begin()->first;
186 export_status->total_samples_current_timespan = current_timespan->get_length();
187 export_status->timespan_name = current_timespan->name();
188 export_status->processed_samples_current_timespan = 0;
190 /* Register file configurations to graph builder */
192 /* Here's the config_map entries that use this timespan */
193 timespan_bounds = config_map.equal_range (current_timespan);
194 graph_builder->reset ();
195 graph_builder->set_current_timespan (current_timespan);
196 handle_duplicate_format_extensions();
197 bool realtime = current_timespan->realtime ();
198 bool region_export = true;
199 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
200 // Filenames can be shared across timespans
201 FileSpec & spec = it->second;
202 spec.filename->set_timespan (it->first);
203 switch (spec.channel_config->region_processing_type ()) {
204 case RegionExportChannelFactory::None:
205 case RegionExportChannelFactory::Processed:
206 region_export = false;
211 graph_builder->add_config (spec, realtime);
214 // ExportDialog::update_realtime_selection does not allow this
215 assert (!region_export || !realtime);
219 post_processing = false;
220 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
221 process_position = current_timespan->get_start();
222 // TODO check if it's a RegionExport.. set flag to skip process_without_events()
223 session.start_audio_export (process_position, realtime, region_export);
227 ExportHandler::handle_duplicate_format_extensions()
229 typedef std::map<std::string, int> ExtCountMap;
232 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
233 if (it->second.filename->include_channel_config && it->second.channel_config) {
234 /* stem-export has multiple files in the same timestamp, but a different channel_config for each.
235 * However channel_config is only set in ExportGraphBuilder::Encoder::init_writer()
236 * so we cannot yet use it->second.filename->get_path(it->second.format).
237 * We have to explicily check uniqueness of "channel-config + extension" here:
239 counts[it->second.channel_config->name() + it->second.format->extension()]++;
241 counts[it->second.format->extension()]++;
245 bool duplicates_found = false;
246 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
247 if (it->second > 1) { duplicates_found = true; }
250 // Set this always, as the filenames are shared...
251 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
252 it->second.filename->include_format_name = duplicates_found;
257 ExportHandler::process (samplecnt_t samples)
259 if (!export_status->running ()) {
261 } else if (post_processing) {
262 Glib::Threads::Mutex::Lock l (export_status->lock());
263 if (AudioEngine::instance()->freewheeling ()) {
264 return post_process ();
266 // wait until we're freewheeling
270 Glib::Threads::Mutex::Lock l (export_status->lock());
271 return process_timespan (samples);
276 ExportHandler::process_timespan (samplecnt_t samples)
278 export_status->active_job = ExportStatus::Exporting;
279 /* update position */
281 samplecnt_t samples_to_read = 0;
282 samplepos_t const end = current_timespan->get_end();
284 bool const last_cycle = (process_position + samples >= end);
287 samples_to_read = end - process_position;
288 export_status->stop = true;
290 samples_to_read = samples;
293 process_position += samples_to_read;
294 export_status->processed_samples += samples_to_read;
295 export_status->processed_samples_current_timespan += samples_to_read;
297 /* Do actual processing */
298 int ret = graph_builder->process (samples_to_read, last_cycle);
300 /* Start post-processing/normalizing if necessary */
302 post_processing = graph_builder->need_postprocessing ();
303 if (post_processing) {
304 export_status->total_postprocessing_cycles = graph_builder->get_postprocessing_cycle_count();
305 export_status->current_postprocessing_cycle = 0;
316 ExportHandler::post_process ()
318 if (graph_builder->post_process ()) {
320 export_status->active_job = ExportStatus::Exporting;
322 if (graph_builder->realtime ()) {
323 export_status->active_job = ExportStatus::Encoding;
325 export_status->active_job = ExportStatus::Normalizing;
329 export_status->current_postprocessing_cycle++;
335 ExportHandler::command_output(std::string output, size_t size)
337 std::cerr << "command: " << size << ", " << output << std::endl;
338 info << output << endmsg;
342 ExportHandler::finish_timespan ()
344 graph_builder->get_analysis_results (export_status->result_map);
346 while (config_map.begin() != timespan_bounds.second) {
348 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
349 std::string filename = config_map.begin()->second.filename->get_path(fmt);
350 if (fmt->with_cue()) {
351 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
354 if (fmt->with_toc()) {
355 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
358 if (fmt->with_mp4chaps()) {
359 export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
362 Session::Exported (current_timespan->name(), filename); /* EMIT SIGNAL */
364 /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
365 * The process cannot access the file because it is being used.
366 * ditto for post-export and upload.
368 graph_builder->reset ();
371 /* TODO: check Umlauts and encoding in filename.
372 * TagLib eventually calls CreateFileA(),
374 export_status->active_job = ExportStatus::Tagging;
375 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
378 if (!fmt->command().empty()) {
379 SessionMetadata const & metadata (*SessionMetadata::Metadata());
381 #if 0 // would be nicer with C++11 initialiser...
382 std::map<char, std::string> subs {
384 { 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR },
385 { 'b', PBD::basename_nosuffix(filename) },
389 export_status->active_job = ExportStatus::Command;
390 PBD::ScopedConnection command_connection;
391 std::map<char, std::string> subs;
393 std::stringstream track_number;
394 track_number << metadata.track_number ();
395 std::stringstream total_tracks;
396 total_tracks << metadata.total_tracks ();
397 std::stringstream year;
398 year << metadata.year ();
400 subs.insert (std::pair<char, std::string> ('a', metadata.artist ()));
401 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
402 subs.insert (std::pair<char, std::string> ('c', metadata.copyright ()));
403 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
404 subs.insert (std::pair<char, std::string> ('f', filename));
405 subs.insert (std::pair<char, std::string> ('l', metadata.lyricist ()));
406 subs.insert (std::pair<char, std::string> ('n', session.name ()));
407 subs.insert (std::pair<char, std::string> ('s', session.path ()));
408 subs.insert (std::pair<char, std::string> ('o', metadata.conductor ()));
409 subs.insert (std::pair<char, std::string> ('t', metadata.title ()));
410 subs.insert (std::pair<char, std::string> ('z', metadata.organization ()));
411 subs.insert (std::pair<char, std::string> ('A', metadata.album ()));
412 subs.insert (std::pair<char, std::string> ('C', metadata.comment ()));
413 subs.insert (std::pair<char, std::string> ('E', metadata.engineer ()));
414 subs.insert (std::pair<char, std::string> ('G', metadata.genre ()));
415 subs.insert (std::pair<char, std::string> ('L', total_tracks.str ()));
416 subs.insert (std::pair<char, std::string> ('M', metadata.mixer ()));
417 subs.insert (std::pair<char, std::string> ('N', current_timespan->name())); // =?= config_map.begin()->first->name ()
418 subs.insert (std::pair<char, std::string> ('O', metadata.composer ()));
419 subs.insert (std::pair<char, std::string> ('P', metadata.producer ()));
420 subs.insert (std::pair<char, std::string> ('S', metadata.disc_subtitle ()));
421 subs.insert (std::pair<char, std::string> ('T', track_number.str ()));
422 subs.insert (std::pair<char, std::string> ('Y', year.str ()));
423 subs.insert (std::pair<char, std::string> ('Z', metadata.country ()));
425 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
426 info << "Post-export command line : {" << se->to_s () << "}" << endmsg;
427 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
428 int ret = se->start (SystemExec::MergeWithStdin);
430 // successfully started
431 while (se->is_running ()) {
432 // wait for system exec to terminate
436 error << "Post-export command FAILED with Error: " << ret << endmsg;
441 // XXX THIS IS IN REALTIME CONTEXT, CALLED FROM
442 // AudioEngine::process_callback()
443 // freewheeling, yes, but still uploading here is NOT
446 // even less so, since SoundcloudProgress is using
447 // connect_same_thread() - GUI updates from the RT thread
448 // will cause crashes. http://pastebin.com/UJKYNGHR
449 if (fmt->soundcloud_upload()) {
450 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
451 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
452 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
453 "uploading %1 - username=%2, password=%3, token=%4",
454 filename, soundcloud_username, soundcloud_password, token) );
455 std::string path = soundcloud_uploader->Upload (
457 PBD::basename_nosuffix(filename), // title
459 soundcloud_make_public,
460 soundcloud_downloadable,
463 if (path.length() != 0) {
464 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
465 if (soundcloud_open_page) {
466 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
467 open_uri(path.c_str()); // open the soundcloud website to the new file
470 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
472 delete soundcloud_uploader;
474 config_map.erase (config_map.begin());
481 ExportHandler::reset ()
484 graph_builder->reset ();
487 /*** CD Marker stuff ***/
489 struct LocationSortByStart {
490 bool operator() (Location *a, Location *b) {
491 return a->start() < b->start();
496 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
497 std::string filename, CDMarkerFormat format)
499 string filepath = get_cd_marker_filename(filename, format);
502 void (ExportHandler::*header_func) (CDMarkerStatus &);
503 void (ExportHandler::*track_func) (CDMarkerStatus &);
504 void (ExportHandler::*index_func) (CDMarkerStatus &);
508 header_func = &ExportHandler::write_toc_header;
509 track_func = &ExportHandler::write_track_info_toc;
510 index_func = &ExportHandler::write_index_info_toc;
513 header_func = &ExportHandler::write_cue_header;
514 track_func = &ExportHandler::write_track_info_cue;
515 index_func = &ExportHandler::write_index_info_cue;
518 header_func = &ExportHandler::write_mp4ch_header;
519 track_func = &ExportHandler::write_track_info_mp4ch;
520 index_func = &ExportHandler::write_index_info_mp4ch;
526 CDMarkerStatus status (filepath, timespan, file_format, filename);
528 (this->*header_func) (status);
530 /* Get locations and sort */
532 Locations::LocationList const & locations (session.locations()->list());
533 Locations::LocationList::const_iterator i;
534 Locations::LocationList temp;
536 for (i = locations.begin(); i != locations.end(); ++i) {
537 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
543 // TODO One index marker for whole thing
547 LocationSortByStart cmp;
549 Locations::LocationList::const_iterator nexti;
551 /* Start actual marker stuff */
553 samplepos_t last_end_time = timespan->get_start();
554 status.track_position = 0;
556 for (i = temp.begin(); i != temp.end(); ++i) {
560 if ((*i)->start() < last_end_time) {
561 if ((*i)->is_mark()) {
562 /* Index within track */
564 status.index_position = (*i)->start() - timespan->get_start();
565 (this->*index_func) (status);
571 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
573 status.track_position = last_end_time - timespan->get_start();
574 status.track_start_sample = (*i)->start() - timespan->get_start(); // everything before this is the pregap
575 status.track_duration = 0;
577 if ((*i)->is_mark()) {
578 // a mark track location needs to look ahead to the next marker's start to determine length
582 if (nexti != temp.end()) {
583 status.track_duration = (*nexti)->start() - last_end_time;
585 last_end_time = (*nexti)->start();
587 // this was the last marker, use timespan end
588 status.track_duration = timespan->get_end() - last_end_time;
590 last_end_time = timespan->get_end();
594 status.track_duration = (*i)->end() - last_end_time;
596 last_end_time = (*i)->end();
599 (this->*track_func) (status);
602 } catch (std::exception& e) {
603 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
604 ::g_unlink (filepath.c_str());
605 } catch (Glib::Exception& e) {
606 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
607 ::g_unlink (filepath.c_str());
612 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
614 /* do not strip file suffix because there may be more than one format,
615 and we do not want the CD marker file from one format to overwrite
616 another (e.g. foo.wav.cue > foo.aiff.cue)
621 return filename + ".toc";
623 return filename + ".cue";
626 unsigned lastdot = filename.find_last_of('.');
627 return filename.substr(0,lastdot) + ".chapters.txt";
630 return filename + ".marker"; // Should not be reached when actually creating a file
635 ExportHandler::write_cue_header (CDMarkerStatus & status)
637 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
640 string barcode = SessionMetadata::Metadata()->barcode();
641 string album_artist = SessionMetadata::Metadata()->album_artist();
642 string album_title = SessionMetadata::Metadata()->album();
644 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
647 status.out << "CATALOG " << barcode << endl;
649 if (album_artist != "")
650 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
652 if (album_title != "")
655 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
657 /* The original cue sheet spec mentions five file types
659 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
660 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
663 We try to use these file types whenever appropriate and
664 default to our own names otherwise.
666 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
667 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
668 status.out << "WAVE";
669 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
670 status.format->sample_format() == ExportFormatBase::SF_16 &&
671 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
672 // Format is RAW 16bit 44.1kHz
673 if (status.format->endianness() == ExportFormatBase::E_Little) {
674 status.out << "BINARY";
676 status.out << "MOTOROLA";
679 // no special case for AIFF format it's name is already "AIFF"
680 status.out << status.format->format_name();
686 ExportHandler::write_toc_header (CDMarkerStatus & status)
688 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
691 string barcode = SessionMetadata::Metadata()->barcode();
692 string album_artist = SessionMetadata::Metadata()->album_artist();
693 string album_title = SessionMetadata::Metadata()->album();
696 status.out << "CATALOG \"" << barcode << "\"" << endl;
698 if (album_title != "")
701 status.out << "CD_DA" << endl;
702 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
703 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
704 status.out << " PERFORMER " << toc_escape_cdtext (album_artist) << endl;
705 status.out << " }" << endl << "}" << endl;
709 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
711 status.out << "00:00:00.000 Intro" << endl;
715 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
719 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
720 status.out << buf << endl;
722 status.out << " FLAGS" ;
723 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
724 status.out << " SCMS ";
726 status.out << " DCP ";
729 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
730 status.out << " PRE";
734 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
735 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
738 if (status.marker->name() != "") {
739 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
742 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
743 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
746 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
747 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
750 if (status.track_position != status.track_start_sample) {
751 samples_to_cd_frame_string (buf, status.track_position);
752 status.out << " INDEX 00" << buf << endl;
755 samples_to_cd_frame_string (buf, status.track_start_sample);
756 status.out << " INDEX 01" << buf << endl;
758 status.index_number = 2;
759 status.track_number++;
763 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
767 status.out << endl << "TRACK AUDIO" << endl;
769 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
772 status.out << "COPY" << endl;
774 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
775 status.out << "PRE_EMPHASIS" << endl;
777 status.out << "NO PRE_EMPHASIS" << endl;
780 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
781 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
784 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
785 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
787 status.out << " PERFORMER ";
788 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
789 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
791 status.out << "\"\"" << endl;
794 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
795 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
798 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
799 status.out << " ISRC \"";
800 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
801 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
802 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
803 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
806 status.out << " }" << endl << "}" << endl;
808 samples_to_cd_frame_string (buf, status.track_position);
809 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
811 samples_to_cd_frame_string (buf, status.track_duration);
812 status.out << buf << endl;
814 samples_to_cd_frame_string (buf, status.track_start_sample - status.track_position);
815 status.out << "START" << buf << endl;
818 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
822 samples_to_chapter_marks_string(buf, status.track_start_sample);
823 status.out << buf << " " << status.marker->name() << endl;
827 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
831 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
833 samples_to_cd_frame_string (buf, status.index_position);
834 status.out << buf << endl;
840 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
844 samples_to_cd_frame_string (buf, status.index_position - status.track_start_sample);
845 status.out << "INDEX" << buf << endl;
849 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
854 ExportHandler::samples_to_cd_frame_string (char* buf, samplepos_t when)
856 samplecnt_t remainder;
857 samplecnt_t fr = session.nominal_sample_rate();
858 int mins, secs, samples;
860 mins = when / (60 * fr);
861 remainder = when - (mins * 60 * fr);
862 secs = remainder / fr;
863 remainder -= secs * fr;
864 samples = remainder / (fr / 75);
865 sprintf (buf, " %02d:%02d:%02d", mins, secs, samples);
869 ExportHandler::samples_to_chapter_marks_string (char* buf, samplepos_t when)
871 samplecnt_t remainder;
872 samplecnt_t fr = session.nominal_sample_rate();
873 int hours, mins, secs, msecs;
875 hours = when / (3600 * fr);
876 remainder = when - (hours * 3600 * fr);
877 mins = remainder / (60 * fr);
878 remainder -= mins * 60 * fr;
879 secs = remainder / fr;
880 remainder -= secs * fr;
881 msecs = (remainder * 1000) / fr;
882 sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
886 ExportHandler::toc_escape_cdtext (const std::string& txt)
888 Glib::ustring check (txt);
890 std::string latin1_txt;
894 latin1_txt = Glib::convert_with_fallback (txt, "ISO-8859-1", "UTF-8", "_");
895 } catch (Glib::ConvertError& err) {
896 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
901 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
905 } else if ((*c) == '\\') {
907 } else if (isprint (*c)) {
910 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
921 ExportHandler::toc_escape_filename (const std::string& txt)
927 // We iterate byte-wise not character-wise over a UTF-8 string here,
928 // because we only want to translate backslashes and double quotes
929 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
933 } else if (*c == '\\') {
946 ExportHandler::cue_escape_cdtext (const std::string& txt)
948 std::string latin1_txt;
952 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
953 } catch (Glib::ConvertError& err) {
954 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
957 // does not do much mor than UTF-8 to Latin1 translation yet, but
958 // that may have to change if cue parsers in burning programs change
959 out = '"' + latin1_txt + '"';
964 ExportHandler::CDMarkerStatus::~CDMarkerStatus () {
965 if (!g_file_set_contents (path.c_str(), out.str().c_str(), -1, NULL)) {
966 PBD::error << string_compose(("Editor: cannot open \"%1\" as export file for CD marker file"), path) << endmsg;
970 } // namespace ARDOUR