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 , post_processing (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 bool realtime = current_timespan->realtime ();
194 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
195 // Filenames can be shared across timespans
196 FileSpec & spec = it->second;
197 spec.filename->set_timespan (it->first);
198 graph_builder->add_config (spec, realtime);
203 post_processing = false;
204 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
205 process_position = current_timespan->get_start();
206 session.start_audio_export (process_position, realtime);
210 ExportHandler::handle_duplicate_format_extensions()
212 typedef std::map<std::string, int> ExtCountMap;
215 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
216 counts[it->second.format->extension()]++;
219 bool duplicates_found = false;
220 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
221 if (it->second > 1) { duplicates_found = true; }
224 // Set this always, as the filenames are shared...
225 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
226 it->second.filename->include_format_name = duplicates_found;
231 ExportHandler::process (framecnt_t frames)
233 if (!export_status->running ()) {
235 } else if (post_processing) {
236 Glib::Threads::Mutex::Lock l (export_status->lock());
237 if (AudioEngine::instance()->freewheeling ()) {
238 return post_process ();
240 // wait until we're freewheeling
244 Glib::Threads::Mutex::Lock l (export_status->lock());
245 return process_timespan (frames);
250 ExportHandler::process_timespan (framecnt_t frames)
252 export_status->active_job = ExportStatus::Exporting;
253 /* update position */
255 framecnt_t frames_to_read = 0;
256 framepos_t const end = current_timespan->get_end();
258 bool const last_cycle = (process_position + frames >= end);
261 frames_to_read = end - process_position;
262 export_status->stop = true;
264 frames_to_read = frames;
267 process_position += frames_to_read;
268 export_status->processed_frames += frames_to_read;
269 export_status->processed_frames_current_timespan += frames_to_read;
271 /* Do actual processing */
272 int ret = graph_builder->process (frames_to_read, last_cycle);
274 /* Start post-processing/normalizing if necessary */
276 post_processing = graph_builder->need_postprocessing ();
277 if (post_processing) {
278 export_status->total_postprocessing_cycles = graph_builder->get_postprocessing_cycle_count();
279 export_status->current_postprocessing_cycle = 0;
290 ExportHandler::post_process ()
292 if (graph_builder->post_process ()) {
294 export_status->active_job = ExportStatus::Exporting;
296 if (graph_builder->realtime ()) {
297 export_status->active_job = ExportStatus::Encoding;
299 export_status->active_job = ExportStatus::Normalizing;
303 export_status->current_postprocessing_cycle++;
309 ExportHandler::command_output(std::string output, size_t size)
311 std::cerr << "command: " << size << ", " << output << std::endl;
312 info << output << endmsg;
316 ExportHandler::finish_timespan ()
318 graph_builder->get_analysis_results (export_status->result_map);
320 while (config_map.begin() != timespan_bounds.second) {
322 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
323 std::string filename = config_map.begin()->second.filename->get_path(fmt);
324 if (fmt->with_cue()) {
325 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
328 if (fmt->with_toc()) {
329 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
332 if (fmt->with_mp4chaps()) {
333 export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
336 Session::Exported (current_timespan->name(), filename); /* EMIT SIGNAL */
338 /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
339 * The process cannot access the file because it is being used.
340 * ditto for post-export and upload.
342 graph_builder->reset ();
345 /* TODO: check Umlauts and encoding in filename.
346 * TagLib eventually calls CreateFileA(),
348 export_status->active_job = ExportStatus::Tagging;
349 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
352 if (!fmt->command().empty()) {
353 SessionMetadata const & metadata (*SessionMetadata::Metadata());
355 #if 0 // would be nicer with C++11 initialiser...
356 std::map<char, std::string> subs {
358 { 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR },
359 { 'b', PBD::basename_nosuffix(filename) },
363 export_status->active_job = ExportStatus::Command;
364 PBD::ScopedConnection command_connection;
365 std::map<char, std::string> subs;
367 std::stringstream track_number;
368 track_number << metadata.track_number ();
369 std::stringstream total_tracks;
370 total_tracks << metadata.total_tracks ();
371 std::stringstream year;
372 year << metadata.year ();
374 subs.insert (std::pair<char, std::string> ('a', metadata.artist ()));
375 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
376 subs.insert (std::pair<char, std::string> ('c', metadata.copyright ()));
377 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
378 subs.insert (std::pair<char, std::string> ('f', filename));
379 subs.insert (std::pair<char, std::string> ('l', metadata.lyricist ()));
380 subs.insert (std::pair<char, std::string> ('n', session.name ()));
381 subs.insert (std::pair<char, std::string> ('s', session.path ()));
382 subs.insert (std::pair<char, std::string> ('o', metadata.conductor ()));
383 subs.insert (std::pair<char, std::string> ('t', metadata.title ()));
384 subs.insert (std::pair<char, std::string> ('z', metadata.organization ()));
385 subs.insert (std::pair<char, std::string> ('A', metadata.album ()));
386 subs.insert (std::pair<char, std::string> ('C', metadata.comment ()));
387 subs.insert (std::pair<char, std::string> ('E', metadata.engineer ()));
388 subs.insert (std::pair<char, std::string> ('G', metadata.genre ()));
389 subs.insert (std::pair<char, std::string> ('L', total_tracks.str ()));
390 subs.insert (std::pair<char, std::string> ('M', metadata.mixer ()));
391 subs.insert (std::pair<char, std::string> ('N', current_timespan->name())); // =?= config_map.begin()->first->name ()
392 subs.insert (std::pair<char, std::string> ('O', metadata.composer ()));
393 subs.insert (std::pair<char, std::string> ('P', metadata.producer ()));
394 subs.insert (std::pair<char, std::string> ('S', metadata.disc_subtitle ()));
395 subs.insert (std::pair<char, std::string> ('T', track_number.str ()));
396 subs.insert (std::pair<char, std::string> ('Y', year.str ()));
397 subs.insert (std::pair<char, std::string> ('Z', metadata.country ()));
399 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
400 info << "Post-export command line : {" << se->to_s () << "}" << endmsg;
401 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
402 int ret = se->start (2);
404 // successfully started
405 while (se->is_running ()) {
406 // wait for system exec to terminate
410 error << "Post-export command FAILED with Error: " << ret << endmsg;
415 if (fmt->soundcloud_upload()) {
416 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
417 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
418 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
419 "uploading %1 - username=%2, password=%3, token=%4",
420 filename, soundcloud_username, soundcloud_password, token) );
421 std::string path = soundcloud_uploader->Upload (
423 PBD::basename_nosuffix(filename), // title
425 soundcloud_make_public,
426 soundcloud_downloadable,
429 if (path.length() != 0) {
430 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
431 if (soundcloud_open_page) {
432 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
433 open_uri(path.c_str()); // open the soundcloud website to the new file
436 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
438 delete soundcloud_uploader;
440 config_map.erase (config_map.begin());
446 /*** CD Marker stuff ***/
448 struct LocationSortByStart {
449 bool operator() (Location *a, Location *b) {
450 return a->start() < b->start();
455 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
456 std::string filename, CDMarkerFormat format)
458 string filepath = get_cd_marker_filename(filename, format);
461 void (ExportHandler::*header_func) (CDMarkerStatus &);
462 void (ExportHandler::*track_func) (CDMarkerStatus &);
463 void (ExportHandler::*index_func) (CDMarkerStatus &);
467 header_func = &ExportHandler::write_toc_header;
468 track_func = &ExportHandler::write_track_info_toc;
469 index_func = &ExportHandler::write_index_info_toc;
472 header_func = &ExportHandler::write_cue_header;
473 track_func = &ExportHandler::write_track_info_cue;
474 index_func = &ExportHandler::write_index_info_cue;
477 header_func = &ExportHandler::write_mp4ch_header;
478 track_func = &ExportHandler::write_track_info_mp4ch;
479 index_func = &ExportHandler::write_index_info_mp4ch;
485 CDMarkerStatus status (filepath, timespan, file_format, filename);
487 (this->*header_func) (status);
489 /* Get locations and sort */
491 Locations::LocationList const & locations (session.locations()->list());
492 Locations::LocationList::const_iterator i;
493 Locations::LocationList temp;
495 for (i = locations.begin(); i != locations.end(); ++i) {
496 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
502 // TODO One index marker for whole thing
506 LocationSortByStart cmp;
508 Locations::LocationList::const_iterator nexti;
510 /* Start actual marker stuff */
512 framepos_t last_end_time = timespan->get_start();
513 status.track_position = 0;
515 for (i = temp.begin(); i != temp.end(); ++i) {
519 if ((*i)->start() < last_end_time) {
520 if ((*i)->is_mark()) {
521 /* Index within track */
523 status.index_position = (*i)->start() - timespan->get_start();
524 (this->*index_func) (status);
530 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
532 status.track_position = last_end_time - timespan->get_start();
533 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
534 status.track_duration = 0;
536 if ((*i)->is_mark()) {
537 // a mark track location needs to look ahead to the next marker's start to determine length
541 if (nexti != temp.end()) {
542 status.track_duration = (*nexti)->start() - last_end_time;
544 last_end_time = (*nexti)->start();
546 // this was the last marker, use timespan end
547 status.track_duration = timespan->get_end() - last_end_time;
549 last_end_time = timespan->get_end();
553 status.track_duration = (*i)->end() - last_end_time;
555 last_end_time = (*i)->end();
558 (this->*track_func) (status);
561 } catch (std::exception& e) {
562 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
563 ::g_unlink (filepath.c_str());
564 } catch (Glib::Exception& e) {
565 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
566 ::g_unlink (filepath.c_str());
571 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
573 /* do not strip file suffix because there may be more than one format,
574 and we do not want the CD marker file from one format to overwrite
575 another (e.g. foo.wav.cue > foo.aiff.cue)
580 return filename + ".toc";
582 return filename + ".cue";
585 unsigned lastdot = filename.find_last_of('.');
586 return filename.substr(0,lastdot) + ".chapters.txt";
589 return filename + ".marker"; // Should not be reached when actually creating a file
594 ExportHandler::write_cue_header (CDMarkerStatus & status)
596 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
599 string barcode = SessionMetadata::Metadata()->barcode();
600 string album_artist = SessionMetadata::Metadata()->album_artist();
601 string album_title = SessionMetadata::Metadata()->album();
603 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
606 status.out << "CATALOG " << barcode << endl;
608 if (album_artist != "")
609 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
611 if (album_title != "")
614 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
616 /* The original cue sheet spec mentions five file types
618 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
619 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
622 We try to use these file types whenever appropriate and
623 default to our own names otherwise.
625 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
626 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
627 status.out << "WAVE";
628 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
629 status.format->sample_format() == ExportFormatBase::SF_16 &&
630 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
631 // Format is RAW 16bit 44.1kHz
632 if (status.format->endianness() == ExportFormatBase::E_Little) {
633 status.out << "BINARY";
635 status.out << "MOTOROLA";
638 // no special case for AIFF format it's name is already "AIFF"
639 status.out << status.format->format_name();
645 ExportHandler::write_toc_header (CDMarkerStatus & status)
647 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
650 string barcode = SessionMetadata::Metadata()->barcode();
651 string album_artist = SessionMetadata::Metadata()->album_artist();
652 string album_title = SessionMetadata::Metadata()->album();
655 status.out << "CATALOG \"" << barcode << "\"" << endl;
657 if (album_title != "")
660 status.out << "CD_DA" << endl;
661 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
662 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
663 status.out << " PERFORMER " << toc_escape_cdtext (album_artist) << endl;
664 status.out << " }" << endl << "}" << endl;
668 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
670 status.out << "00:00:00.000 Intro" << endl;
674 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
678 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
679 status.out << buf << endl;
681 status.out << " FLAGS" ;
682 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
683 status.out << " SCMS ";
685 status.out << " DCP ";
688 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
689 status.out << " PRE";
693 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
694 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
697 if (status.marker->name() != "") {
698 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
701 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
702 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
705 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
706 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
709 if (status.track_position != status.track_start_frame) {
710 frames_to_cd_frames_string (buf, status.track_position);
711 status.out << " INDEX 00" << buf << endl;
714 frames_to_cd_frames_string (buf, status.track_start_frame);
715 status.out << " INDEX 01" << buf << endl;
717 status.index_number = 2;
718 status.track_number++;
722 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
726 status.out << endl << "TRACK AUDIO" << endl;
728 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
731 status.out << "COPY" << endl;
733 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
734 status.out << "PRE_EMPHASIS" << endl;
736 status.out << "NO PRE_EMPHASIS" << endl;
739 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
740 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
743 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
744 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
746 status.out << " PERFORMER ";
747 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
748 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
750 status.out << "\"\"" << endl;
753 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
754 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
757 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
758 status.out << " ISRC \"";
759 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
760 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
761 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
762 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
765 status.out << " }" << endl << "}" << endl;
767 frames_to_cd_frames_string (buf, status.track_position);
768 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
770 frames_to_cd_frames_string (buf, status.track_duration);
771 status.out << buf << endl;
773 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
774 status.out << "START" << buf << endl;
777 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
781 frames_to_chapter_marks_string(buf, status.track_start_frame);
782 status.out << buf << " " << status.marker->name() << endl;
786 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
790 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
792 frames_to_cd_frames_string (buf, status.index_position);
793 status.out << buf << endl;
799 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
803 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
804 status.out << "INDEX" << buf << endl;
808 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
813 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
815 framecnt_t remainder;
816 framecnt_t fr = session.nominal_frame_rate();
817 int mins, secs, frames;
819 mins = when / (60 * fr);
820 remainder = when - (mins * 60 * fr);
821 secs = remainder / fr;
822 remainder -= secs * fr;
823 frames = remainder / (fr / 75);
824 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
828 ExportHandler::frames_to_chapter_marks_string (char* buf, framepos_t when)
830 framecnt_t remainder;
831 framecnt_t fr = session.nominal_frame_rate();
832 int hours, mins, secs, msecs;
834 hours = when / (3600 * fr);
835 remainder = when - (hours * 3600 * fr);
836 mins = remainder / (60 * fr);
837 remainder -= mins * 60 * fr;
838 secs = remainder / fr;
839 remainder -= secs * fr;
840 msecs = (remainder * 1000) / fr;
841 sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
845 ExportHandler::toc_escape_cdtext (const std::string& txt)
847 Glib::ustring check (txt);
849 std::string latin1_txt;
853 latin1_txt = Glib::convert_with_fallback (txt, "ISO-8859-1", "UTF-8", "_");
854 } catch (Glib::ConvertError& err) {
855 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
860 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
864 } else if ((*c) == '\\') {
866 } else if (isprint (*c)) {
869 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
880 ExportHandler::toc_escape_filename (const std::string& txt)
886 // We iterate byte-wise not character-wise over a UTF-8 string here,
887 // because we only want to translate backslashes and double quotes
888 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
892 } else if (*c == '\\') {
905 ExportHandler::cue_escape_cdtext (const std::string& txt)
907 std::string latin1_txt;
911 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
912 } catch (Glib::ConvertError& err) {
913 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
916 // does not do much mor than UTF-8 to Latin1 translation yet, but
917 // that may have to change if cue parsers in burning programs change
918 out = '"' + latin1_txt + '"';
923 } // namespace ARDOUR