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 /* update position */
247 framecnt_t frames_to_read = 0;
248 framepos_t const end = current_timespan->get_end();
250 bool const last_cycle = (process_position + frames >= end);
253 frames_to_read = end - process_position;
254 export_status->stop = true;
256 frames_to_read = frames;
259 process_position += frames_to_read;
260 export_status->processed_frames += frames_to_read;
261 export_status->processed_frames_current_timespan += frames_to_read;
263 /* Do actual processing */
264 int ret = graph_builder->process (frames_to_read, last_cycle);
266 /* Start normalizing if necessary */
268 normalizing = graph_builder->will_normalize();
270 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
271 export_status->current_normalize_cycle = 0;
282 ExportHandler::process_normalize ()
284 if (graph_builder->process_normalize ()) {
286 export_status->active_job = ExportStatus::Exporting;
288 export_status->active_job = ExportStatus::Normalizing;
291 export_status->current_normalize_cycle++;
297 ExportHandler::command_output(std::string output, size_t size)
299 std::cerr << "command: " << size << ", " << output << std::endl;
300 info << output << endmsg;
304 ExportHandler::finish_timespan ()
306 graph_builder->get_analysis_results (export_status->result_map);
308 while (config_map.begin() != timespan_bounds.second) {
310 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
311 std::string filename = config_map.begin()->second.filename->get_path(fmt);
312 if (fmt->with_cue()) {
313 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
316 if (fmt->with_toc()) {
317 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
320 if (fmt->with_mp4chaps()) {
321 export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
324 /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
325 * The process cannot access the file because it is being used.
326 * ditto for post-export and upload.
328 graph_builder->reset ();
331 /* TODO: check Umlauts and encoding in filename.
332 * TagLib eventually calls CreateFileA(),
334 export_status->active_job = ExportStatus::Tagging;
335 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
338 if (!fmt->command().empty()) {
340 #if 0 // would be nicer with C++11 initialiser...
341 std::map<char, std::string> subs {
343 { 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR },
344 { 'b', PBD::basename_nosuffix(filename) },
348 export_status->active_job = ExportStatus::Command;
349 PBD::ScopedConnection command_connection;
350 std::map<char, std::string> subs;
351 subs.insert (std::pair<char, std::string> ('f', filename));
352 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
353 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
354 subs.insert (std::pair<char, std::string> ('s', session.path ()));
355 subs.insert (std::pair<char, std::string> ('n', session.name ()));
357 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
358 info << "Post-export command line : {" << se->to_s () << "}" << endmsg;
359 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
360 int ret = se->start (2);
362 // successfully started
363 while (se->is_running ()) {
364 // wait for system exec to terminate
368 error << "Post-export command FAILED with Error: " << ret << endmsg;
373 if (fmt->soundcloud_upload()) {
374 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
375 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
376 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
377 "uploading %1 - username=%2, password=%3, token=%4",
378 filename, soundcloud_username, soundcloud_password, token) );
379 std::string path = soundcloud_uploader->Upload (
381 PBD::basename_nosuffix(filename), // title
383 soundcloud_make_public,
384 soundcloud_downloadable,
387 if (path.length() != 0) {
388 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
389 if (soundcloud_open_page) {
390 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
391 open_uri(path.c_str()); // open the soundcloud website to the new file
394 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
396 delete soundcloud_uploader;
398 config_map.erase (config_map.begin());
404 /*** CD Marker stuff ***/
406 struct LocationSortByStart {
407 bool operator() (Location *a, Location *b) {
408 return a->start() < b->start();
413 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
414 std::string filename, CDMarkerFormat format)
416 string filepath = get_cd_marker_filename(filename, format);
419 void (ExportHandler::*header_func) (CDMarkerStatus &);
420 void (ExportHandler::*track_func) (CDMarkerStatus &);
421 void (ExportHandler::*index_func) (CDMarkerStatus &);
425 header_func = &ExportHandler::write_toc_header;
426 track_func = &ExportHandler::write_track_info_toc;
427 index_func = &ExportHandler::write_index_info_toc;
430 header_func = &ExportHandler::write_cue_header;
431 track_func = &ExportHandler::write_track_info_cue;
432 index_func = &ExportHandler::write_index_info_cue;
435 header_func = &ExportHandler::write_mp4ch_header;
436 track_func = &ExportHandler::write_track_info_mp4ch;
437 index_func = &ExportHandler::write_index_info_mp4ch;
443 CDMarkerStatus status (filepath, timespan, file_format, filename);
445 (this->*header_func) (status);
447 /* Get locations and sort */
449 Locations::LocationList const & locations (session.locations()->list());
450 Locations::LocationList::const_iterator i;
451 Locations::LocationList temp;
453 for (i = locations.begin(); i != locations.end(); ++i) {
454 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
460 // TODO One index marker for whole thing
464 LocationSortByStart cmp;
466 Locations::LocationList::const_iterator nexti;
468 /* Start actual marker stuff */
470 framepos_t last_end_time = timespan->get_start();
471 status.track_position = 0;
473 for (i = temp.begin(); i != temp.end(); ++i) {
477 if ((*i)->start() < last_end_time) {
478 if ((*i)->is_mark()) {
479 /* Index within track */
481 status.index_position = (*i)->start() - timespan->get_start();
482 (this->*index_func) (status);
488 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
490 status.track_position = last_end_time - timespan->get_start();
491 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
492 status.track_duration = 0;
494 if ((*i)->is_mark()) {
495 // a mark track location needs to look ahead to the next marker's start to determine length
499 if (nexti != temp.end()) {
500 status.track_duration = (*nexti)->start() - last_end_time;
502 last_end_time = (*nexti)->start();
504 // this was the last marker, use timespan end
505 status.track_duration = timespan->get_end() - last_end_time;
507 last_end_time = timespan->get_end();
511 status.track_duration = (*i)->end() - last_end_time;
513 last_end_time = (*i)->end();
516 (this->*track_func) (status);
519 } catch (std::exception& e) {
520 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
521 ::g_unlink (filepath.c_str());
522 } catch (Glib::Exception& e) {
523 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
524 ::g_unlink (filepath.c_str());
529 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
531 /* do not strip file suffix because there may be more than one format,
532 and we do not want the CD marker file from one format to overwrite
533 another (e.g. foo.wav.cue > foo.aiff.cue)
538 return filename + ".toc";
540 return filename + ".cue";
543 unsigned lastdot = filename.find_last_of('.');
544 return filename.substr(0,lastdot) + ".chapters.txt";
547 return filename + ".marker"; // Should not be reached when actually creating a file
552 ExportHandler::write_cue_header (CDMarkerStatus & status)
554 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
557 string barcode = SessionMetadata::Metadata()->barcode();
558 string album_artist = SessionMetadata::Metadata()->album_artist();
559 string album_title = SessionMetadata::Metadata()->album();
561 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
564 status.out << "CATALOG " << barcode << endl;
566 if (album_artist != "")
567 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
569 if (album_title != "")
572 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
574 /* The original cue sheet spec mentions five file types
576 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
577 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
580 We try to use these file types whenever appropriate and
581 default to our own names otherwise.
583 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
584 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
585 status.out << "WAVE";
586 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
587 status.format->sample_format() == ExportFormatBase::SF_16 &&
588 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
589 // Format is RAW 16bit 44.1kHz
590 if (status.format->endianness() == ExportFormatBase::E_Little) {
591 status.out << "BINARY";
593 status.out << "MOTOROLA";
596 // no special case for AIFF format it's name is already "AIFF"
597 status.out << status.format->format_name();
603 ExportHandler::write_toc_header (CDMarkerStatus & status)
605 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
608 string barcode = SessionMetadata::Metadata()->barcode();
609 string album_artist = SessionMetadata::Metadata()->album_artist();
610 string album_title = SessionMetadata::Metadata()->album();
613 status.out << "CATALOG \"" << barcode << "\"" << endl;
615 if (album_title != "")
618 status.out << "CD_DA" << endl;
619 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
620 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
621 status.out << " PERFORMER " << toc_escape_cdtext (album_artist) << endl;
622 status.out << " }" << endl << "}" << endl;
626 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
628 status.out << "00:00:00.000 Intro" << endl;
632 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
636 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
637 status.out << buf << endl;
639 status.out << " FLAGS" ;
640 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
641 status.out << " SCMS ";
643 status.out << " DCP ";
646 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
647 status.out << " PRE";
651 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
652 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
655 if (status.marker->name() != "") {
656 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
659 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
660 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
663 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
664 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
667 if (status.track_position != status.track_start_frame) {
668 frames_to_cd_frames_string (buf, status.track_position);
669 status.out << " INDEX 00" << buf << endl;
672 frames_to_cd_frames_string (buf, status.track_start_frame);
673 status.out << " INDEX 01" << buf << endl;
675 status.index_number = 2;
676 status.track_number++;
680 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
684 status.out << endl << "TRACK AUDIO" << endl;
686 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
689 status.out << "COPY" << endl;
691 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
692 status.out << "PRE_EMPHASIS" << endl;
694 status.out << "NO PRE_EMPHASIS" << endl;
697 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
698 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
701 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
702 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
704 status.out << " PERFORMER ";
705 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
706 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
708 status.out << "\"\"" << endl;
711 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
712 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
715 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
716 status.out << " ISRC \"";
717 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
718 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
719 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
720 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
723 status.out << " }" << endl << "}" << endl;
725 frames_to_cd_frames_string (buf, status.track_position);
726 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
728 frames_to_cd_frames_string (buf, status.track_duration);
729 status.out << buf << endl;
731 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
732 status.out << "START" << buf << endl;
735 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
739 frames_to_chapter_marks_string(buf, status.track_start_frame);
740 status.out << buf << " " << status.marker->name() << endl;
744 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
748 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
750 frames_to_cd_frames_string (buf, status.index_position);
751 status.out << buf << endl;
757 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
761 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
762 status.out << "INDEX" << buf << endl;
766 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
771 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
773 framecnt_t remainder;
774 framecnt_t fr = session.nominal_frame_rate();
775 int mins, secs, frames;
777 mins = when / (60 * fr);
778 remainder = when - (mins * 60 * fr);
779 secs = remainder / fr;
780 remainder -= secs * fr;
781 frames = remainder / (fr / 75);
782 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
786 ExportHandler::frames_to_chapter_marks_string (char* buf, framepos_t when)
788 framecnt_t remainder;
789 framecnt_t fr = session.nominal_frame_rate();
790 int hours, mins, secs, msecs;
792 hours = when / (3600 * fr);
793 remainder = when - (hours * 3600 * fr);
794 mins = remainder / (60 * fr);
795 remainder -= mins * 60 * fr;
796 secs = remainder / fr;
797 remainder -= secs * fr;
798 msecs = (remainder * 1000) / fr;
799 sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
803 ExportHandler::toc_escape_cdtext (const std::string& txt)
805 Glib::ustring check (txt);
807 std::string latin1_txt;
811 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
812 } catch (Glib::ConvertError& err) {
813 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
818 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
822 } else if ((*c) == '\\') {
824 } else if (isprint (*c)) {
827 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
838 ExportHandler::toc_escape_filename (const std::string& txt)
844 // We iterate byte-wise not character-wise over a UTF-8 string here,
845 // because we only want to translate backslashes and double quotes
846 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
850 } else if (*c == '\\') {
863 ExportHandler::cue_escape_cdtext (const std::string& txt)
865 std::string latin1_txt;
869 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
870 } catch (Glib::ConvertError& err) {
871 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
874 // does not do much mor than UTF-8 to Latin1 translation yet, but
875 // that may have to change if cue parsers in burning programs change
876 out = '"' + latin1_txt + '"';
881 } // namespace ARDOUR