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.sample_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_samples += 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_samples_current_timespan = current_timespan->get_length();
184 export_status->timespan_name = current_timespan->name();
185 export_status->processed_samples_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 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
197 // Filenames can be shared across timespans
198 FileSpec & spec = it->second;
199 spec.filename->set_timespan (it->first);
200 switch (spec.channel_config->region_processing_type ()) {
201 case RegionExportChannelFactory::None:
202 case RegionExportChannelFactory::Processed:
203 region_export = false;
208 graph_builder->add_config (spec, realtime);
211 // ExportDialog::update_realtime_selection does not allow this
212 assert (!region_export || !realtime);
216 post_processing = false;
217 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
218 process_position = current_timespan->get_start();
219 // TODO check if it's a RegionExport.. set flag to skip process_without_events()
220 session.start_audio_export (process_position, realtime, region_export);
224 ExportHandler::handle_duplicate_format_extensions()
226 typedef std::map<std::string, int> ExtCountMap;
229 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
230 counts[it->second.format->extension()]++;
233 bool duplicates_found = false;
234 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
235 if (it->second > 1) { duplicates_found = true; }
238 // Set this always, as the filenames are shared...
239 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
240 it->second.filename->include_format_name = duplicates_found;
245 ExportHandler::process (samplecnt_t samples)
247 if (!export_status->running ()) {
249 } else if (post_processing) {
250 Glib::Threads::Mutex::Lock l (export_status->lock());
251 if (AudioEngine::instance()->freewheeling ()) {
252 return post_process ();
254 // wait until we're freewheeling
258 Glib::Threads::Mutex::Lock l (export_status->lock());
259 return process_timespan (samples);
264 ExportHandler::process_timespan (samplecnt_t samples)
266 export_status->active_job = ExportStatus::Exporting;
267 /* update position */
269 samplecnt_t samples_to_read = 0;
270 samplepos_t const end = current_timespan->get_end();
272 bool const last_cycle = (process_position + samples >= end);
275 samples_to_read = end - process_position;
276 export_status->stop = true;
278 samples_to_read = samples;
281 process_position += samples_to_read;
282 export_status->processed_samples += samples_to_read;
283 export_status->processed_samples_current_timespan += samples_to_read;
285 /* Do actual processing */
286 int ret = graph_builder->process (samples_to_read, last_cycle);
288 /* Start post-processing/normalizing if necessary */
290 post_processing = graph_builder->need_postprocessing ();
291 if (post_processing) {
292 export_status->total_postprocessing_cycles = graph_builder->get_postprocessing_cycle_count();
293 export_status->current_postprocessing_cycle = 0;
304 ExportHandler::post_process ()
306 if (graph_builder->post_process ()) {
308 export_status->active_job = ExportStatus::Exporting;
310 if (graph_builder->realtime ()) {
311 export_status->active_job = ExportStatus::Encoding;
313 export_status->active_job = ExportStatus::Normalizing;
317 export_status->current_postprocessing_cycle++;
323 ExportHandler::command_output(std::string output, size_t size)
325 std::cerr << "command: " << size << ", " << output << std::endl;
326 info << output << endmsg;
330 ExportHandler::finish_timespan ()
332 graph_builder->get_analysis_results (export_status->result_map);
334 while (config_map.begin() != timespan_bounds.second) {
336 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
337 std::string filename = config_map.begin()->second.filename->get_path(fmt);
338 if (fmt->with_cue()) {
339 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
342 if (fmt->with_toc()) {
343 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
346 if (fmt->with_mp4chaps()) {
347 export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
350 Session::Exported (current_timespan->name(), filename); /* EMIT SIGNAL */
352 /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
353 * The process cannot access the file because it is being used.
354 * ditto for post-export and upload.
356 graph_builder->reset ();
359 /* TODO: check Umlauts and encoding in filename.
360 * TagLib eventually calls CreateFileA(),
362 export_status->active_job = ExportStatus::Tagging;
363 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
366 if (!fmt->command().empty()) {
367 SessionMetadata const & metadata (*SessionMetadata::Metadata());
369 #if 0 // would be nicer with C++11 initialiser...
370 std::map<char, std::string> subs {
372 { 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR },
373 { 'b', PBD::basename_nosuffix(filename) },
377 export_status->active_job = ExportStatus::Command;
378 PBD::ScopedConnection command_connection;
379 std::map<char, std::string> subs;
381 std::stringstream track_number;
382 track_number << metadata.track_number ();
383 std::stringstream total_tracks;
384 total_tracks << metadata.total_tracks ();
385 std::stringstream year;
386 year << metadata.year ();
388 subs.insert (std::pair<char, std::string> ('a', metadata.artist ()));
389 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
390 subs.insert (std::pair<char, std::string> ('c', metadata.copyright ()));
391 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
392 subs.insert (std::pair<char, std::string> ('f', filename));
393 subs.insert (std::pair<char, std::string> ('l', metadata.lyricist ()));
394 subs.insert (std::pair<char, std::string> ('n', session.name ()));
395 subs.insert (std::pair<char, std::string> ('s', session.path ()));
396 subs.insert (std::pair<char, std::string> ('o', metadata.conductor ()));
397 subs.insert (std::pair<char, std::string> ('t', metadata.title ()));
398 subs.insert (std::pair<char, std::string> ('z', metadata.organization ()));
399 subs.insert (std::pair<char, std::string> ('A', metadata.album ()));
400 subs.insert (std::pair<char, std::string> ('C', metadata.comment ()));
401 subs.insert (std::pair<char, std::string> ('E', metadata.engineer ()));
402 subs.insert (std::pair<char, std::string> ('G', metadata.genre ()));
403 subs.insert (std::pair<char, std::string> ('L', total_tracks.str ()));
404 subs.insert (std::pair<char, std::string> ('M', metadata.mixer ()));
405 subs.insert (std::pair<char, std::string> ('N', current_timespan->name())); // =?= config_map.begin()->first->name ()
406 subs.insert (std::pair<char, std::string> ('O', metadata.composer ()));
407 subs.insert (std::pair<char, std::string> ('P', metadata.producer ()));
408 subs.insert (std::pair<char, std::string> ('S', metadata.disc_subtitle ()));
409 subs.insert (std::pair<char, std::string> ('T', track_number.str ()));
410 subs.insert (std::pair<char, std::string> ('Y', year.str ()));
411 subs.insert (std::pair<char, std::string> ('Z', metadata.country ()));
413 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
414 info << "Post-export command line : {" << se->to_s () << "}" << endmsg;
415 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
416 int ret = se->start (2);
418 // successfully started
419 while (se->is_running ()) {
420 // wait for system exec to terminate
424 error << "Post-export command FAILED with Error: " << ret << endmsg;
429 // XXX THIS IS IN REALTIME CONTEXT, CALLED FROM
430 // AudioEngine::process_callback()
431 // freewheeling, yes, but still uploading here is NOT
434 // even less so, since SoundcloudProgress is using
435 // connect_same_thread() - GUI updates from the RT thread
436 // will cause crashes. http://pastebin.com/UJKYNGHR
437 if (fmt->soundcloud_upload()) {
438 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
439 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
440 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
441 "uploading %1 - username=%2, password=%3, token=%4",
442 filename, soundcloud_username, soundcloud_password, token) );
443 std::string path = soundcloud_uploader->Upload (
445 PBD::basename_nosuffix(filename), // title
447 soundcloud_make_public,
448 soundcloud_downloadable,
451 if (path.length() != 0) {
452 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
453 if (soundcloud_open_page) {
454 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
455 open_uri(path.c_str()); // open the soundcloud website to the new file
458 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
460 delete soundcloud_uploader;
462 config_map.erase (config_map.begin());
468 /*** CD Marker stuff ***/
470 struct LocationSortByStart {
471 bool operator() (Location *a, Location *b) {
472 return a->start() < b->start();
477 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
478 std::string filename, CDMarkerFormat format)
480 string filepath = get_cd_marker_filename(filename, format);
483 void (ExportHandler::*header_func) (CDMarkerStatus &);
484 void (ExportHandler::*track_func) (CDMarkerStatus &);
485 void (ExportHandler::*index_func) (CDMarkerStatus &);
489 header_func = &ExportHandler::write_toc_header;
490 track_func = &ExportHandler::write_track_info_toc;
491 index_func = &ExportHandler::write_index_info_toc;
494 header_func = &ExportHandler::write_cue_header;
495 track_func = &ExportHandler::write_track_info_cue;
496 index_func = &ExportHandler::write_index_info_cue;
499 header_func = &ExportHandler::write_mp4ch_header;
500 track_func = &ExportHandler::write_track_info_mp4ch;
501 index_func = &ExportHandler::write_index_info_mp4ch;
507 CDMarkerStatus status (filepath, timespan, file_format, filename);
509 (this->*header_func) (status);
511 /* Get locations and sort */
513 Locations::LocationList const & locations (session.locations()->list());
514 Locations::LocationList::const_iterator i;
515 Locations::LocationList temp;
517 for (i = locations.begin(); i != locations.end(); ++i) {
518 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
524 // TODO One index marker for whole thing
528 LocationSortByStart cmp;
530 Locations::LocationList::const_iterator nexti;
532 /* Start actual marker stuff */
534 samplepos_t last_end_time = timespan->get_start();
535 status.track_position = 0;
537 for (i = temp.begin(); i != temp.end(); ++i) {
541 if ((*i)->start() < last_end_time) {
542 if ((*i)->is_mark()) {
543 /* Index within track */
545 status.index_position = (*i)->start() - timespan->get_start();
546 (this->*index_func) (status);
552 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
554 status.track_position = last_end_time - timespan->get_start();
555 status.track_start_sample = (*i)->start() - timespan->get_start(); // everything before this is the pregap
556 status.track_duration = 0;
558 if ((*i)->is_mark()) {
559 // a mark track location needs to look ahead to the next marker's start to determine length
563 if (nexti != temp.end()) {
564 status.track_duration = (*nexti)->start() - last_end_time;
566 last_end_time = (*nexti)->start();
568 // this was the last marker, use timespan end
569 status.track_duration = timespan->get_end() - last_end_time;
571 last_end_time = timespan->get_end();
575 status.track_duration = (*i)->end() - last_end_time;
577 last_end_time = (*i)->end();
580 (this->*track_func) (status);
583 } catch (std::exception& e) {
584 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
585 ::g_unlink (filepath.c_str());
586 } catch (Glib::Exception& e) {
587 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
588 ::g_unlink (filepath.c_str());
593 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
595 /* do not strip file suffix because there may be more than one format,
596 and we do not want the CD marker file from one format to overwrite
597 another (e.g. foo.wav.cue > foo.aiff.cue)
602 return filename + ".toc";
604 return filename + ".cue";
607 unsigned lastdot = filename.find_last_of('.');
608 return filename.substr(0,lastdot) + ".chapters.txt";
611 return filename + ".marker"; // Should not be reached when actually creating a file
616 ExportHandler::write_cue_header (CDMarkerStatus & status)
618 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
621 string barcode = SessionMetadata::Metadata()->barcode();
622 string album_artist = SessionMetadata::Metadata()->album_artist();
623 string album_title = SessionMetadata::Metadata()->album();
625 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
628 status.out << "CATALOG " << barcode << endl;
630 if (album_artist != "")
631 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
633 if (album_title != "")
636 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
638 /* The original cue sheet spec mentions five file types
640 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
641 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
644 We try to use these file types whenever appropriate and
645 default to our own names otherwise.
647 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
648 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
649 status.out << "WAVE";
650 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
651 status.format->sample_format() == ExportFormatBase::SF_16 &&
652 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
653 // Format is RAW 16bit 44.1kHz
654 if (status.format->endianness() == ExportFormatBase::E_Little) {
655 status.out << "BINARY";
657 status.out << "MOTOROLA";
660 // no special case for AIFF format it's name is already "AIFF"
661 status.out << status.format->format_name();
667 ExportHandler::write_toc_header (CDMarkerStatus & status)
669 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
672 string barcode = SessionMetadata::Metadata()->barcode();
673 string album_artist = SessionMetadata::Metadata()->album_artist();
674 string album_title = SessionMetadata::Metadata()->album();
677 status.out << "CATALOG \"" << barcode << "\"" << endl;
679 if (album_title != "")
682 status.out << "CD_DA" << endl;
683 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
684 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
685 status.out << " PERFORMER " << toc_escape_cdtext (album_artist) << endl;
686 status.out << " }" << endl << "}" << endl;
690 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
692 status.out << "00:00:00.000 Intro" << endl;
696 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
700 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
701 status.out << buf << endl;
703 status.out << " FLAGS" ;
704 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
705 status.out << " SCMS ";
707 status.out << " DCP ";
710 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
711 status.out << " PRE";
715 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
716 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
719 if (status.marker->name() != "") {
720 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
723 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
724 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
727 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
728 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
731 if (status.track_position != status.track_start_sample) {
732 samples_to_cd_samples_string (buf, status.track_position);
733 status.out << " INDEX 00" << buf << endl;
736 samples_to_cd_samples_string (buf, status.track_start_sample);
737 status.out << " INDEX 01" << buf << endl;
739 status.index_number = 2;
740 status.track_number++;
744 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
748 status.out << endl << "TRACK AUDIO" << endl;
750 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
753 status.out << "COPY" << endl;
755 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
756 status.out << "PRE_EMPHASIS" << endl;
758 status.out << "NO PRE_EMPHASIS" << endl;
761 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
762 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
765 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
766 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
768 status.out << " PERFORMER ";
769 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
770 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
772 status.out << "\"\"" << endl;
775 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
776 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
779 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
780 status.out << " ISRC \"";
781 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
782 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
783 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
784 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
787 status.out << " }" << endl << "}" << endl;
789 samples_to_cd_samples_string (buf, status.track_position);
790 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
792 samples_to_cd_samples_string (buf, status.track_duration);
793 status.out << buf << endl;
795 samples_to_cd_samples_string (buf, status.track_start_sample - status.track_position);
796 status.out << "START" << buf << endl;
799 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
803 samples_to_chapter_marks_string(buf, status.track_start_sample);
804 status.out << buf << " " << status.marker->name() << endl;
808 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
812 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
814 samples_to_cd_samples_string (buf, status.index_position);
815 status.out << buf << endl;
821 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
825 samples_to_cd_samples_string (buf, status.index_position - status.track_position);
826 status.out << "INDEX" << buf << endl;
830 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
835 ExportHandler::samples_to_cd_samples_string (char* buf, samplepos_t when)
837 samplecnt_t remainder;
838 samplecnt_t fr = session.nominal_sample_rate();
839 int mins, secs, samples;
841 mins = when / (60 * fr);
842 remainder = when - (mins * 60 * fr);
843 secs = remainder / fr;
844 remainder -= secs * fr;
845 samples = remainder / (fr / 75);
846 sprintf (buf, " %02d:%02d:%02d", mins, secs, samples);
850 ExportHandler::samples_to_chapter_marks_string (char* buf, samplepos_t when)
852 samplecnt_t remainder;
853 samplecnt_t fr = session.nominal_sample_rate();
854 int hours, mins, secs, msecs;
856 hours = when / (3600 * fr);
857 remainder = when - (hours * 3600 * fr);
858 mins = remainder / (60 * fr);
859 remainder -= mins * 60 * fr;
860 secs = remainder / fr;
861 remainder -= secs * fr;
862 msecs = (remainder * 1000) / fr;
863 sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
867 ExportHandler::toc_escape_cdtext (const std::string& txt)
869 Glib::ustring check (txt);
871 std::string latin1_txt;
875 latin1_txt = Glib::convert_with_fallback (txt, "ISO-8859-1", "UTF-8", "_");
876 } catch (Glib::ConvertError& err) {
877 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
882 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
886 } else if ((*c) == '\\') {
888 } else if (isprint (*c)) {
891 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
902 ExportHandler::toc_escape_filename (const std::string& txt)
908 // We iterate byte-wise not character-wise over a UTF-8 string here,
909 // because we only want to translate backslashes and double quotes
910 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
914 } else if (*c == '\\') {
927 ExportHandler::cue_escape_cdtext (const std::string& txt)
929 std::string latin1_txt;
933 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
934 } catch (Glib::ConvertError& err) {
935 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
938 // does not do much mor than UTF-8 to Latin1 translation yet, but
939 // that may have to change if cue parsers in burning programs change
940 out = '"' + latin1_txt + '"';
945 } // namespace ARDOUR