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 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
316 if (!fmt->command().empty()) {
318 #if 0 // would be nicer with C++11 initialiser...
319 std::map<char, std::string> subs {
321 { 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR },
322 { 'b', PBD::basename_nosuffix(filename) },
327 PBD::ScopedConnection command_connection;
328 std::map<char, std::string> subs;
329 subs.insert (std::pair<char, std::string> ('f', filename));
330 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
331 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
332 subs.insert (std::pair<char, std::string> ('s', session.path ()));
333 subs.insert (std::pair<char, std::string> ('n', session.name ()));
335 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
336 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
337 if (se->start (2) == 0) {
338 // successfully started
339 while (se->is_running ()) {
340 // wait for system exec to terminate
344 error << "post-export hook failed! " << fmt->command() << endmsg;
349 if (fmt->soundcloud_upload()) {
350 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
351 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
352 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
353 "uploading %1 - username=%2, password=%3, token=%4",
354 filename, soundcloud_username, soundcloud_password, token) );
355 std::string path = soundcloud_uploader->Upload (
357 PBD::basename_nosuffix(filename), // title
359 soundcloud_make_public,
360 soundcloud_downloadable,
363 if (path.length() != 0) {
364 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
365 if (soundcloud_open_page) {
366 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
367 open_uri(path.c_str()); // open the soundcloud website to the new file
370 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
372 delete soundcloud_uploader;
374 config_map.erase (config_map.begin());
380 /*** CD Marker stuff ***/
382 struct LocationSortByStart {
383 bool operator() (Location *a, Location *b) {
384 return a->start() < b->start();
389 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
390 std::string filename, CDMarkerFormat format)
392 string filepath = get_cd_marker_filename(filename, format);
395 void (ExportHandler::*header_func) (CDMarkerStatus &);
396 void (ExportHandler::*track_func) (CDMarkerStatus &);
397 void (ExportHandler::*index_func) (CDMarkerStatus &);
401 header_func = &ExportHandler::write_toc_header;
402 track_func = &ExportHandler::write_track_info_toc;
403 index_func = &ExportHandler::write_index_info_toc;
406 header_func = &ExportHandler::write_cue_header;
407 track_func = &ExportHandler::write_track_info_cue;
408 index_func = &ExportHandler::write_index_info_cue;
411 header_func = &ExportHandler::write_mp4ch_header;
412 track_func = &ExportHandler::write_track_info_mp4ch;
413 index_func = &ExportHandler::write_index_info_mp4ch;
419 CDMarkerStatus status (filepath, timespan, file_format, filename);
422 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
426 (this->*header_func) (status);
428 /* Get locations and sort */
430 Locations::LocationList const & locations (session.locations()->list());
431 Locations::LocationList::const_iterator i;
432 Locations::LocationList temp;
434 for (i = locations.begin(); i != locations.end(); ++i) {
435 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
441 // TODO One index marker for whole thing
445 LocationSortByStart cmp;
447 Locations::LocationList::const_iterator nexti;
449 /* Start actual marker stuff */
451 framepos_t last_end_time = timespan->get_start();
452 status.track_position = 0;
454 for (i = temp.begin(); i != temp.end(); ++i) {
458 if ((*i)->start() < last_end_time) {
459 if ((*i)->is_mark()) {
460 /* Index within track */
462 status.index_position = (*i)->start() - timespan->get_start();
463 (this->*index_func) (status);
469 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
471 status.track_position = last_end_time - timespan->get_start();
472 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
473 status.track_duration = 0;
475 if ((*i)->is_mark()) {
476 // a mark track location needs to look ahead to the next marker's start to determine length
480 if (nexti != temp.end()) {
481 status.track_duration = (*nexti)->start() - last_end_time;
483 last_end_time = (*nexti)->start();
485 // this was the last marker, use timespan end
486 status.track_duration = timespan->get_end() - last_end_time;
488 last_end_time = timespan->get_end();
492 status.track_duration = (*i)->end() - last_end_time;
494 last_end_time = (*i)->end();
497 (this->*track_func) (status);
500 } catch (std::exception& e) {
501 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
502 ::g_unlink (filepath.c_str());
503 } catch (Glib::Exception& e) {
504 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
505 ::g_unlink (filepath.c_str());
510 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
512 /* do not strip file suffix because there may be more than one format,
513 and we do not want the CD marker file from one format to overwrite
514 another (e.g. foo.wav.cue > foo.aiff.cue)
519 return filename + ".toc";
521 return filename + ".cue";
524 unsigned lastdot = filename.find_last_of('.');
525 return filename.substr(0,lastdot) + ".chapters.txt";
528 return filename + ".marker"; // Should not be reached when actually creating a file
533 ExportHandler::write_cue_header (CDMarkerStatus & status)
535 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
538 string barcode = SessionMetadata::Metadata()->barcode();
539 string album_artist = SessionMetadata::Metadata()->album_artist();
540 string album_title = SessionMetadata::Metadata()->album();
542 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
545 status.out << "CATALOG " << barcode << endl;
547 if (album_artist != "")
548 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
550 if (album_title != "")
553 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
555 /* The original cue sheet spec mentions five file types
557 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
558 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
561 We try to use these file types whenever appropriate and
562 default to our own names otherwise.
564 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
565 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
566 status.out << "WAVE";
567 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
568 status.format->sample_format() == ExportFormatBase::SF_16 &&
569 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
570 // Format is RAW 16bit 44.1kHz
571 if (status.format->endianness() == ExportFormatBase::E_Little) {
572 status.out << "BINARY";
574 status.out << "MOTOROLA";
577 // no special case for AIFF format it's name is already "AIFF"
578 status.out << status.format->format_name();
584 ExportHandler::write_toc_header (CDMarkerStatus & status)
586 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
589 string barcode = SessionMetadata::Metadata()->barcode();
590 string album_artist = SessionMetadata::Metadata()->album_artist();
591 string album_title = SessionMetadata::Metadata()->album();
594 status.out << "CATALOG \"" << barcode << "\"" << endl;
596 if (album_title != "")
599 status.out << "CD_DA" << endl;
600 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
601 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
602 status.out << " PERFORMER " << toc_escape_cdtext (album_artist) << endl;
603 status.out << " }" << endl << "}" << endl;
607 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
609 status.out << "00:00:00.000 Intro" << endl;
613 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
617 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
618 status.out << buf << endl;
620 status.out << " FLAGS" ;
621 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
622 status.out << " SCMS ";
624 status.out << " DCP ";
627 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
628 status.out << " PRE";
632 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
633 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
636 if (status.marker->name() != "") {
637 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
640 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
641 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
644 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
645 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
648 if (status.track_position != status.track_start_frame) {
649 frames_to_cd_frames_string (buf, status.track_position);
650 status.out << " INDEX 00" << buf << endl;
653 frames_to_cd_frames_string (buf, status.track_start_frame);
654 status.out << " INDEX 01" << buf << endl;
656 status.index_number = 2;
657 status.track_number++;
661 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
665 status.out << endl << "TRACK AUDIO" << endl;
667 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
670 status.out << "COPY" << endl;
672 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
673 status.out << "PRE_EMPHASIS" << endl;
675 status.out << "NO PRE_EMPHASIS" << endl;
678 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
679 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
682 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
683 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
685 status.out << " PERFORMER ";
686 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
687 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
689 status.out << "\"\"" << endl;
692 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
693 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
696 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
697 status.out << " ISRC \"";
698 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
699 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
700 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
701 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
704 status.out << " }" << endl << "}" << endl;
706 frames_to_cd_frames_string (buf, status.track_position);
707 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
709 frames_to_cd_frames_string (buf, status.track_duration);
710 status.out << buf << endl;
712 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
713 status.out << "START" << buf << endl;
716 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
720 frames_to_chapter_marks_string(buf, status.track_start_frame);
721 status.out << buf << " " << status.marker->name() << endl;
725 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
729 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
731 frames_to_cd_frames_string (buf, status.index_position);
732 status.out << buf << endl;
738 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
742 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
743 status.out << "INDEX" << buf << endl;
747 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
752 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
754 framecnt_t remainder;
755 framecnt_t fr = session.nominal_frame_rate();
756 int mins, secs, frames;
758 mins = when / (60 * fr);
759 remainder = when - (mins * 60 * fr);
760 secs = remainder / fr;
761 remainder -= secs * fr;
762 frames = remainder / (fr / 75);
763 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
767 ExportHandler::frames_to_chapter_marks_string (char* buf, framepos_t when)
769 framecnt_t remainder;
770 framecnt_t fr = session.nominal_frame_rate();
771 int hours, mins, secs, msecs;
773 hours = when / (3600 * fr);
774 remainder = when - (hours * 3600 * fr);
775 mins = remainder / (60 * fr);
776 remainder -= mins * 60 * fr;
777 secs = remainder / fr;
778 remainder -= secs * fr;
779 msecs = (remainder * 1000) / fr;
780 sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
784 ExportHandler::toc_escape_cdtext (const std::string& txt)
786 Glib::ustring check (txt);
788 std::string latin1_txt;
792 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
793 } catch (Glib::ConvertError& err) {
794 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
799 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
803 } else if ((*c) == '\\') {
805 } else if (isprint (*c)) {
808 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
819 ExportHandler::toc_escape_filename (const std::string& txt)
825 // We iterate byte-wise not character-wise over a UTF-8 string here,
826 // because we only want to translate backslashes and double quotes
827 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
831 } else if (*c == '\\') {
844 ExportHandler::cue_escape_cdtext (const std::string& txt)
846 std::string latin1_txt;
850 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
851 } catch (Glib::ConvertError& err) {
852 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
855 // does not do much mor than UTF-8 to Latin1 translation yet, but
856 // that may have to change if cue parsers in burning programs change
857 out = '"' + latin1_txt + '"';
862 } // namespace ARDOUR