2 Copyright (C) 2008-2009 Paul Davis
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.
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.
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.
21 #include "ardour/export_handler.h"
23 #include "pbd/gstdio_compat.h"
25 #include <glibmm/convert.h>
27 #include "pbd/convert.h"
29 #include "ardour/audioengine.h"
30 #include "ardour/audiofile_tagger.h"
31 #include "ardour/audio_port.h"
32 #include "ardour/debug.h"
33 #include "ardour/export_graph_builder.h"
34 #include "ardour/export_timespan.h"
35 #include "ardour/export_channel_configuration.h"
36 #include "ardour/export_status.h"
37 #include "ardour/export_format_specification.h"
38 #include "ardour/export_filename.h"
39 #include "ardour/soundcloud_upload.h"
40 #include "ardour/system_exec.h"
41 #include "pbd/openuri.h"
42 #include "pbd/basename.h"
43 #include "ardour/session_metadata.h"
53 /*** ExportElementFactory ***/
55 ExportElementFactory::ExportElementFactory (Session & session) :
61 ExportElementFactory::~ExportElementFactory ()
67 ExportElementFactory::add_timespan ()
69 return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
72 ExportChannelConfigPtr
73 ExportElementFactory::add_channel_config ()
75 return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
79 ExportElementFactory::add_format ()
81 return ExportFormatSpecPtr (new ExportFormatSpecification (session));
85 ExportElementFactory::add_format (XMLNode const & state)
87 return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
91 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
93 return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
97 ExportElementFactory::add_filename ()
99 return ExportFilenamePtr (new ExportFilename (session));
103 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
105 return ExportFilenamePtr (new ExportFilename (*other));
108 /*** ExportHandler ***/
110 ExportHandler::ExportHandler (Session & session)
111 : ExportElementFactory (session)
113 , graph_builder (new ExportGraphBuilder (session))
114 , export_status (session.get_export_status ())
115 , post_processing (false)
121 ExportHandler::~ExportHandler ()
123 graph_builder->cleanup (export_status->aborted () );
126 /** Add an export to the `to-do' list */
128 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
129 ExportFormatSpecPtr format, ExportFilenamePtr filename,
130 BroadcastInfoPtr broadcast_info)
132 FileSpec spec (channel_config, format, filename, broadcast_info);
133 config_map.insert (make_pair (timespan, spec));
139 ExportHandler::do_export ()
141 /* Count timespans */
143 export_status->init();
144 std::set<ExportTimespanPtr> timespan_set;
145 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
146 bool new_timespan = timespan_set.insert (it->first).second;
148 export_status->total_frames += it->first->get_length();
151 export_status->total_timespans = timespan_set.size();
153 if (export_status->total_timespans > 1) {
154 // always include timespan if there's more than one.
155 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
156 FileSpec & spec = it->second;
157 spec.filename->include_timespan = true;
163 Glib::Threads::Mutex::Lock l (export_status->lock());
168 ExportHandler::start_timespan ()
170 export_status->timespan++;
172 if (config_map.empty()) {
173 // freewheeling has to be stopped from outside the process cycle
174 export_status->set_running (false);
178 /* finish_timespan pops the config_map entry that has been done, so
179 this is the timespan to do this time
181 current_timespan = config_map.begin()->first;
183 export_status->total_frames_current_timespan = current_timespan->get_length();
184 export_status->timespan_name = current_timespan->name();
185 export_status->processed_frames_current_timespan = 0;
187 /* Register file configurations to graph builder */
189 /* Here's the config_map entries that use this timespan */
190 timespan_bounds = config_map.equal_range (current_timespan);
191 graph_builder->reset ();
192 graph_builder->set_current_timespan (current_timespan);
193 handle_duplicate_format_extensions();
194 bool realtime = current_timespan->realtime ();
195 bool region_export = true;
196 bool incl_master_bus = false;
197 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
198 // Filenames can be shared across timespans
199 FileSpec & spec = it->second;
200 spec.filename->set_timespan (it->first);
201 switch (spec.channel_config->region_processing_type ()) {
202 case RegionExportChannelFactory::None:
203 case RegionExportChannelFactory::Processed:
204 region_export = false;
209 #if 1 // hack alert -- align master bus, compensate master latency
211 /* there's no easier way to get this information here.
212 * Ports are configured in the PortExportChannelSelector GUI,
213 * This ExportHandler has no context of routes.
215 boost::shared_ptr<Route> master_bus = session.master_out ();
217 const PortSet& ps = master_bus->output ()->ports();
219 const ExportChannelConfiguration::ChannelList& channels = spec.channel_config->get_channels ();
220 for (ExportChannelConfiguration::ChannelList::const_iterator it = channels.begin(); it != channels.end(); ++it) {
222 boost::shared_ptr <PortExportChannel> pep = boost::dynamic_pointer_cast<PortExportChannel> (*it);
226 PortExportChannel::PortSet const& ports = pep->get_ports ();
227 for (PortExportChannel::PortSet::const_iterator it = ports.begin(); it != ports.end(); ++it) {
228 boost::shared_ptr<AudioPort> ap = (*it).lock();
229 if (ps.contains (ap)) {
230 incl_master_bus = true;
236 graph_builder->add_config (spec, realtime);
239 // ExportDialog::update_realtime_selection does not allow this
240 assert (!region_export || !realtime);
244 post_processing = false;
245 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
246 process_position = current_timespan->get_start();
247 // TODO check if it's a RegionExport.. set flag to skip process_without_events()
248 session.start_audio_export (process_position, realtime, region_export, incl_master_bus);
252 ExportHandler::handle_duplicate_format_extensions()
254 typedef std::map<std::string, int> ExtCountMap;
257 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
258 counts[it->second.format->extension()]++;
261 bool duplicates_found = false;
262 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
263 if (it->second > 1) { duplicates_found = true; }
266 // Set this always, as the filenames are shared...
267 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
268 it->second.filename->include_format_name = duplicates_found;
273 ExportHandler::process (framecnt_t frames)
275 if (!export_status->running ()) {
277 } else if (post_processing) {
278 Glib::Threads::Mutex::Lock l (export_status->lock());
279 if (AudioEngine::instance()->freewheeling ()) {
280 return post_process ();
282 // wait until we're freewheeling
286 Glib::Threads::Mutex::Lock l (export_status->lock());
287 return process_timespan (frames);
292 ExportHandler::process_timespan (framecnt_t frames)
294 export_status->active_job = ExportStatus::Exporting;
295 /* update position */
297 framecnt_t frames_to_read = 0;
298 framepos_t const end = current_timespan->get_end();
300 bool const last_cycle = (process_position + frames >= end);
303 frames_to_read = end - process_position;
304 export_status->stop = true;
306 frames_to_read = frames;
309 process_position += frames_to_read;
310 export_status->processed_frames += frames_to_read;
311 export_status->processed_frames_current_timespan += frames_to_read;
313 /* Do actual processing */
314 int ret = graph_builder->process (frames_to_read, last_cycle);
316 /* Start post-processing/normalizing if necessary */
318 post_processing = graph_builder->need_postprocessing ();
319 if (post_processing) {
320 export_status->total_postprocessing_cycles = graph_builder->get_postprocessing_cycle_count();
321 export_status->current_postprocessing_cycle = 0;
332 ExportHandler::post_process ()
334 if (graph_builder->post_process ()) {
336 export_status->active_job = ExportStatus::Exporting;
338 if (graph_builder->realtime ()) {
339 export_status->active_job = ExportStatus::Encoding;
341 export_status->active_job = ExportStatus::Normalizing;
345 export_status->current_postprocessing_cycle++;
351 ExportHandler::command_output(std::string output, size_t size)
353 std::cerr << "command: " << size << ", " << output << std::endl;
354 info << output << endmsg;
358 ExportHandler::finish_timespan ()
360 graph_builder->get_analysis_results (export_status->result_map);
362 while (config_map.begin() != timespan_bounds.second) {
364 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
365 std::string filename = config_map.begin()->second.filename->get_path(fmt);
366 if (fmt->with_cue()) {
367 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
370 if (fmt->with_toc()) {
371 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
374 if (fmt->with_mp4chaps()) {
375 export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
378 Session::Exported (current_timespan->name(), filename); /* EMIT SIGNAL */
380 /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
381 * The process cannot access the file because it is being used.
382 * ditto for post-export and upload.
384 graph_builder->reset ();
387 /* TODO: check Umlauts and encoding in filename.
388 * TagLib eventually calls CreateFileA(),
390 export_status->active_job = ExportStatus::Tagging;
391 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
394 if (!fmt->command().empty()) {
395 SessionMetadata const & metadata (*SessionMetadata::Metadata());
397 #if 0 // would be nicer with C++11 initialiser...
398 std::map<char, std::string> subs {
400 { 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR },
401 { 'b', PBD::basename_nosuffix(filename) },
405 export_status->active_job = ExportStatus::Command;
406 PBD::ScopedConnection command_connection;
407 std::map<char, std::string> subs;
409 std::stringstream track_number;
410 track_number << metadata.track_number ();
411 std::stringstream total_tracks;
412 total_tracks << metadata.total_tracks ();
413 std::stringstream year;
414 year << metadata.year ();
416 subs.insert (std::pair<char, std::string> ('a', metadata.artist ()));
417 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
418 subs.insert (std::pair<char, std::string> ('c', metadata.copyright ()));
419 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
420 subs.insert (std::pair<char, std::string> ('f', filename));
421 subs.insert (std::pair<char, std::string> ('l', metadata.lyricist ()));
422 subs.insert (std::pair<char, std::string> ('n', session.name ()));
423 subs.insert (std::pair<char, std::string> ('s', session.path ()));
424 subs.insert (std::pair<char, std::string> ('o', metadata.conductor ()));
425 subs.insert (std::pair<char, std::string> ('t', metadata.title ()));
426 subs.insert (std::pair<char, std::string> ('z', metadata.organization ()));
427 subs.insert (std::pair<char, std::string> ('A', metadata.album ()));
428 subs.insert (std::pair<char, std::string> ('C', metadata.comment ()));
429 subs.insert (std::pair<char, std::string> ('E', metadata.engineer ()));
430 subs.insert (std::pair<char, std::string> ('G', metadata.genre ()));
431 subs.insert (std::pair<char, std::string> ('L', total_tracks.str ()));
432 subs.insert (std::pair<char, std::string> ('M', metadata.mixer ()));
433 subs.insert (std::pair<char, std::string> ('N', current_timespan->name())); // =?= config_map.begin()->first->name ()
434 subs.insert (std::pair<char, std::string> ('O', metadata.composer ()));
435 subs.insert (std::pair<char, std::string> ('P', metadata.producer ()));
436 subs.insert (std::pair<char, std::string> ('S', metadata.disc_subtitle ()));
437 subs.insert (std::pair<char, std::string> ('T', track_number.str ()));
438 subs.insert (std::pair<char, std::string> ('Y', year.str ()));
439 subs.insert (std::pair<char, std::string> ('Z', metadata.country ()));
441 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
442 info << "Post-export command line : {" << se->to_s () << "}" << endmsg;
443 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
444 int ret = se->start (2);
446 // successfully started
447 while (se->is_running ()) {
448 // wait for system exec to terminate
452 error << "Post-export command FAILED with Error: " << ret << endmsg;
457 // XXX THIS IS IN REALTIME CONTEXT, CALLED FROM
458 // AudioEngine::process_callback()
459 // freewheeling, yes, but still uploading here is NOT
462 // even less so, since SoundcloudProgress is using
463 // connect_same_thread() - GUI updates from the RT thread
464 // will cause crashes. http://pastebin.com/UJKYNGHR
465 if (fmt->soundcloud_upload()) {
466 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
467 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
468 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
469 "uploading %1 - username=%2, password=%3, token=%4",
470 filename, soundcloud_username, soundcloud_password, token) );
471 std::string path = soundcloud_uploader->Upload (
473 PBD::basename_nosuffix(filename), // title
475 soundcloud_make_public,
476 soundcloud_downloadable,
479 if (path.length() != 0) {
480 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
481 if (soundcloud_open_page) {
482 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
483 open_uri(path.c_str()); // open the soundcloud website to the new file
486 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
488 delete soundcloud_uploader;
490 config_map.erase (config_map.begin());
496 /*** CD Marker stuff ***/
498 struct LocationSortByStart {
499 bool operator() (Location *a, Location *b) {
500 return a->start() < b->start();
505 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
506 std::string filename, CDMarkerFormat format)
508 string filepath = get_cd_marker_filename(filename, format);
511 void (ExportHandler::*header_func) (CDMarkerStatus &);
512 void (ExportHandler::*track_func) (CDMarkerStatus &);
513 void (ExportHandler::*index_func) (CDMarkerStatus &);
517 header_func = &ExportHandler::write_toc_header;
518 track_func = &ExportHandler::write_track_info_toc;
519 index_func = &ExportHandler::write_index_info_toc;
522 header_func = &ExportHandler::write_cue_header;
523 track_func = &ExportHandler::write_track_info_cue;
524 index_func = &ExportHandler::write_index_info_cue;
527 header_func = &ExportHandler::write_mp4ch_header;
528 track_func = &ExportHandler::write_track_info_mp4ch;
529 index_func = &ExportHandler::write_index_info_mp4ch;
535 CDMarkerStatus status (filepath, timespan, file_format, filename);
537 (this->*header_func) (status);
539 /* Get locations and sort */
541 Locations::LocationList const & locations (session.locations()->list());
542 Locations::LocationList::const_iterator i;
543 Locations::LocationList temp;
545 for (i = locations.begin(); i != locations.end(); ++i) {
546 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
552 // TODO One index marker for whole thing
556 LocationSortByStart cmp;
558 Locations::LocationList::const_iterator nexti;
560 /* Start actual marker stuff */
562 framepos_t last_end_time = timespan->get_start();
563 status.track_position = 0;
565 for (i = temp.begin(); i != temp.end(); ++i) {
569 if ((*i)->start() < last_end_time) {
570 if ((*i)->is_mark()) {
571 /* Index within track */
573 status.index_position = (*i)->start() - timespan->get_start();
574 (this->*index_func) (status);
580 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
582 status.track_position = last_end_time - timespan->get_start();
583 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
584 status.track_duration = 0;
586 if ((*i)->is_mark()) {
587 // a mark track location needs to look ahead to the next marker's start to determine length
591 if (nexti != temp.end()) {
592 status.track_duration = (*nexti)->start() - last_end_time;
594 last_end_time = (*nexti)->start();
596 // this was the last marker, use timespan end
597 status.track_duration = timespan->get_end() - last_end_time;
599 last_end_time = timespan->get_end();
603 status.track_duration = (*i)->end() - last_end_time;
605 last_end_time = (*i)->end();
608 (this->*track_func) (status);
611 } catch (std::exception& e) {
612 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
613 ::g_unlink (filepath.c_str());
614 } catch (Glib::Exception& e) {
615 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
616 ::g_unlink (filepath.c_str());
621 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
623 /* do not strip file suffix because there may be more than one format,
624 and we do not want the CD marker file from one format to overwrite
625 another (e.g. foo.wav.cue > foo.aiff.cue)
630 return filename + ".toc";
632 return filename + ".cue";
635 unsigned lastdot = filename.find_last_of('.');
636 return filename.substr(0,lastdot) + ".chapters.txt";
639 return filename + ".marker"; // Should not be reached when actually creating a file
644 ExportHandler::write_cue_header (CDMarkerStatus & status)
646 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
649 string barcode = SessionMetadata::Metadata()->barcode();
650 string album_artist = SessionMetadata::Metadata()->album_artist();
651 string album_title = SessionMetadata::Metadata()->album();
653 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
656 status.out << "CATALOG " << barcode << endl;
658 if (album_artist != "")
659 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
661 if (album_title != "")
664 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
666 /* The original cue sheet spec mentions five file types
668 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
669 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
672 We try to use these file types whenever appropriate and
673 default to our own names otherwise.
675 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
676 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
677 status.out << "WAVE";
678 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
679 status.format->sample_format() == ExportFormatBase::SF_16 &&
680 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
681 // Format is RAW 16bit 44.1kHz
682 if (status.format->endianness() == ExportFormatBase::E_Little) {
683 status.out << "BINARY";
685 status.out << "MOTOROLA";
688 // no special case for AIFF format it's name is already "AIFF"
689 status.out << status.format->format_name();
695 ExportHandler::write_toc_header (CDMarkerStatus & status)
697 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
700 string barcode = SessionMetadata::Metadata()->barcode();
701 string album_artist = SessionMetadata::Metadata()->album_artist();
702 string album_title = SessionMetadata::Metadata()->album();
705 status.out << "CATALOG \"" << barcode << "\"" << endl;
707 if (album_title != "")
710 status.out << "CD_DA" << endl;
711 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
712 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
713 status.out << " PERFORMER " << toc_escape_cdtext (album_artist) << endl;
714 status.out << " }" << endl << "}" << endl;
718 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
720 status.out << "00:00:00.000 Intro" << endl;
724 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
728 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
729 status.out << buf << endl;
731 status.out << " FLAGS" ;
732 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
733 status.out << " SCMS ";
735 status.out << " DCP ";
738 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
739 status.out << " PRE";
743 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
744 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
747 if (status.marker->name() != "") {
748 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
751 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
752 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
755 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
756 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
759 if (status.track_position != status.track_start_frame) {
760 frames_to_cd_frames_string (buf, status.track_position);
761 status.out << " INDEX 00" << buf << endl;
764 frames_to_cd_frames_string (buf, status.track_start_frame);
765 status.out << " INDEX 01" << buf << endl;
767 status.index_number = 2;
768 status.track_number++;
772 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
776 status.out << endl << "TRACK AUDIO" << endl;
778 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
781 status.out << "COPY" << endl;
783 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
784 status.out << "PRE_EMPHASIS" << endl;
786 status.out << "NO PRE_EMPHASIS" << endl;
789 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
790 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
793 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
794 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
796 status.out << " PERFORMER ";
797 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
798 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
800 status.out << "\"\"" << endl;
803 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
804 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
807 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
808 status.out << " ISRC \"";
809 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
810 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
811 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
812 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
815 status.out << " }" << endl << "}" << endl;
817 frames_to_cd_frames_string (buf, status.track_position);
818 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
820 frames_to_cd_frames_string (buf, status.track_duration);
821 status.out << buf << endl;
823 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
824 status.out << "START" << buf << endl;
827 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
831 frames_to_chapter_marks_string(buf, status.track_start_frame);
832 status.out << buf << " " << status.marker->name() << endl;
836 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
840 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
842 frames_to_cd_frames_string (buf, status.index_position);
843 status.out << buf << endl;
849 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
853 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
854 status.out << "INDEX" << buf << endl;
858 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
863 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
865 framecnt_t remainder;
866 framecnt_t fr = session.nominal_frame_rate();
867 int mins, secs, frames;
869 mins = when / (60 * fr);
870 remainder = when - (mins * 60 * fr);
871 secs = remainder / fr;
872 remainder -= secs * fr;
873 frames = remainder / (fr / 75);
874 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
878 ExportHandler::frames_to_chapter_marks_string (char* buf, framepos_t when)
880 framecnt_t remainder;
881 framecnt_t fr = session.nominal_frame_rate();
882 int hours, mins, secs, msecs;
884 hours = when / (3600 * fr);
885 remainder = when - (hours * 3600 * fr);
886 mins = remainder / (60 * fr);
887 remainder -= mins * 60 * fr;
888 secs = remainder / fr;
889 remainder -= secs * fr;
890 msecs = (remainder * 1000) / fr;
891 sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
895 ExportHandler::toc_escape_cdtext (const std::string& txt)
897 Glib::ustring check (txt);
899 std::string latin1_txt;
903 latin1_txt = Glib::convert_with_fallback (txt, "ISO-8859-1", "UTF-8", "_");
904 } catch (Glib::ConvertError& err) {
905 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
910 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
914 } else if ((*c) == '\\') {
916 } else if (isprint (*c)) {
919 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
930 ExportHandler::toc_escape_filename (const std::string& txt)
936 // We iterate byte-wise not character-wise over a UTF-8 string here,
937 // because we only want to translate backslashes and double quotes
938 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
942 } else if (*c == '\\') {
955 ExportHandler::cue_escape_cdtext (const std::string& txt)
957 std::string latin1_txt;
961 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
962 } catch (Glib::ConvertError& err) {
963 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
966 // does not do much mor than UTF-8 to Latin1 translation yet, but
967 // that may have to change if cue parsers in burning programs change
968 out = '"' + latin1_txt + '"';
973 } // namespace ARDOUR