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 <glib/gstdio.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();
157 ExportHandler::start_timespan ()
159 export_status->timespan++;
161 if (config_map.empty()) {
162 // freewheeling has to be stopped from outside the process cycle
163 export_status->running = false;
167 /* finish_timespan pops the config_map entry that has been done, so
168 this is the timespan to do this time
170 current_timespan = config_map.begin()->first;
172 export_status->total_frames_current_timespan = current_timespan->get_length();
173 export_status->timespan_name = current_timespan->name();
174 export_status->processed_frames_current_timespan = 0;
176 /* Register file configurations to graph builder */
178 /* Here's the config_map entries that use this timespan */
179 timespan_bounds = config_map.equal_range (current_timespan);
180 graph_builder->reset ();
181 graph_builder->set_current_timespan (current_timespan);
182 handle_duplicate_format_extensions();
183 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
184 // Filenames can be shared across timespans
185 FileSpec & spec = it->second;
186 spec.filename->set_timespan (it->first);
187 graph_builder->add_config (spec);
193 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
194 process_position = current_timespan->get_start();
195 session.start_audio_export (process_position);
199 ExportHandler::handle_duplicate_format_extensions()
201 typedef std::map<std::string, int> ExtCountMap;
204 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
205 counts[it->second.format->extension()]++;
208 bool duplicates_found = false;
209 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
210 if (it->second > 1) { duplicates_found = true; }
213 // Set this always, as the filenames are shared...
214 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
215 it->second.filename->include_format_name = duplicates_found;
220 ExportHandler::process (framecnt_t frames)
222 if (!export_status->running) {
224 } else if (normalizing) {
225 return process_normalize ();
227 return process_timespan (frames);
232 ExportHandler::process_timespan (framecnt_t frames)
234 /* update position */
236 framecnt_t frames_to_read = 0;
237 framepos_t const end = current_timespan->get_end();
239 bool const last_cycle = (process_position + frames >= end);
242 frames_to_read = end - process_position;
243 export_status->stop = true;
245 frames_to_read = frames;
248 process_position += frames_to_read;
249 export_status->processed_frames += frames_to_read;
250 export_status->processed_frames_current_timespan += frames_to_read;
252 /* Do actual processing */
253 int ret = graph_builder->process (frames_to_read, last_cycle);
255 /* Start normalizing if necessary */
257 normalizing = graph_builder->will_normalize();
259 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
260 export_status->current_normalize_cycle = 0;
271 ExportHandler::process_normalize ()
273 if (graph_builder->process_normalize ()) {
275 export_status->normalizing = false;
277 export_status->normalizing = true;
280 export_status->current_normalize_cycle++;
286 ExportHandler::command_output(std::string output, size_t size)
288 std::cerr << "command: " << size << ", " << output << std::endl;
289 info << output << endmsg;
293 ExportHandler::finish_timespan ()
295 while (config_map.begin() != timespan_bounds.second) {
297 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
298 std::string filename = config_map.begin()->second.filename->get_path(fmt);
300 if (fmt->with_cue()) {
301 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
304 if (fmt->with_toc()) {
305 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
308 if (fmt->with_mp4chaps()) {
309 export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
313 /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
314 * The process cannot access the file because it is being used.
316 * TODO: check Umlauts and encoding in filename.
317 * TagLib eventually calls CreateFileA(),
319 graph_builder->reset ();
320 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
323 if (!fmt->command().empty()) {
325 #if 0 // would be nicer with C++11 initialiser...
326 std::map<char, std::string> subs {
328 { 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR },
329 { 'b', PBD::basename_nosuffix(filename) },
334 PBD::ScopedConnection command_connection;
335 std::map<char, std::string> subs;
336 subs.insert (std::pair<char, std::string> ('f', filename));
337 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
338 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
339 subs.insert (std::pair<char, std::string> ('s', session.path ()));
340 subs.insert (std::pair<char, std::string> ('n', session.name ()));
342 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
343 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
344 if (se->start (2) == 0) {
345 // successfully started
346 while (se->is_running ()) {
347 // wait for system exec to terminate
351 error << "post-export hook failed! " << fmt->command() << endmsg;
356 if (fmt->soundcloud_upload()) {
357 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
358 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
359 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
360 "uploading %1 - username=%2, password=%3, token=%4",
361 filename, soundcloud_username, soundcloud_password, token) );
362 std::string path = soundcloud_uploader->Upload (
364 PBD::basename_nosuffix(filename), // title
366 soundcloud_make_public,
367 soundcloud_downloadable,
370 if (path.length() != 0) {
371 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
372 if (soundcloud_open_page) {
373 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
374 open_uri(path.c_str()); // open the soundcloud website to the new file
377 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
379 delete soundcloud_uploader;
381 config_map.erase (config_map.begin());
387 /*** CD Marker stuff ***/
389 struct LocationSortByStart {
390 bool operator() (Location *a, Location *b) {
391 return a->start() < b->start();
396 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
397 std::string filename, CDMarkerFormat format)
399 string filepath = get_cd_marker_filename(filename, format);
402 void (ExportHandler::*header_func) (CDMarkerStatus &);
403 void (ExportHandler::*track_func) (CDMarkerStatus &);
404 void (ExportHandler::*index_func) (CDMarkerStatus &);
408 header_func = &ExportHandler::write_toc_header;
409 track_func = &ExportHandler::write_track_info_toc;
410 index_func = &ExportHandler::write_index_info_toc;
413 header_func = &ExportHandler::write_cue_header;
414 track_func = &ExportHandler::write_track_info_cue;
415 index_func = &ExportHandler::write_index_info_cue;
418 header_func = &ExportHandler::write_mp4ch_header;
419 track_func = &ExportHandler::write_track_info_mp4ch;
420 index_func = &ExportHandler::write_index_info_mp4ch;
426 CDMarkerStatus status (filepath, timespan, file_format, filename);
429 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
433 (this->*header_func) (status);
435 /* Get locations and sort */
437 Locations::LocationList const & locations (session.locations()->list());
438 Locations::LocationList::const_iterator i;
439 Locations::LocationList temp;
441 for (i = locations.begin(); i != locations.end(); ++i) {
442 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
448 // TODO One index marker for whole thing
452 LocationSortByStart cmp;
454 Locations::LocationList::const_iterator nexti;
456 /* Start actual marker stuff */
458 framepos_t last_end_time = timespan->get_start();
459 status.track_position = 0;
461 for (i = temp.begin(); i != temp.end(); ++i) {
465 if ((*i)->start() < last_end_time) {
466 if ((*i)->is_mark()) {
467 /* Index within track */
469 status.index_position = (*i)->start() - timespan->get_start();
470 (this->*index_func) (status);
476 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
478 status.track_position = last_end_time - timespan->get_start();
479 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
480 status.track_duration = 0;
482 if ((*i)->is_mark()) {
483 // a mark track location needs to look ahead to the next marker's start to determine length
487 if (nexti != temp.end()) {
488 status.track_duration = (*nexti)->start() - last_end_time;
490 last_end_time = (*nexti)->start();
492 // this was the last marker, use timespan end
493 status.track_duration = timespan->get_end() - last_end_time;
495 last_end_time = timespan->get_end();
499 status.track_duration = (*i)->end() - last_end_time;
501 last_end_time = (*i)->end();
504 (this->*track_func) (status);
507 } catch (std::exception& e) {
508 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
509 ::g_unlink (filepath.c_str());
510 } catch (Glib::Exception& e) {
511 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
512 ::g_unlink (filepath.c_str());
517 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
519 /* do not strip file suffix because there may be more than one format,
520 and we do not want the CD marker file from one format to overwrite
521 another (e.g. foo.wav.cue > foo.aiff.cue)
526 return filename + ".toc";
528 return filename + ".cue";
531 unsigned lastdot = filename.find_last_of('.');
532 return filename.substr(0,lastdot) + ".chapters.txt";
535 return filename + ".marker"; // Should not be reached when actually creating a file
540 ExportHandler::write_cue_header (CDMarkerStatus & status)
542 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
545 string barcode = SessionMetadata::Metadata()->barcode();
546 string album_artist = SessionMetadata::Metadata()->album_artist();
547 string album_title = SessionMetadata::Metadata()->album();
549 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
552 status.out << "CATALOG " << barcode << endl;
554 if (album_artist != "")
555 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
557 if (album_title != "")
560 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
562 /* The original cue sheet spec mentions five file types
564 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
565 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
568 We try to use these file types whenever appropriate and
569 default to our own names otherwise.
571 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
572 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
573 status.out << "WAVE";
574 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
575 status.format->sample_format() == ExportFormatBase::SF_16 &&
576 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
577 // Format is RAW 16bit 44.1kHz
578 if (status.format->endianness() == ExportFormatBase::E_Little) {
579 status.out << "BINARY";
581 status.out << "MOTOROLA";
584 // no special case for AIFF format it's name is already "AIFF"
585 status.out << status.format->format_name();
591 ExportHandler::write_toc_header (CDMarkerStatus & status)
593 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
596 string barcode = SessionMetadata::Metadata()->barcode();
597 string album_artist = SessionMetadata::Metadata()->album_artist();
598 string album_title = SessionMetadata::Metadata()->album();
601 status.out << "CATALOG \"" << barcode << "\"" << endl;
603 if (album_title != "")
606 status.out << "CD_DA" << endl;
607 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
608 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
609 status.out << " PERFORMER " << toc_escape_cdtext (album_artist) << endl;
610 status.out << " }" << endl << "}" << endl;
614 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
616 status.out << "00:00:00.000 Intro" << endl;
620 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
624 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
625 status.out << buf << endl;
627 status.out << " FLAGS" ;
628 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
629 status.out << " SCMS ";
631 status.out << " DCP ";
634 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
635 status.out << " PRE";
639 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
640 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
643 if (status.marker->name() != "") {
644 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
647 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
648 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
651 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
652 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
655 if (status.track_position != status.track_start_frame) {
656 frames_to_cd_frames_string (buf, status.track_position);
657 status.out << " INDEX 00" << buf << endl;
660 frames_to_cd_frames_string (buf, status.track_start_frame);
661 status.out << " INDEX 01" << buf << endl;
663 status.index_number = 2;
664 status.track_number++;
668 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
672 status.out << endl << "TRACK AUDIO" << endl;
674 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
677 status.out << "COPY" << endl;
679 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
680 status.out << "PRE_EMPHASIS" << endl;
682 status.out << "NO PRE_EMPHASIS" << endl;
685 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
686 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
689 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
690 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
692 status.out << " PERFORMER ";
693 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
694 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
696 status.out << "\"\"" << endl;
699 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
700 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
703 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
704 status.out << " ISRC \"";
705 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
706 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
707 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
708 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
711 status.out << " }" << endl << "}" << endl;
713 frames_to_cd_frames_string (buf, status.track_position);
714 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
716 frames_to_cd_frames_string (buf, status.track_duration);
717 status.out << buf << endl;
719 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
720 status.out << "START" << buf << endl;
723 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
727 frames_to_chapter_marks_string(buf, status.track_start_frame);
728 status.out << buf << " " << status.marker->name() << endl;
732 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
736 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
738 frames_to_cd_frames_string (buf, status.index_position);
739 status.out << buf << endl;
745 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
749 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
750 status.out << "INDEX" << buf << endl;
754 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
759 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
761 framecnt_t remainder;
762 framecnt_t fr = session.nominal_frame_rate();
763 int mins, secs, frames;
765 mins = when / (60 * fr);
766 remainder = when - (mins * 60 * fr);
767 secs = remainder / fr;
768 remainder -= secs * fr;
769 frames = remainder / (fr / 75);
770 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
774 ExportHandler::frames_to_chapter_marks_string (char* buf, framepos_t when)
776 framecnt_t remainder;
777 framecnt_t fr = session.nominal_frame_rate();
778 int hours, mins, secs, msecs;
780 hours = when / (3600 * fr);
781 remainder = when - (hours * 3600 * fr);
782 mins = remainder / (60 * fr);
783 remainder -= mins * 60 * fr;
784 secs = remainder / fr;
785 remainder -= secs * fr;
786 msecs = (remainder * 1000) / fr;
787 sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
791 ExportHandler::toc_escape_cdtext (const std::string& txt)
793 Glib::ustring check (txt);
795 std::string latin1_txt;
799 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
800 } catch (Glib::ConvertError& err) {
801 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
806 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
810 } else if ((*c) == '\\') {
812 } else if (isprint (*c)) {
815 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
826 ExportHandler::toc_escape_filename (const std::string& txt)
832 // We iterate byte-wise not character-wise over a UTF-8 string here,
833 // because we only want to translate backslashes and double quotes
834 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
838 } else if (*c == '\\') {
851 ExportHandler::cue_escape_cdtext (const std::string& txt)
853 std::string latin1_txt;
857 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
858 } catch (Glib::ConvertError& err) {
859 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
862 // does not do much mor than UTF-8 to Latin1 translation yet, but
863 // that may have to change if cue parsers in burning programs change
864 out = '"' + latin1_txt + '"';
869 } // namespace ARDOUR