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 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
359 if (se->start (2) == 0) {
360 // successfully started
361 while (se->is_running ()) {
362 // wait for system exec to terminate
366 error << "post-export hook failed! " << fmt->command() << endmsg;
371 if (fmt->soundcloud_upload()) {
372 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
373 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
374 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
375 "uploading %1 - username=%2, password=%3, token=%4",
376 filename, soundcloud_username, soundcloud_password, token) );
377 std::string path = soundcloud_uploader->Upload (
379 PBD::basename_nosuffix(filename), // title
381 soundcloud_make_public,
382 soundcloud_downloadable,
385 if (path.length() != 0) {
386 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
387 if (soundcloud_open_page) {
388 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
389 open_uri(path.c_str()); // open the soundcloud website to the new file
392 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
394 delete soundcloud_uploader;
396 config_map.erase (config_map.begin());
402 /*** CD Marker stuff ***/
404 struct LocationSortByStart {
405 bool operator() (Location *a, Location *b) {
406 return a->start() < b->start();
411 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
412 std::string filename, CDMarkerFormat format)
414 string filepath = get_cd_marker_filename(filename, format);
417 void (ExportHandler::*header_func) (CDMarkerStatus &);
418 void (ExportHandler::*track_func) (CDMarkerStatus &);
419 void (ExportHandler::*index_func) (CDMarkerStatus &);
423 header_func = &ExportHandler::write_toc_header;
424 track_func = &ExportHandler::write_track_info_toc;
425 index_func = &ExportHandler::write_index_info_toc;
428 header_func = &ExportHandler::write_cue_header;
429 track_func = &ExportHandler::write_track_info_cue;
430 index_func = &ExportHandler::write_index_info_cue;
433 header_func = &ExportHandler::write_mp4ch_header;
434 track_func = &ExportHandler::write_track_info_mp4ch;
435 index_func = &ExportHandler::write_index_info_mp4ch;
441 CDMarkerStatus status (filepath, timespan, file_format, filename);
443 (this->*header_func) (status);
445 /* Get locations and sort */
447 Locations::LocationList const & locations (session.locations()->list());
448 Locations::LocationList::const_iterator i;
449 Locations::LocationList temp;
451 for (i = locations.begin(); i != locations.end(); ++i) {
452 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
458 // TODO One index marker for whole thing
462 LocationSortByStart cmp;
464 Locations::LocationList::const_iterator nexti;
466 /* Start actual marker stuff */
468 framepos_t last_end_time = timespan->get_start();
469 status.track_position = 0;
471 for (i = temp.begin(); i != temp.end(); ++i) {
475 if ((*i)->start() < last_end_time) {
476 if ((*i)->is_mark()) {
477 /* Index within track */
479 status.index_position = (*i)->start() - timespan->get_start();
480 (this->*index_func) (status);
486 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
488 status.track_position = last_end_time - timespan->get_start();
489 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
490 status.track_duration = 0;
492 if ((*i)->is_mark()) {
493 // a mark track location needs to look ahead to the next marker's start to determine length
497 if (nexti != temp.end()) {
498 status.track_duration = (*nexti)->start() - last_end_time;
500 last_end_time = (*nexti)->start();
502 // this was the last marker, use timespan end
503 status.track_duration = timespan->get_end() - last_end_time;
505 last_end_time = timespan->get_end();
509 status.track_duration = (*i)->end() - last_end_time;
511 last_end_time = (*i)->end();
514 (this->*track_func) (status);
517 } catch (std::exception& e) {
518 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
519 ::g_unlink (filepath.c_str());
520 } catch (Glib::Exception& e) {
521 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
522 ::g_unlink (filepath.c_str());
527 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
529 /* do not strip file suffix because there may be more than one format,
530 and we do not want the CD marker file from one format to overwrite
531 another (e.g. foo.wav.cue > foo.aiff.cue)
536 return filename + ".toc";
538 return filename + ".cue";
541 unsigned lastdot = filename.find_last_of('.');
542 return filename.substr(0,lastdot) + ".chapters.txt";
545 return filename + ".marker"; // Should not be reached when actually creating a file
550 ExportHandler::write_cue_header (CDMarkerStatus & status)
552 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
555 string barcode = SessionMetadata::Metadata()->barcode();
556 string album_artist = SessionMetadata::Metadata()->album_artist();
557 string album_title = SessionMetadata::Metadata()->album();
559 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
562 status.out << "CATALOG " << barcode << endl;
564 if (album_artist != "")
565 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
567 if (album_title != "")
570 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
572 /* The original cue sheet spec mentions five file types
574 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
575 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
578 We try to use these file types whenever appropriate and
579 default to our own names otherwise.
581 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
582 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
583 status.out << "WAVE";
584 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
585 status.format->sample_format() == ExportFormatBase::SF_16 &&
586 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
587 // Format is RAW 16bit 44.1kHz
588 if (status.format->endianness() == ExportFormatBase::E_Little) {
589 status.out << "BINARY";
591 status.out << "MOTOROLA";
594 // no special case for AIFF format it's name is already "AIFF"
595 status.out << status.format->format_name();
601 ExportHandler::write_toc_header (CDMarkerStatus & status)
603 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
606 string barcode = SessionMetadata::Metadata()->barcode();
607 string album_artist = SessionMetadata::Metadata()->album_artist();
608 string album_title = SessionMetadata::Metadata()->album();
611 status.out << "CATALOG \"" << barcode << "\"" << endl;
613 if (album_title != "")
616 status.out << "CD_DA" << endl;
617 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
618 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
619 status.out << " PERFORMER " << toc_escape_cdtext (album_artist) << endl;
620 status.out << " }" << endl << "}" << endl;
624 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
626 status.out << "00:00:00.000 Intro" << endl;
630 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
634 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
635 status.out << buf << endl;
637 status.out << " FLAGS" ;
638 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
639 status.out << " SCMS ";
641 status.out << " DCP ";
644 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
645 status.out << " PRE";
649 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
650 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
653 if (status.marker->name() != "") {
654 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
657 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
658 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
661 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
662 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
665 if (status.track_position != status.track_start_frame) {
666 frames_to_cd_frames_string (buf, status.track_position);
667 status.out << " INDEX 00" << buf << endl;
670 frames_to_cd_frames_string (buf, status.track_start_frame);
671 status.out << " INDEX 01" << buf << endl;
673 status.index_number = 2;
674 status.track_number++;
678 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
682 status.out << endl << "TRACK AUDIO" << endl;
684 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
687 status.out << "COPY" << endl;
689 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
690 status.out << "PRE_EMPHASIS" << endl;
692 status.out << "NO PRE_EMPHASIS" << endl;
695 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
696 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
699 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
700 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
702 status.out << " PERFORMER ";
703 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
704 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
706 status.out << "\"\"" << endl;
709 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
710 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
713 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
714 status.out << " ISRC \"";
715 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
716 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
717 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
718 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
721 status.out << " }" << endl << "}" << endl;
723 frames_to_cd_frames_string (buf, status.track_position);
724 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
726 frames_to_cd_frames_string (buf, status.track_duration);
727 status.out << buf << endl;
729 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
730 status.out << "START" << buf << endl;
733 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
737 frames_to_chapter_marks_string(buf, status.track_start_frame);
738 status.out << buf << " " << status.marker->name() << endl;
742 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
746 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
748 frames_to_cd_frames_string (buf, status.index_position);
749 status.out << buf << endl;
755 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
759 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
760 status.out << "INDEX" << buf << endl;
764 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
769 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
771 framecnt_t remainder;
772 framecnt_t fr = session.nominal_frame_rate();
773 int mins, secs, frames;
775 mins = when / (60 * fr);
776 remainder = when - (mins * 60 * fr);
777 secs = remainder / fr;
778 remainder -= secs * fr;
779 frames = remainder / (fr / 75);
780 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
784 ExportHandler::frames_to_chapter_marks_string (char* buf, framepos_t when)
786 framecnt_t remainder;
787 framecnt_t fr = session.nominal_frame_rate();
788 int hours, mins, secs, msecs;
790 hours = when / (3600 * fr);
791 remainder = when - (hours * 3600 * fr);
792 mins = remainder / (60 * fr);
793 remainder -= mins * 60 * fr;
794 secs = remainder / fr;
795 remainder -= secs * fr;
796 msecs = (remainder * 1000) / fr;
797 sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
801 ExportHandler::toc_escape_cdtext (const std::string& txt)
803 Glib::ustring check (txt);
805 std::string latin1_txt;
809 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
810 } catch (Glib::ConvertError& err) {
811 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
816 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
820 } else if ((*c) == '\\') {
822 } else if (isprint (*c)) {
825 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
836 ExportHandler::toc_escape_filename (const std::string& txt)
842 // We iterate byte-wise not character-wise over a UTF-8 string here,
843 // because we only want to translate backslashes and double quotes
844 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
848 } else if (*c == '\\') {
861 ExportHandler::cue_escape_cdtext (const std::string& txt)
863 std::string latin1_txt;
867 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
868 } catch (Glib::ConvertError& err) {
869 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
872 // does not do much mor than UTF-8 to Latin1 translation yet, but
873 // that may have to change if cue parsers in burning programs change
874 out = '"' + latin1_txt + '"';
879 } // namespace ARDOUR