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();
153 Glib::Threads::Mutex::Lock l (export_status->lock());
158 ExportHandler::start_timespan ()
160 export_status->timespan++;
162 if (config_map.empty()) {
163 // freewheeling has to be stopped from outside the process cycle
164 export_status->set_running (false);
168 /* finish_timespan pops the config_map entry that has been done, so
169 this is the timespan to do this time
171 current_timespan = config_map.begin()->first;
173 export_status->total_frames_current_timespan = current_timespan->get_length();
174 export_status->timespan_name = current_timespan->name();
175 export_status->processed_frames_current_timespan = 0;
177 /* Register file configurations to graph builder */
179 /* Here's the config_map entries that use this timespan */
180 timespan_bounds = config_map.equal_range (current_timespan);
181 graph_builder->reset ();
182 graph_builder->set_current_timespan (current_timespan);
183 handle_duplicate_format_extensions();
184 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
185 // Filenames can be shared across timespans
186 FileSpec & spec = it->second;
187 spec.filename->set_timespan (it->first);
188 graph_builder->add_config (spec);
194 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
195 process_position = current_timespan->get_start();
196 session.start_audio_export (process_position);
200 ExportHandler::handle_duplicate_format_extensions()
202 typedef std::map<std::string, int> ExtCountMap;
205 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
206 counts[it->second.format->extension()]++;
209 bool duplicates_found = false;
210 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
211 if (it->second > 1) { duplicates_found = true; }
214 // Set this always, as the filenames are shared...
215 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
216 it->second.filename->include_format_name = duplicates_found;
221 ExportHandler::process (framecnt_t frames)
223 if (!export_status->running ()) {
225 } else if (normalizing) {
226 Glib::Threads::Mutex::Lock l (export_status->lock());
227 return process_normalize ();
229 Glib::Threads::Mutex::Lock l (export_status->lock());
230 return process_timespan (frames);
235 ExportHandler::process_timespan (framecnt_t frames)
237 /* update position */
239 framecnt_t frames_to_read = 0;
240 framepos_t const end = current_timespan->get_end();
242 bool const last_cycle = (process_position + frames >= end);
245 frames_to_read = end - process_position;
246 export_status->stop = true;
248 frames_to_read = frames;
251 process_position += frames_to_read;
252 export_status->processed_frames += frames_to_read;
253 export_status->processed_frames_current_timespan += frames_to_read;
255 /* Do actual processing */
256 int ret = graph_builder->process (frames_to_read, last_cycle);
258 /* Start normalizing if necessary */
260 normalizing = graph_builder->will_normalize();
262 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
263 export_status->current_normalize_cycle = 0;
274 ExportHandler::process_normalize ()
276 if (graph_builder->process_normalize ()) {
278 export_status->active_job = ExportStatus::Exporting;
280 export_status->active_job = ExportStatus::Normalizing;
283 export_status->current_normalize_cycle++;
289 ExportHandler::command_output(std::string output, size_t size)
291 std::cerr << "command: " << size << ", " << output << std::endl;
292 info << output << endmsg;
296 ExportHandler::finish_timespan ()
298 while (config_map.begin() != timespan_bounds.second) {
300 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
301 std::string filename = config_map.begin()->second.filename->get_path(fmt);
303 if (fmt->with_cue()) {
304 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
307 if (fmt->with_toc()) {
308 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
311 if (fmt->with_mp4chaps()) {
312 export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
316 /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
317 * The process cannot access the file because it is being used.
319 * TODO: check Umlauts and encoding in filename.
320 * TagLib eventually calls CreateFileA(),
322 export_status->active_job = ExportStatus::Tagging;
323 graph_builder->reset ();
324 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
327 if (!fmt->command().empty()) {
329 #if 0 // would be nicer with C++11 initialiser...
330 std::map<char, std::string> subs {
332 { 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR },
333 { 'b', PBD::basename_nosuffix(filename) },
337 export_status->active_job = ExportStatus::Command;
338 PBD::ScopedConnection command_connection;
339 std::map<char, std::string> subs;
340 subs.insert (std::pair<char, std::string> ('f', filename));
341 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
342 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
343 subs.insert (std::pair<char, std::string> ('s', session.path ()));
344 subs.insert (std::pair<char, std::string> ('n', session.name ()));
346 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
347 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
348 if (se->start (2) == 0) {
349 // successfully started
350 while (se->is_running ()) {
351 // wait for system exec to terminate
355 error << "post-export hook failed! " << fmt->command() << endmsg;
360 if (fmt->soundcloud_upload()) {
361 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
362 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
363 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
364 "uploading %1 - username=%2, password=%3, token=%4",
365 filename, soundcloud_username, soundcloud_password, token) );
366 std::string path = soundcloud_uploader->Upload (
368 PBD::basename_nosuffix(filename), // title
370 soundcloud_make_public,
371 soundcloud_downloadable,
374 if (path.length() != 0) {
375 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
376 if (soundcloud_open_page) {
377 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
378 open_uri(path.c_str()); // open the soundcloud website to the new file
381 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
383 delete soundcloud_uploader;
385 config_map.erase (config_map.begin());
391 /*** CD Marker stuff ***/
393 struct LocationSortByStart {
394 bool operator() (Location *a, Location *b) {
395 return a->start() < b->start();
400 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
401 std::string filename, CDMarkerFormat format)
403 string filepath = get_cd_marker_filename(filename, format);
406 void (ExportHandler::*header_func) (CDMarkerStatus &);
407 void (ExportHandler::*track_func) (CDMarkerStatus &);
408 void (ExportHandler::*index_func) (CDMarkerStatus &);
412 header_func = &ExportHandler::write_toc_header;
413 track_func = &ExportHandler::write_track_info_toc;
414 index_func = &ExportHandler::write_index_info_toc;
417 header_func = &ExportHandler::write_cue_header;
418 track_func = &ExportHandler::write_track_info_cue;
419 index_func = &ExportHandler::write_index_info_cue;
422 header_func = &ExportHandler::write_mp4ch_header;
423 track_func = &ExportHandler::write_track_info_mp4ch;
424 index_func = &ExportHandler::write_index_info_mp4ch;
430 CDMarkerStatus status (filepath, timespan, file_format, filename);
432 (this->*header_func) (status);
434 /* Get locations and sort */
436 Locations::LocationList const & locations (session.locations()->list());
437 Locations::LocationList::const_iterator i;
438 Locations::LocationList temp;
440 for (i = locations.begin(); i != locations.end(); ++i) {
441 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
447 // TODO One index marker for whole thing
451 LocationSortByStart cmp;
453 Locations::LocationList::const_iterator nexti;
455 /* Start actual marker stuff */
457 framepos_t last_end_time = timespan->get_start();
458 status.track_position = 0;
460 for (i = temp.begin(); i != temp.end(); ++i) {
464 if ((*i)->start() < last_end_time) {
465 if ((*i)->is_mark()) {
466 /* Index within track */
468 status.index_position = (*i)->start() - timespan->get_start();
469 (this->*index_func) (status);
475 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
477 status.track_position = last_end_time - timespan->get_start();
478 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
479 status.track_duration = 0;
481 if ((*i)->is_mark()) {
482 // a mark track location needs to look ahead to the next marker's start to determine length
486 if (nexti != temp.end()) {
487 status.track_duration = (*nexti)->start() - last_end_time;
489 last_end_time = (*nexti)->start();
491 // this was the last marker, use timespan end
492 status.track_duration = timespan->get_end() - last_end_time;
494 last_end_time = timespan->get_end();
498 status.track_duration = (*i)->end() - last_end_time;
500 last_end_time = (*i)->end();
503 (this->*track_func) (status);
506 } catch (std::exception& e) {
507 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
508 ::g_unlink (filepath.c_str());
509 } catch (Glib::Exception& e) {
510 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
511 ::g_unlink (filepath.c_str());
516 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
518 /* do not strip file suffix because there may be more than one format,
519 and we do not want the CD marker file from one format to overwrite
520 another (e.g. foo.wav.cue > foo.aiff.cue)
525 return filename + ".toc";
527 return filename + ".cue";
530 unsigned lastdot = filename.find_last_of('.');
531 return filename.substr(0,lastdot) + ".chapters.txt";
534 return filename + ".marker"; // Should not be reached when actually creating a file
539 ExportHandler::write_cue_header (CDMarkerStatus & status)
541 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
544 string barcode = SessionMetadata::Metadata()->barcode();
545 string album_artist = SessionMetadata::Metadata()->album_artist();
546 string album_title = SessionMetadata::Metadata()->album();
548 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
551 status.out << "CATALOG " << barcode << endl;
553 if (album_artist != "")
554 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
556 if (album_title != "")
559 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
561 /* The original cue sheet spec mentions five file types
563 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
564 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
567 We try to use these file types whenever appropriate and
568 default to our own names otherwise.
570 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
571 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
572 status.out << "WAVE";
573 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
574 status.format->sample_format() == ExportFormatBase::SF_16 &&
575 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
576 // Format is RAW 16bit 44.1kHz
577 if (status.format->endianness() == ExportFormatBase::E_Little) {
578 status.out << "BINARY";
580 status.out << "MOTOROLA";
583 // no special case for AIFF format it's name is already "AIFF"
584 status.out << status.format->format_name();
590 ExportHandler::write_toc_header (CDMarkerStatus & status)
592 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
595 string barcode = SessionMetadata::Metadata()->barcode();
596 string album_artist = SessionMetadata::Metadata()->album_artist();
597 string album_title = SessionMetadata::Metadata()->album();
600 status.out << "CATALOG \"" << barcode << "\"" << endl;
602 if (album_title != "")
605 status.out << "CD_DA" << endl;
606 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
607 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
608 status.out << " PERFORMER " << toc_escape_cdtext (album_artist) << endl;
609 status.out << " }" << endl << "}" << endl;
613 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
615 status.out << "00:00:00.000 Intro" << endl;
619 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
623 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
624 status.out << buf << endl;
626 status.out << " FLAGS" ;
627 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
628 status.out << " SCMS ";
630 status.out << " DCP ";
633 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
634 status.out << " PRE";
638 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
639 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
642 if (status.marker->name() != "") {
643 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
646 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
647 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
650 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
651 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
654 if (status.track_position != status.track_start_frame) {
655 frames_to_cd_frames_string (buf, status.track_position);
656 status.out << " INDEX 00" << buf << endl;
659 frames_to_cd_frames_string (buf, status.track_start_frame);
660 status.out << " INDEX 01" << buf << endl;
662 status.index_number = 2;
663 status.track_number++;
667 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
671 status.out << endl << "TRACK AUDIO" << endl;
673 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
676 status.out << "COPY" << endl;
678 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
679 status.out << "PRE_EMPHASIS" << endl;
681 status.out << "NO PRE_EMPHASIS" << endl;
684 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
685 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
688 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
689 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
691 status.out << " PERFORMER ";
692 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
693 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
695 status.out << "\"\"" << endl;
698 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
699 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
702 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
703 status.out << " ISRC \"";
704 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
705 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
706 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
707 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
710 status.out << " }" << endl << "}" << endl;
712 frames_to_cd_frames_string (buf, status.track_position);
713 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
715 frames_to_cd_frames_string (buf, status.track_duration);
716 status.out << buf << endl;
718 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
719 status.out << "START" << buf << endl;
722 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
726 frames_to_chapter_marks_string(buf, status.track_start_frame);
727 status.out << buf << " " << status.marker->name() << endl;
731 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
735 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
737 frames_to_cd_frames_string (buf, status.index_position);
738 status.out << buf << endl;
744 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
748 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
749 status.out << "INDEX" << buf << endl;
753 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
758 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
760 framecnt_t remainder;
761 framecnt_t fr = session.nominal_frame_rate();
762 int mins, secs, frames;
764 mins = when / (60 * fr);
765 remainder = when - (mins * 60 * fr);
766 secs = remainder / fr;
767 remainder -= secs * fr;
768 frames = remainder / (fr / 75);
769 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
773 ExportHandler::frames_to_chapter_marks_string (char* buf, framepos_t when)
775 framecnt_t remainder;
776 framecnt_t fr = session.nominal_frame_rate();
777 int hours, mins, secs, msecs;
779 hours = when / (3600 * fr);
780 remainder = when - (hours * 3600 * fr);
781 mins = remainder / (60 * fr);
782 remainder -= mins * 60 * fr;
783 secs = remainder / fr;
784 remainder -= secs * fr;
785 msecs = (remainder * 1000) / fr;
786 sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
790 ExportHandler::toc_escape_cdtext (const std::string& txt)
792 Glib::ustring check (txt);
794 std::string latin1_txt;
798 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
799 } catch (Glib::ConvertError& err) {
800 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
805 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
809 } else if ((*c) == '\\') {
811 } else if (isprint (*c)) {
814 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
825 ExportHandler::toc_escape_filename (const std::string& txt)
831 // We iterate byte-wise not character-wise over a UTF-8 string here,
832 // because we only want to translate backslashes and double quotes
833 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
837 } else if (*c == '\\') {
850 ExportHandler::cue_escape_cdtext (const std::string& txt)
852 std::string latin1_txt;
856 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
857 } catch (Glib::ConvertError& err) {
858 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
861 // does not do much mor than UTF-8 to Latin1 translation yet, but
862 // that may have to change if cue parsers in burning programs change
863 out = '"' + latin1_txt + '"';
868 } // namespace ARDOUR