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 if (it->second.filename->include_channel_config && it->second.channel_config) {
231 /* stem-export has multiple files in the same timestamp, but a different channel_config for each.
232 * However channel_config is only set in ExportGraphBuilder::Encoder::init_writer()
233 * so we cannot yet use it->second.filename->get_path(it->second.format).
234 * We have to explicily check uniqueness of "channel-config + extension" here:
236 counts[it->second.channel_config->name() + it->second.format->extension()]++;
238 counts[it->second.format->extension()]++;
242 bool duplicates_found = false;
243 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
244 if (it->second > 1) { duplicates_found = true; }
247 // Set this always, as the filenames are shared...
248 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
249 it->second.filename->include_format_name = duplicates_found;
254 ExportHandler::process (samplecnt_t samples)
256 if (!export_status->running ()) {
258 } else if (post_processing) {
259 Glib::Threads::Mutex::Lock l (export_status->lock());
260 if (AudioEngine::instance()->freewheeling ()) {
261 return post_process ();
263 // wait until we're freewheeling
267 Glib::Threads::Mutex::Lock l (export_status->lock());
268 return process_timespan (samples);
273 ExportHandler::process_timespan (samplecnt_t samples)
275 export_status->active_job = ExportStatus::Exporting;
276 /* update position */
278 samplecnt_t samples_to_read = 0;
279 samplepos_t const end = current_timespan->get_end();
281 bool const last_cycle = (process_position + samples >= end);
284 samples_to_read = end - process_position;
285 export_status->stop = true;
287 samples_to_read = samples;
290 process_position += samples_to_read;
291 export_status->processed_samples += samples_to_read;
292 export_status->processed_samples_current_timespan += samples_to_read;
294 /* Do actual processing */
295 int ret = graph_builder->process (samples_to_read, last_cycle);
297 /* Start post-processing/normalizing if necessary */
299 post_processing = graph_builder->need_postprocessing ();
300 if (post_processing) {
301 export_status->total_postprocessing_cycles = graph_builder->get_postprocessing_cycle_count();
302 export_status->current_postprocessing_cycle = 0;
313 ExportHandler::post_process ()
315 if (graph_builder->post_process ()) {
317 export_status->active_job = ExportStatus::Exporting;
319 if (graph_builder->realtime ()) {
320 export_status->active_job = ExportStatus::Encoding;
322 export_status->active_job = ExportStatus::Normalizing;
326 export_status->current_postprocessing_cycle++;
332 ExportHandler::command_output(std::string output, size_t size)
334 std::cerr << "command: " << size << ", " << output << std::endl;
335 info << output << endmsg;
339 ExportHandler::finish_timespan ()
341 graph_builder->get_analysis_results (export_status->result_map);
343 while (config_map.begin() != timespan_bounds.second) {
345 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
346 std::string filename = config_map.begin()->second.filename->get_path(fmt);
347 if (fmt->with_cue()) {
348 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
351 if (fmt->with_toc()) {
352 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
355 if (fmt->with_mp4chaps()) {
356 export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
359 Session::Exported (current_timespan->name(), filename); /* EMIT SIGNAL */
361 /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
362 * The process cannot access the file because it is being used.
363 * ditto for post-export and upload.
365 graph_builder->reset ();
368 /* TODO: check Umlauts and encoding in filename.
369 * TagLib eventually calls CreateFileA(),
371 export_status->active_job = ExportStatus::Tagging;
372 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
375 if (!fmt->command().empty()) {
376 SessionMetadata const & metadata (*SessionMetadata::Metadata());
378 #if 0 // would be nicer with C++11 initialiser...
379 std::map<char, std::string> subs {
381 { 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR },
382 { 'b', PBD::basename_nosuffix(filename) },
386 export_status->active_job = ExportStatus::Command;
387 PBD::ScopedConnection command_connection;
388 std::map<char, std::string> subs;
390 std::stringstream track_number;
391 track_number << metadata.track_number ();
392 std::stringstream total_tracks;
393 total_tracks << metadata.total_tracks ();
394 std::stringstream year;
395 year << metadata.year ();
397 subs.insert (std::pair<char, std::string> ('a', metadata.artist ()));
398 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
399 subs.insert (std::pair<char, std::string> ('c', metadata.copyright ()));
400 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
401 subs.insert (std::pair<char, std::string> ('f', filename));
402 subs.insert (std::pair<char, std::string> ('l', metadata.lyricist ()));
403 subs.insert (std::pair<char, std::string> ('n', session.name ()));
404 subs.insert (std::pair<char, std::string> ('s', session.path ()));
405 subs.insert (std::pair<char, std::string> ('o', metadata.conductor ()));
406 subs.insert (std::pair<char, std::string> ('t', metadata.title ()));
407 subs.insert (std::pair<char, std::string> ('z', metadata.organization ()));
408 subs.insert (std::pair<char, std::string> ('A', metadata.album ()));
409 subs.insert (std::pair<char, std::string> ('C', metadata.comment ()));
410 subs.insert (std::pair<char, std::string> ('E', metadata.engineer ()));
411 subs.insert (std::pair<char, std::string> ('G', metadata.genre ()));
412 subs.insert (std::pair<char, std::string> ('L', total_tracks.str ()));
413 subs.insert (std::pair<char, std::string> ('M', metadata.mixer ()));
414 subs.insert (std::pair<char, std::string> ('N', current_timespan->name())); // =?= config_map.begin()->first->name ()
415 subs.insert (std::pair<char, std::string> ('O', metadata.composer ()));
416 subs.insert (std::pair<char, std::string> ('P', metadata.producer ()));
417 subs.insert (std::pair<char, std::string> ('S', metadata.disc_subtitle ()));
418 subs.insert (std::pair<char, std::string> ('T', track_number.str ()));
419 subs.insert (std::pair<char, std::string> ('Y', year.str ()));
420 subs.insert (std::pair<char, std::string> ('Z', metadata.country ()));
422 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
423 info << "Post-export command line : {" << se->to_s () << "}" << endmsg;
424 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
425 int ret = se->start (SystemExec::MergeWithStdin);
427 // successfully started
428 while (se->is_running ()) {
429 // wait for system exec to terminate
433 error << "Post-export command FAILED with Error: " << ret << endmsg;
438 // XXX THIS IS IN REALTIME CONTEXT, CALLED FROM
439 // AudioEngine::process_callback()
440 // freewheeling, yes, but still uploading here is NOT
443 // even less so, since SoundcloudProgress is using
444 // connect_same_thread() - GUI updates from the RT thread
445 // will cause crashes. http://pastebin.com/UJKYNGHR
446 if (fmt->soundcloud_upload()) {
447 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
448 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
449 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
450 "uploading %1 - username=%2, password=%3, token=%4",
451 filename, soundcloud_username, soundcloud_password, token) );
452 std::string path = soundcloud_uploader->Upload (
454 PBD::basename_nosuffix(filename), // title
456 soundcloud_make_public,
457 soundcloud_downloadable,
460 if (path.length() != 0) {
461 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
462 if (soundcloud_open_page) {
463 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
464 open_uri(path.c_str()); // open the soundcloud website to the new file
467 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
469 delete soundcloud_uploader;
471 config_map.erase (config_map.begin());
478 ExportHandler::reset ()
481 graph_builder->reset ();
484 /*** CD Marker stuff ***/
486 struct LocationSortByStart {
487 bool operator() (Location *a, Location *b) {
488 return a->start() < b->start();
493 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
494 std::string filename, CDMarkerFormat format)
496 string filepath = get_cd_marker_filename(filename, format);
499 void (ExportHandler::*header_func) (CDMarkerStatus &);
500 void (ExportHandler::*track_func) (CDMarkerStatus &);
501 void (ExportHandler::*index_func) (CDMarkerStatus &);
505 header_func = &ExportHandler::write_toc_header;
506 track_func = &ExportHandler::write_track_info_toc;
507 index_func = &ExportHandler::write_index_info_toc;
510 header_func = &ExportHandler::write_cue_header;
511 track_func = &ExportHandler::write_track_info_cue;
512 index_func = &ExportHandler::write_index_info_cue;
515 header_func = &ExportHandler::write_mp4ch_header;
516 track_func = &ExportHandler::write_track_info_mp4ch;
517 index_func = &ExportHandler::write_index_info_mp4ch;
523 CDMarkerStatus status (filepath, timespan, file_format, filename);
525 (this->*header_func) (status);
527 /* Get locations and sort */
529 Locations::LocationList const & locations (session.locations()->list());
530 Locations::LocationList::const_iterator i;
531 Locations::LocationList temp;
533 for (i = locations.begin(); i != locations.end(); ++i) {
534 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
540 // TODO One index marker for whole thing
544 LocationSortByStart cmp;
546 Locations::LocationList::const_iterator nexti;
548 /* Start actual marker stuff */
550 samplepos_t last_end_time = timespan->get_start();
551 status.track_position = 0;
553 for (i = temp.begin(); i != temp.end(); ++i) {
557 if ((*i)->start() < last_end_time) {
558 if ((*i)->is_mark()) {
559 /* Index within track */
561 status.index_position = (*i)->start() - timespan->get_start();
562 (this->*index_func) (status);
568 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
570 status.track_position = last_end_time - timespan->get_start();
571 status.track_start_sample = (*i)->start() - timespan->get_start(); // everything before this is the pregap
572 status.track_duration = 0;
574 if ((*i)->is_mark()) {
575 // a mark track location needs to look ahead to the next marker's start to determine length
579 if (nexti != temp.end()) {
580 status.track_duration = (*nexti)->start() - last_end_time;
582 last_end_time = (*nexti)->start();
584 // this was the last marker, use timespan end
585 status.track_duration = timespan->get_end() - last_end_time;
587 last_end_time = timespan->get_end();
591 status.track_duration = (*i)->end() - last_end_time;
593 last_end_time = (*i)->end();
596 (this->*track_func) (status);
599 } catch (std::exception& e) {
600 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
601 ::g_unlink (filepath.c_str());
602 } catch (Glib::Exception& e) {
603 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
604 ::g_unlink (filepath.c_str());
609 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
611 /* do not strip file suffix because there may be more than one format,
612 and we do not want the CD marker file from one format to overwrite
613 another (e.g. foo.wav.cue > foo.aiff.cue)
618 return filename + ".toc";
620 return filename + ".cue";
623 unsigned lastdot = filename.find_last_of('.');
624 return filename.substr(0,lastdot) + ".chapters.txt";
627 return filename + ".marker"; // Should not be reached when actually creating a file
632 ExportHandler::write_cue_header (CDMarkerStatus & status)
634 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
637 string barcode = SessionMetadata::Metadata()->barcode();
638 string album_artist = SessionMetadata::Metadata()->album_artist();
639 string album_title = SessionMetadata::Metadata()->album();
641 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
644 status.out << "CATALOG " << barcode << endl;
646 if (album_artist != "")
647 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
649 if (album_title != "")
652 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
654 /* The original cue sheet spec mentions five file types
656 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
657 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
660 We try to use these file types whenever appropriate and
661 default to our own names otherwise.
663 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
664 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
665 status.out << "WAVE";
666 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
667 status.format->sample_format() == ExportFormatBase::SF_16 &&
668 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
669 // Format is RAW 16bit 44.1kHz
670 if (status.format->endianness() == ExportFormatBase::E_Little) {
671 status.out << "BINARY";
673 status.out << "MOTOROLA";
676 // no special case for AIFF format it's name is already "AIFF"
677 status.out << status.format->format_name();
683 ExportHandler::write_toc_header (CDMarkerStatus & status)
685 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
688 string barcode = SessionMetadata::Metadata()->barcode();
689 string album_artist = SessionMetadata::Metadata()->album_artist();
690 string album_title = SessionMetadata::Metadata()->album();
693 status.out << "CATALOG \"" << barcode << "\"" << endl;
695 if (album_title != "")
698 status.out << "CD_DA" << endl;
699 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
700 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
701 status.out << " PERFORMER " << toc_escape_cdtext (album_artist) << endl;
702 status.out << " }" << endl << "}" << endl;
706 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
708 status.out << "00:00:00.000 Intro" << endl;
712 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
716 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
717 status.out << buf << endl;
719 status.out << " FLAGS" ;
720 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
721 status.out << " SCMS ";
723 status.out << " DCP ";
726 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
727 status.out << " PRE";
731 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
732 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
735 if (status.marker->name() != "") {
736 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
739 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
740 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
743 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
744 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
747 if (status.track_position != status.track_start_sample) {
748 samples_to_cd_samples_string (buf, status.track_position);
749 status.out << " INDEX 00" << buf << endl;
752 samples_to_cd_samples_string (buf, status.track_start_sample);
753 status.out << " INDEX 01" << buf << endl;
755 status.index_number = 2;
756 status.track_number++;
760 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
764 status.out << endl << "TRACK AUDIO" << endl;
766 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
769 status.out << "COPY" << endl;
771 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
772 status.out << "PRE_EMPHASIS" << endl;
774 status.out << "NO PRE_EMPHASIS" << endl;
777 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
778 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
781 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
782 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
784 status.out << " PERFORMER ";
785 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
786 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
788 status.out << "\"\"" << endl;
791 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
792 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
795 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
796 status.out << " ISRC \"";
797 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
798 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
799 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
800 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
803 status.out << " }" << endl << "}" << endl;
805 samples_to_cd_samples_string (buf, status.track_position);
806 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
808 samples_to_cd_samples_string (buf, status.track_duration);
809 status.out << buf << endl;
811 samples_to_cd_samples_string (buf, status.track_start_sample - status.track_position);
812 status.out << "START" << buf << endl;
815 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
819 samples_to_chapter_marks_string(buf, status.track_start_sample);
820 status.out << buf << " " << status.marker->name() << endl;
824 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
828 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
830 samples_to_cd_samples_string (buf, status.index_position);
831 status.out << buf << endl;
837 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
841 samples_to_cd_samples_string (buf, status.index_position - status.track_position);
842 status.out << "INDEX" << buf << endl;
846 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
851 ExportHandler::samples_to_cd_samples_string (char* buf, samplepos_t when)
853 samplecnt_t remainder;
854 samplecnt_t fr = session.nominal_sample_rate();
855 int mins, secs, samples;
857 mins = when / (60 * fr);
858 remainder = when - (mins * 60 * fr);
859 secs = remainder / fr;
860 remainder -= secs * fr;
861 samples = remainder / (fr / 75);
862 sprintf (buf, " %02d:%02d:%02d", mins, secs, samples);
866 ExportHandler::samples_to_chapter_marks_string (char* buf, samplepos_t when)
868 samplecnt_t remainder;
869 samplecnt_t fr = session.nominal_sample_rate();
870 int hours, mins, secs, msecs;
872 hours = when / (3600 * fr);
873 remainder = when - (hours * 3600 * fr);
874 mins = remainder / (60 * fr);
875 remainder -= mins * 60 * fr;
876 secs = remainder / fr;
877 remainder -= secs * fr;
878 msecs = (remainder * 1000) / fr;
879 sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
883 ExportHandler::toc_escape_cdtext (const std::string& txt)
885 Glib::ustring check (txt);
887 std::string latin1_txt;
891 latin1_txt = Glib::convert_with_fallback (txt, "ISO-8859-1", "UTF-8", "_");
892 } catch (Glib::ConvertError& err) {
893 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
898 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
902 } else if ((*c) == '\\') {
904 } else if (isprint (*c)) {
907 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
918 ExportHandler::toc_escape_filename (const std::string& txt)
924 // We iterate byte-wise not character-wise over a UTF-8 string here,
925 // because we only want to translate backslashes and double quotes
926 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
930 } else if (*c == '\\') {
943 ExportHandler::cue_escape_cdtext (const std::string& txt)
945 std::string latin1_txt;
949 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
950 } catch (Glib::ConvertError& err) {
951 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
954 // does not do much mor than UTF-8 to Latin1 translation yet, but
955 // that may have to change if cue parsers in burning programs change
956 out = '"' + latin1_txt + '"';
961 } // namespace ARDOUR