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/audiofile_tagger.h"
30 #include "ardour/debug.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/soundcloud_upload.h"
38 #include "ardour/system_exec.h"
39 #include "pbd/openuri.h"
40 #include "pbd/basename.h"
41 #include "ardour/session_metadata.h"
51 /*** ExportElementFactory ***/
53 ExportElementFactory::ExportElementFactory (Session & session) :
59 ExportElementFactory::~ExportElementFactory ()
65 ExportElementFactory::add_timespan ()
67 return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
70 ExportChannelConfigPtr
71 ExportElementFactory::add_channel_config ()
73 return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
77 ExportElementFactory::add_format ()
79 return ExportFormatSpecPtr (new ExportFormatSpecification (session));
83 ExportElementFactory::add_format (XMLNode const & state)
85 return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
89 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
91 return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
95 ExportElementFactory::add_filename ()
97 return ExportFilenamePtr (new ExportFilename (session));
101 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
103 return ExportFilenamePtr (new ExportFilename (*other));
106 /*** ExportHandler ***/
108 ExportHandler::ExportHandler (Session & session)
109 : ExportElementFactory (session)
111 , graph_builder (new ExportGraphBuilder (session))
112 , export_status (session.get_export_status ())
113 , normalizing (false)
119 ExportHandler::~ExportHandler ()
121 graph_builder->cleanup (export_status->aborted () );
124 /** Add an export to the `to-do' list */
126 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
127 ExportFormatSpecPtr format, ExportFilenamePtr filename,
128 BroadcastInfoPtr broadcast_info)
130 FileSpec spec (channel_config, format, filename, broadcast_info);
131 config_map.insert (make_pair (timespan, spec));
137 ExportHandler::do_export ()
139 /* Count timespans */
141 export_status->init();
142 std::set<ExportTimespanPtr> timespan_set;
143 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
144 bool new_timespan = timespan_set.insert (it->first).second;
146 export_status->total_frames += it->first->get_length();
149 export_status->total_timespans = timespan_set.size();
151 if (export_status->total_timespans > 1) {
152 // always include timespan if there's more than one.
153 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
154 FileSpec & spec = it->second;
155 spec.filename->include_timespan = true;
161 Glib::Threads::Mutex::Lock l (export_status->lock());
166 ExportHandler::start_timespan ()
168 export_status->timespan++;
170 if (config_map.empty()) {
171 // freewheeling has to be stopped from outside the process cycle
172 export_status->set_running (false);
176 /* finish_timespan pops the config_map entry that has been done, so
177 this is the timespan to do this time
179 current_timespan = config_map.begin()->first;
181 export_status->total_frames_current_timespan = current_timespan->get_length();
182 export_status->timespan_name = current_timespan->name();
183 export_status->processed_frames_current_timespan = 0;
185 /* Register file configurations to graph builder */
187 /* Here's the config_map entries that use this timespan */
188 timespan_bounds = config_map.equal_range (current_timespan);
189 graph_builder->reset ();
190 graph_builder->set_current_timespan (current_timespan);
191 handle_duplicate_format_extensions();
192 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
193 // Filenames can be shared across timespans
194 FileSpec & spec = it->second;
195 spec.filename->set_timespan (it->first);
196 graph_builder->add_config (spec);
202 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
203 process_position = current_timespan->get_start();
204 session.start_audio_export (process_position);
208 ExportHandler::handle_duplicate_format_extensions()
210 typedef std::map<std::string, int> ExtCountMap;
213 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
214 counts[it->second.format->extension()]++;
217 bool duplicates_found = false;
218 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
219 if (it->second > 1) { duplicates_found = true; }
222 // Set this always, as the filenames are shared...
223 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
224 it->second.filename->include_format_name = duplicates_found;
229 ExportHandler::process (framecnt_t frames)
231 if (!export_status->running ()) {
233 } else if (normalizing) {
234 Glib::Threads::Mutex::Lock l (export_status->lock());
235 return process_normalize ();
237 Glib::Threads::Mutex::Lock l (export_status->lock());
238 return process_timespan (frames);
243 ExportHandler::process_timespan (framecnt_t frames)
245 export_status->active_job = ExportStatus::Exporting;
246 /* update position */
248 framecnt_t frames_to_read = 0;
249 framepos_t const end = current_timespan->get_end();
251 bool const last_cycle = (process_position + frames >= end);
254 frames_to_read = end - process_position;
255 export_status->stop = true;
257 frames_to_read = frames;
260 process_position += frames_to_read;
261 export_status->processed_frames += frames_to_read;
262 export_status->processed_frames_current_timespan += frames_to_read;
264 /* Do actual processing */
265 int ret = graph_builder->process (frames_to_read, last_cycle);
267 /* Start normalizing if necessary */
269 normalizing = graph_builder->will_normalize();
271 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
272 export_status->current_normalize_cycle = 0;
283 ExportHandler::process_normalize ()
285 if (graph_builder->process_normalize ()) {
287 export_status->active_job = ExportStatus::Exporting;
289 export_status->active_job = ExportStatus::Normalizing;
292 export_status->current_normalize_cycle++;
298 ExportHandler::command_output(std::string output, size_t size)
300 std::cerr << "command: " << size << ", " << output << std::endl;
301 info << output << endmsg;
305 ExportHandler::finish_timespan ()
307 graph_builder->get_analysis_results (export_status->result_map);
309 while (config_map.begin() != timespan_bounds.second) {
311 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
312 std::string filename = config_map.begin()->second.filename->get_path(fmt);
313 if (fmt->with_cue()) {
314 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
317 if (fmt->with_toc()) {
318 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
321 if (fmt->with_mp4chaps()) {
322 export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
325 Session::Exported (current_timespan->name(), filename); /* EMIT SIGNAL */
327 /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
328 * The process cannot access the file because it is being used.
329 * ditto for post-export and upload.
331 graph_builder->reset ();
334 /* TODO: check Umlauts and encoding in filename.
335 * TagLib eventually calls CreateFileA(),
337 export_status->active_job = ExportStatus::Tagging;
338 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
341 if (!fmt->command().empty()) {
342 SessionMetadata const & metadata (*SessionMetadata::Metadata());
344 #if 0 // would be nicer with C++11 initialiser...
345 std::map<char, std::string> subs {
347 { 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR },
348 { 'b', PBD::basename_nosuffix(filename) },
352 export_status->active_job = ExportStatus::Command;
353 PBD::ScopedConnection command_connection;
354 std::map<char, std::string> subs;
356 std::stringstream track_number;
357 track_number << metadata.track_number ();
358 std::stringstream total_tracks;
359 total_tracks << metadata.total_tracks ();
360 std::stringstream year;
361 year << metadata.year ();
363 subs.insert (std::pair<char, std::string> ('a', metadata.artist ()));
364 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
365 subs.insert (std::pair<char, std::string> ('c', metadata.copyright ()));
366 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
367 subs.insert (std::pair<char, std::string> ('f', filename));
368 subs.insert (std::pair<char, std::string> ('l', metadata.lyricist ()));
369 subs.insert (std::pair<char, std::string> ('n', session.name ()));
370 subs.insert (std::pair<char, std::string> ('s', session.path ()));
371 subs.insert (std::pair<char, std::string> ('o', metadata.conductor ()));
372 subs.insert (std::pair<char, std::string> ('t', metadata.title ()));
373 subs.insert (std::pair<char, std::string> ('z', metadata.organization ()));
374 subs.insert (std::pair<char, std::string> ('A', metadata.album ()));
375 subs.insert (std::pair<char, std::string> ('C', metadata.comment ()));
376 subs.insert (std::pair<char, std::string> ('E', metadata.engineer ()));
377 subs.insert (std::pair<char, std::string> ('G', metadata.genre ()));
378 subs.insert (std::pair<char, std::string> ('L', total_tracks.str ()));
379 subs.insert (std::pair<char, std::string> ('M', metadata.mixer ()));
380 subs.insert (std::pair<char, std::string> ('N', current_timespan->name())); // =?= config_map.begin()->first->name ()
381 subs.insert (std::pair<char, std::string> ('O', metadata.composer ()));
382 subs.insert (std::pair<char, std::string> ('P', metadata.producer ()));
383 subs.insert (std::pair<char, std::string> ('S', metadata.disc_subtitle ()));
384 subs.insert (std::pair<char, std::string> ('T', track_number.str ()));
385 subs.insert (std::pair<char, std::string> ('Y', year.str ()));
386 subs.insert (std::pair<char, std::string> ('Z', metadata.country ()));
388 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
389 info << "Post-export command line : {" << se->to_s () << "}" << endmsg;
390 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
391 int ret = se->start (2);
393 // successfully started
394 while (se->is_running ()) {
395 // wait for system exec to terminate
399 error << "Post-export command FAILED with Error: " << ret << endmsg;
404 if (fmt->soundcloud_upload()) {
405 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
406 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
407 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
408 "uploading %1 - username=%2, password=%3, token=%4",
409 filename, soundcloud_username, soundcloud_password, token) );
410 std::string path = soundcloud_uploader->Upload (
412 PBD::basename_nosuffix(filename), // title
414 soundcloud_make_public,
415 soundcloud_downloadable,
418 if (path.length() != 0) {
419 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
420 if (soundcloud_open_page) {
421 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
422 open_uri(path.c_str()); // open the soundcloud website to the new file
425 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
427 delete soundcloud_uploader;
429 config_map.erase (config_map.begin());
435 /*** CD Marker stuff ***/
437 struct LocationSortByStart {
438 bool operator() (Location *a, Location *b) {
439 return a->start() < b->start();
444 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
445 std::string filename, CDMarkerFormat format)
447 string filepath = get_cd_marker_filename(filename, format);
450 void (ExportHandler::*header_func) (CDMarkerStatus &);
451 void (ExportHandler::*track_func) (CDMarkerStatus &);
452 void (ExportHandler::*index_func) (CDMarkerStatus &);
456 header_func = &ExportHandler::write_toc_header;
457 track_func = &ExportHandler::write_track_info_toc;
458 index_func = &ExportHandler::write_index_info_toc;
461 header_func = &ExportHandler::write_cue_header;
462 track_func = &ExportHandler::write_track_info_cue;
463 index_func = &ExportHandler::write_index_info_cue;
466 header_func = &ExportHandler::write_mp4ch_header;
467 track_func = &ExportHandler::write_track_info_mp4ch;
468 index_func = &ExportHandler::write_index_info_mp4ch;
474 CDMarkerStatus status (filepath, timespan, file_format, filename);
476 (this->*header_func) (status);
478 /* Get locations and sort */
480 Locations::LocationList const & locations (session.locations()->list());
481 Locations::LocationList::const_iterator i;
482 Locations::LocationList temp;
484 for (i = locations.begin(); i != locations.end(); ++i) {
485 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
491 // TODO One index marker for whole thing
495 LocationSortByStart cmp;
497 Locations::LocationList::const_iterator nexti;
499 /* Start actual marker stuff */
501 framepos_t last_end_time = timespan->get_start();
502 status.track_position = 0;
504 for (i = temp.begin(); i != temp.end(); ++i) {
508 if ((*i)->start() < last_end_time) {
509 if ((*i)->is_mark()) {
510 /* Index within track */
512 status.index_position = (*i)->start() - timespan->get_start();
513 (this->*index_func) (status);
519 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
521 status.track_position = last_end_time - timespan->get_start();
522 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
523 status.track_duration = 0;
525 if ((*i)->is_mark()) {
526 // a mark track location needs to look ahead to the next marker's start to determine length
530 if (nexti != temp.end()) {
531 status.track_duration = (*nexti)->start() - last_end_time;
533 last_end_time = (*nexti)->start();
535 // this was the last marker, use timespan end
536 status.track_duration = timespan->get_end() - last_end_time;
538 last_end_time = timespan->get_end();
542 status.track_duration = (*i)->end() - last_end_time;
544 last_end_time = (*i)->end();
547 (this->*track_func) (status);
550 } catch (std::exception& e) {
551 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
552 ::g_unlink (filepath.c_str());
553 } catch (Glib::Exception& e) {
554 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
555 ::g_unlink (filepath.c_str());
560 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
562 /* do not strip file suffix because there may be more than one format,
563 and we do not want the CD marker file from one format to overwrite
564 another (e.g. foo.wav.cue > foo.aiff.cue)
569 return filename + ".toc";
571 return filename + ".cue";
574 unsigned lastdot = filename.find_last_of('.');
575 return filename.substr(0,lastdot) + ".chapters.txt";
578 return filename + ".marker"; // Should not be reached when actually creating a file
583 ExportHandler::write_cue_header (CDMarkerStatus & status)
585 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
588 string barcode = SessionMetadata::Metadata()->barcode();
589 string album_artist = SessionMetadata::Metadata()->album_artist();
590 string album_title = SessionMetadata::Metadata()->album();
592 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
595 status.out << "CATALOG " << barcode << endl;
597 if (album_artist != "")
598 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
600 if (album_title != "")
603 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
605 /* The original cue sheet spec mentions five file types
607 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
608 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
611 We try to use these file types whenever appropriate and
612 default to our own names otherwise.
614 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
615 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
616 status.out << "WAVE";
617 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
618 status.format->sample_format() == ExportFormatBase::SF_16 &&
619 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
620 // Format is RAW 16bit 44.1kHz
621 if (status.format->endianness() == ExportFormatBase::E_Little) {
622 status.out << "BINARY";
624 status.out << "MOTOROLA";
627 // no special case for AIFF format it's name is already "AIFF"
628 status.out << status.format->format_name();
634 ExportHandler::write_toc_header (CDMarkerStatus & status)
636 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
639 string barcode = SessionMetadata::Metadata()->barcode();
640 string album_artist = SessionMetadata::Metadata()->album_artist();
641 string album_title = SessionMetadata::Metadata()->album();
644 status.out << "CATALOG \"" << barcode << "\"" << endl;
646 if (album_title != "")
649 status.out << "CD_DA" << endl;
650 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
651 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
652 status.out << " PERFORMER " << toc_escape_cdtext (album_artist) << endl;
653 status.out << " }" << endl << "}" << endl;
657 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
659 status.out << "00:00:00.000 Intro" << endl;
663 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
667 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
668 status.out << buf << endl;
670 status.out << " FLAGS" ;
671 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
672 status.out << " SCMS ";
674 status.out << " DCP ";
677 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
678 status.out << " PRE";
682 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
683 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
686 if (status.marker->name() != "") {
687 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
690 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
691 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
694 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
695 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
698 if (status.track_position != status.track_start_frame) {
699 frames_to_cd_frames_string (buf, status.track_position);
700 status.out << " INDEX 00" << buf << endl;
703 frames_to_cd_frames_string (buf, status.track_start_frame);
704 status.out << " INDEX 01" << buf << endl;
706 status.index_number = 2;
707 status.track_number++;
711 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
715 status.out << endl << "TRACK AUDIO" << endl;
717 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
720 status.out << "COPY" << endl;
722 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
723 status.out << "PRE_EMPHASIS" << endl;
725 status.out << "NO PRE_EMPHASIS" << endl;
728 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
729 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
732 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
733 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
735 status.out << " PERFORMER ";
736 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
737 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
739 status.out << "\"\"" << endl;
742 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
743 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
746 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
747 status.out << " ISRC \"";
748 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
749 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
750 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
751 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
754 status.out << " }" << endl << "}" << endl;
756 frames_to_cd_frames_string (buf, status.track_position);
757 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
759 frames_to_cd_frames_string (buf, status.track_duration);
760 status.out << buf << endl;
762 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
763 status.out << "START" << buf << endl;
766 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
770 frames_to_chapter_marks_string(buf, status.track_start_frame);
771 status.out << buf << " " << status.marker->name() << endl;
775 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
779 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
781 frames_to_cd_frames_string (buf, status.index_position);
782 status.out << buf << endl;
788 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
792 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
793 status.out << "INDEX" << buf << endl;
797 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
802 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
804 framecnt_t remainder;
805 framecnt_t fr = session.nominal_frame_rate();
806 int mins, secs, frames;
808 mins = when / (60 * fr);
809 remainder = when - (mins * 60 * fr);
810 secs = remainder / fr;
811 remainder -= secs * fr;
812 frames = remainder / (fr / 75);
813 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
817 ExportHandler::frames_to_chapter_marks_string (char* buf, framepos_t when)
819 framecnt_t remainder;
820 framecnt_t fr = session.nominal_frame_rate();
821 int hours, mins, secs, msecs;
823 hours = when / (3600 * fr);
824 remainder = when - (hours * 3600 * fr);
825 mins = remainder / (60 * fr);
826 remainder -= mins * 60 * fr;
827 secs = remainder / fr;
828 remainder -= secs * fr;
829 msecs = (remainder * 1000) / fr;
830 sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
834 ExportHandler::toc_escape_cdtext (const std::string& txt)
836 Glib::ustring check (txt);
838 std::string latin1_txt;
842 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
843 } catch (Glib::ConvertError& err) {
844 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
849 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
853 } else if ((*c) == '\\') {
855 } else if (isprint (*c)) {
858 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
869 ExportHandler::toc_escape_filename (const std::string& txt)
875 // We iterate byte-wise not character-wise over a UTF-8 string here,
876 // because we only want to translate backslashes and double quotes
877 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
881 } else if (*c == '\\') {
894 ExportHandler::cue_escape_cdtext (const std::string& txt)
896 std::string latin1_txt;
900 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
901 } catch (Glib::ConvertError& err) {
902 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
905 // does not do much mor than UTF-8 to Latin1 translation yet, but
906 // that may have to change if cue parsers in burning programs change
907 out = '"' + latin1_txt + '"';
912 } // namespace ARDOUR