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/debug.h"
32 #include "ardour/export_graph_builder.h"
33 #include "ardour/export_timespan.h"
34 #include "ardour/export_channel_configuration.h"
35 #include "ardour/export_status.h"
36 #include "ardour/export_format_specification.h"
37 #include "ardour/export_filename.h"
38 #include "ardour/soundcloud_upload.h"
39 #include "ardour/system_exec.h"
40 #include "pbd/openuri.h"
41 #include "pbd/basename.h"
42 #include "ardour/session_metadata.h"
52 /*** ExportElementFactory ***/
54 ExportElementFactory::ExportElementFactory (Session & session) :
60 ExportElementFactory::~ExportElementFactory ()
66 ExportElementFactory::add_timespan ()
68 return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
71 ExportChannelConfigPtr
72 ExportElementFactory::add_channel_config ()
74 return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
78 ExportElementFactory::add_format ()
80 return ExportFormatSpecPtr (new ExportFormatSpecification (session));
84 ExportElementFactory::add_format (XMLNode const & state)
86 return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
90 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
92 return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
96 ExportElementFactory::add_filename ()
98 return ExportFilenamePtr (new ExportFilename (session));
102 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
104 return ExportFilenamePtr (new ExportFilename (*other));
107 /*** ExportHandler ***/
109 ExportHandler::ExportHandler (Session & session)
110 : ExportElementFactory (session)
112 , graph_builder (new ExportGraphBuilder (session))
113 , export_status (session.get_export_status ())
114 , normalizing (false)
120 ExportHandler::~ExportHandler ()
122 graph_builder->cleanup (export_status->aborted () );
125 /** Add an export to the `to-do' list */
127 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
128 ExportFormatSpecPtr format, ExportFilenamePtr filename,
129 BroadcastInfoPtr broadcast_info)
131 FileSpec spec (channel_config, format, filename, broadcast_info);
132 config_map.insert (make_pair (timespan, spec));
138 ExportHandler::do_export ()
140 /* Count timespans */
142 export_status->init();
143 std::set<ExportTimespanPtr> timespan_set;
144 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
145 bool new_timespan = timespan_set.insert (it->first).second;
147 export_status->total_frames += it->first->get_length();
150 export_status->total_timespans = timespan_set.size();
152 if (export_status->total_timespans > 1) {
153 // always include timespan if there's more than one.
154 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
155 FileSpec & spec = it->second;
156 spec.filename->include_timespan = true;
162 Glib::Threads::Mutex::Lock l (export_status->lock());
167 ExportHandler::start_timespan ()
169 export_status->timespan++;
171 if (config_map.empty()) {
172 // freewheeling has to be stopped from outside the process cycle
173 export_status->set_running (false);
177 /* finish_timespan pops the config_map entry that has been done, so
178 this is the timespan to do this time
180 current_timespan = config_map.begin()->first;
182 export_status->total_frames_current_timespan = current_timespan->get_length();
183 export_status->timespan_name = current_timespan->name();
184 export_status->processed_frames_current_timespan = 0;
186 /* Register file configurations to graph builder */
188 /* Here's the config_map entries that use this timespan */
189 timespan_bounds = config_map.equal_range (current_timespan);
190 graph_builder->reset ();
191 graph_builder->set_current_timespan (current_timespan);
192 handle_duplicate_format_extensions();
193 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
194 // Filenames can be shared across timespans
195 FileSpec & spec = it->second;
196 spec.filename->set_timespan (it->first);
197 graph_builder->add_config (spec);
203 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
204 process_position = current_timespan->get_start();
205 session.start_audio_export (process_position);
209 ExportHandler::handle_duplicate_format_extensions()
211 typedef std::map<std::string, int> ExtCountMap;
214 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
215 counts[it->second.format->extension()]++;
218 bool duplicates_found = false;
219 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
220 if (it->second > 1) { duplicates_found = true; }
223 // Set this always, as the filenames are shared...
224 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
225 it->second.filename->include_format_name = duplicates_found;
230 ExportHandler::process (framecnt_t frames)
232 if (!export_status->running ()) {
234 } else if (normalizing) {
235 Glib::Threads::Mutex::Lock l (export_status->lock());
236 if (AudioEngine::instance()->freewheeling ()) {
237 return process_normalize ();
239 // wait until we're freewheeling
243 Glib::Threads::Mutex::Lock l (export_status->lock());
244 return process_timespan (frames);
249 ExportHandler::process_timespan (framecnt_t frames)
251 export_status->active_job = ExportStatus::Exporting;
252 /* update position */
254 framecnt_t frames_to_read = 0;
255 framepos_t const end = current_timespan->get_end();
257 bool const last_cycle = (process_position + frames >= end);
260 frames_to_read = end - process_position;
261 export_status->stop = true;
263 frames_to_read = frames;
266 process_position += frames_to_read;
267 export_status->processed_frames += frames_to_read;
268 export_status->processed_frames_current_timespan += frames_to_read;
270 /* Do actual processing */
271 int ret = graph_builder->process (frames_to_read, last_cycle);
273 /* Start normalizing if necessary */
275 normalizing = graph_builder->will_normalize();
277 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
278 export_status->current_normalize_cycle = 0;
289 ExportHandler::process_normalize ()
291 if (graph_builder->process_normalize ()) {
293 export_status->active_job = ExportStatus::Exporting;
295 export_status->active_job = ExportStatus::Normalizing;
298 export_status->current_normalize_cycle++;
304 ExportHandler::command_output(std::string output, size_t size)
306 std::cerr << "command: " << size << ", " << output << std::endl;
307 info << output << endmsg;
311 ExportHandler::finish_timespan ()
313 graph_builder->get_analysis_results (export_status->result_map);
315 while (config_map.begin() != timespan_bounds.second) {
317 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
318 std::string filename = config_map.begin()->second.filename->get_path(fmt);
319 if (fmt->with_cue()) {
320 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
323 if (fmt->with_toc()) {
324 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
327 if (fmt->with_mp4chaps()) {
328 export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
331 Session::Exported (current_timespan->name(), filename); /* EMIT SIGNAL */
333 /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
334 * The process cannot access the file because it is being used.
335 * ditto for post-export and upload.
337 graph_builder->reset ();
340 /* TODO: check Umlauts and encoding in filename.
341 * TagLib eventually calls CreateFileA(),
343 export_status->active_job = ExportStatus::Tagging;
344 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
347 if (!fmt->command().empty()) {
348 SessionMetadata const & metadata (*SessionMetadata::Metadata());
350 #if 0 // would be nicer with C++11 initialiser...
351 std::map<char, std::string> subs {
353 { 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR },
354 { 'b', PBD::basename_nosuffix(filename) },
358 export_status->active_job = ExportStatus::Command;
359 PBD::ScopedConnection command_connection;
360 std::map<char, std::string> subs;
362 std::stringstream track_number;
363 track_number << metadata.track_number ();
364 std::stringstream total_tracks;
365 total_tracks << metadata.total_tracks ();
366 std::stringstream year;
367 year << metadata.year ();
369 subs.insert (std::pair<char, std::string> ('a', metadata.artist ()));
370 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
371 subs.insert (std::pair<char, std::string> ('c', metadata.copyright ()));
372 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
373 subs.insert (std::pair<char, std::string> ('f', filename));
374 subs.insert (std::pair<char, std::string> ('l', metadata.lyricist ()));
375 subs.insert (std::pair<char, std::string> ('n', session.name ()));
376 subs.insert (std::pair<char, std::string> ('s', session.path ()));
377 subs.insert (std::pair<char, std::string> ('o', metadata.conductor ()));
378 subs.insert (std::pair<char, std::string> ('t', metadata.title ()));
379 subs.insert (std::pair<char, std::string> ('z', metadata.organization ()));
380 subs.insert (std::pair<char, std::string> ('A', metadata.album ()));
381 subs.insert (std::pair<char, std::string> ('C', metadata.comment ()));
382 subs.insert (std::pair<char, std::string> ('E', metadata.engineer ()));
383 subs.insert (std::pair<char, std::string> ('G', metadata.genre ()));
384 subs.insert (std::pair<char, std::string> ('L', total_tracks.str ()));
385 subs.insert (std::pair<char, std::string> ('M', metadata.mixer ()));
386 subs.insert (std::pair<char, std::string> ('N', current_timespan->name())); // =?= config_map.begin()->first->name ()
387 subs.insert (std::pair<char, std::string> ('O', metadata.composer ()));
388 subs.insert (std::pair<char, std::string> ('P', metadata.producer ()));
389 subs.insert (std::pair<char, std::string> ('S', metadata.disc_subtitle ()));
390 subs.insert (std::pair<char, std::string> ('T', track_number.str ()));
391 subs.insert (std::pair<char, std::string> ('Y', year.str ()));
392 subs.insert (std::pair<char, std::string> ('Z', metadata.country ()));
394 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
395 info << "Post-export command line : {" << se->to_s () << "}" << endmsg;
396 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
397 int ret = se->start (2);
399 // successfully started
400 while (se->is_running ()) {
401 // wait for system exec to terminate
405 error << "Post-export command FAILED with Error: " << ret << endmsg;
410 if (fmt->soundcloud_upload()) {
411 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
412 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
413 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
414 "uploading %1 - username=%2, password=%3, token=%4",
415 filename, soundcloud_username, soundcloud_password, token) );
416 std::string path = soundcloud_uploader->Upload (
418 PBD::basename_nosuffix(filename), // title
420 soundcloud_make_public,
421 soundcloud_downloadable,
424 if (path.length() != 0) {
425 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
426 if (soundcloud_open_page) {
427 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
428 open_uri(path.c_str()); // open the soundcloud website to the new file
431 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
433 delete soundcloud_uploader;
435 config_map.erase (config_map.begin());
441 /*** CD Marker stuff ***/
443 struct LocationSortByStart {
444 bool operator() (Location *a, Location *b) {
445 return a->start() < b->start();
450 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
451 std::string filename, CDMarkerFormat format)
453 string filepath = get_cd_marker_filename(filename, format);
456 void (ExportHandler::*header_func) (CDMarkerStatus &);
457 void (ExportHandler::*track_func) (CDMarkerStatus &);
458 void (ExportHandler::*index_func) (CDMarkerStatus &);
462 header_func = &ExportHandler::write_toc_header;
463 track_func = &ExportHandler::write_track_info_toc;
464 index_func = &ExportHandler::write_index_info_toc;
467 header_func = &ExportHandler::write_cue_header;
468 track_func = &ExportHandler::write_track_info_cue;
469 index_func = &ExportHandler::write_index_info_cue;
472 header_func = &ExportHandler::write_mp4ch_header;
473 track_func = &ExportHandler::write_track_info_mp4ch;
474 index_func = &ExportHandler::write_index_info_mp4ch;
480 CDMarkerStatus status (filepath, timespan, file_format, filename);
482 (this->*header_func) (status);
484 /* Get locations and sort */
486 Locations::LocationList const & locations (session.locations()->list());
487 Locations::LocationList::const_iterator i;
488 Locations::LocationList temp;
490 for (i = locations.begin(); i != locations.end(); ++i) {
491 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
497 // TODO One index marker for whole thing
501 LocationSortByStart cmp;
503 Locations::LocationList::const_iterator nexti;
505 /* Start actual marker stuff */
507 framepos_t last_end_time = timespan->get_start();
508 status.track_position = 0;
510 for (i = temp.begin(); i != temp.end(); ++i) {
514 if ((*i)->start() < last_end_time) {
515 if ((*i)->is_mark()) {
516 /* Index within track */
518 status.index_position = (*i)->start() - timespan->get_start();
519 (this->*index_func) (status);
525 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
527 status.track_position = last_end_time - timespan->get_start();
528 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
529 status.track_duration = 0;
531 if ((*i)->is_mark()) {
532 // a mark track location needs to look ahead to the next marker's start to determine length
536 if (nexti != temp.end()) {
537 status.track_duration = (*nexti)->start() - last_end_time;
539 last_end_time = (*nexti)->start();
541 // this was the last marker, use timespan end
542 status.track_duration = timespan->get_end() - last_end_time;
544 last_end_time = timespan->get_end();
548 status.track_duration = (*i)->end() - last_end_time;
550 last_end_time = (*i)->end();
553 (this->*track_func) (status);
556 } catch (std::exception& e) {
557 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
558 ::g_unlink (filepath.c_str());
559 } catch (Glib::Exception& e) {
560 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
561 ::g_unlink (filepath.c_str());
566 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
568 /* do not strip file suffix because there may be more than one format,
569 and we do not want the CD marker file from one format to overwrite
570 another (e.g. foo.wav.cue > foo.aiff.cue)
575 return filename + ".toc";
577 return filename + ".cue";
580 unsigned lastdot = filename.find_last_of('.');
581 return filename.substr(0,lastdot) + ".chapters.txt";
584 return filename + ".marker"; // Should not be reached when actually creating a file
589 ExportHandler::write_cue_header (CDMarkerStatus & status)
591 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
594 string barcode = SessionMetadata::Metadata()->barcode();
595 string album_artist = SessionMetadata::Metadata()->album_artist();
596 string album_title = SessionMetadata::Metadata()->album();
598 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
601 status.out << "CATALOG " << barcode << endl;
603 if (album_artist != "")
604 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
606 if (album_title != "")
609 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
611 /* The original cue sheet spec mentions five file types
613 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
614 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
617 We try to use these file types whenever appropriate and
618 default to our own names otherwise.
620 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
621 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
622 status.out << "WAVE";
623 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
624 status.format->sample_format() == ExportFormatBase::SF_16 &&
625 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
626 // Format is RAW 16bit 44.1kHz
627 if (status.format->endianness() == ExportFormatBase::E_Little) {
628 status.out << "BINARY";
630 status.out << "MOTOROLA";
633 // no special case for AIFF format it's name is already "AIFF"
634 status.out << status.format->format_name();
640 ExportHandler::write_toc_header (CDMarkerStatus & status)
642 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
645 string barcode = SessionMetadata::Metadata()->barcode();
646 string album_artist = SessionMetadata::Metadata()->album_artist();
647 string album_title = SessionMetadata::Metadata()->album();
650 status.out << "CATALOG \"" << barcode << "\"" << endl;
652 if (album_title != "")
655 status.out << "CD_DA" << endl;
656 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
657 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
658 status.out << " PERFORMER " << toc_escape_cdtext (album_artist) << endl;
659 status.out << " }" << endl << "}" << endl;
663 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
665 status.out << "00:00:00.000 Intro" << endl;
669 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
673 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
674 status.out << buf << endl;
676 status.out << " FLAGS" ;
677 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
678 status.out << " SCMS ";
680 status.out << " DCP ";
683 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
684 status.out << " PRE";
688 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
689 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
692 if (status.marker->name() != "") {
693 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
696 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
697 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
700 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
701 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
704 if (status.track_position != status.track_start_frame) {
705 frames_to_cd_frames_string (buf, status.track_position);
706 status.out << " INDEX 00" << buf << endl;
709 frames_to_cd_frames_string (buf, status.track_start_frame);
710 status.out << " INDEX 01" << buf << endl;
712 status.index_number = 2;
713 status.track_number++;
717 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
721 status.out << endl << "TRACK AUDIO" << endl;
723 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
726 status.out << "COPY" << endl;
728 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
729 status.out << "PRE_EMPHASIS" << endl;
731 status.out << "NO PRE_EMPHASIS" << endl;
734 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
735 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
738 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
739 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
741 status.out << " PERFORMER ";
742 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
743 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
745 status.out << "\"\"" << endl;
748 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
749 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
752 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
753 status.out << " ISRC \"";
754 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
755 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
756 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
757 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
760 status.out << " }" << endl << "}" << endl;
762 frames_to_cd_frames_string (buf, status.track_position);
763 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
765 frames_to_cd_frames_string (buf, status.track_duration);
766 status.out << buf << endl;
768 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
769 status.out << "START" << buf << endl;
772 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
776 frames_to_chapter_marks_string(buf, status.track_start_frame);
777 status.out << buf << " " << status.marker->name() << endl;
781 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
785 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
787 frames_to_cd_frames_string (buf, status.index_position);
788 status.out << buf << endl;
794 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
798 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
799 status.out << "INDEX" << buf << endl;
803 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
808 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
810 framecnt_t remainder;
811 framecnt_t fr = session.nominal_frame_rate();
812 int mins, secs, frames;
814 mins = when / (60 * fr);
815 remainder = when - (mins * 60 * fr);
816 secs = remainder / fr;
817 remainder -= secs * fr;
818 frames = remainder / (fr / 75);
819 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
823 ExportHandler::frames_to_chapter_marks_string (char* buf, framepos_t when)
825 framecnt_t remainder;
826 framecnt_t fr = session.nominal_frame_rate();
827 int hours, mins, secs, msecs;
829 hours = when / (3600 * fr);
830 remainder = when - (hours * 3600 * fr);
831 mins = remainder / (60 * fr);
832 remainder -= mins * 60 * fr;
833 secs = remainder / fr;
834 remainder -= secs * fr;
835 msecs = (remainder * 1000) / fr;
836 sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
840 ExportHandler::toc_escape_cdtext (const std::string& txt)
842 Glib::ustring check (txt);
844 std::string latin1_txt;
848 latin1_txt = Glib::convert_with_fallback (txt, "ISO-8859-1", "UTF-8", "_");
849 } catch (Glib::ConvertError& err) {
850 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
855 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
859 } else if ((*c) == '\\') {
861 } else if (isprint (*c)) {
864 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
875 ExportHandler::toc_escape_filename (const std::string& txt)
881 // We iterate byte-wise not character-wise over a UTF-8 string here,
882 // because we only want to translate backslashes and double quotes
883 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
887 } else if (*c == '\\') {
900 ExportHandler::cue_escape_cdtext (const std::string& txt)
902 std::string latin1_txt;
906 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
907 } catch (Glib::ConvertError& err) {
908 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
911 // does not do much mor than UTF-8 to Latin1 translation yet, but
912 // that may have to change if cue parsers in burning programs change
913 out = '"' + latin1_txt + '"';
918 } // namespace ARDOUR