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 // TODO remove files that were written but not finished
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);
309 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
312 if (!fmt->command().empty()) {
314 #if 0 // would be nicer with C++11 initialiser...
315 std::map<char, std::string> subs {
317 { 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR },
318 { 'b', PBD::basename_nosuffix(filename) },
323 PBD::ScopedConnection command_connection;
324 std::map<char, std::string> subs;
325 subs.insert (std::pair<char, std::string> ('f', filename));
326 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
327 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
328 subs.insert (std::pair<char, std::string> ('s', session.path ()));
329 subs.insert (std::pair<char, std::string> ('n', session.name ()));
331 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
332 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
333 if (se->start (2) == 0) {
334 // successfully started
335 while (se->is_running ()) {
336 // wait for system exec to terminate
340 error << "post-export hook failed! " << fmt->command() << endmsg;
345 if (fmt->soundcloud_upload()) {
346 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
347 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
348 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
349 "uploading %1 - username=%2, password=%3, token=%4",
350 filename, soundcloud_username, soundcloud_password, token) );
351 std::string path = soundcloud_uploader->Upload (
353 PBD::basename_nosuffix(filename), // title
355 soundcloud_make_public,
356 soundcloud_downloadable,
359 if (path.length() != 0) {
360 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
361 if (soundcloud_open_page) {
362 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
363 open_uri(path.c_str()); // open the soundcloud website to the new file
366 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
368 delete soundcloud_uploader;
370 config_map.erase (config_map.begin());
376 /*** CD Marker stuff ***/
378 struct LocationSortByStart {
379 bool operator() (Location *a, Location *b) {
380 return a->start() < b->start();
385 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
386 std::string filename, CDMarkerFormat format)
388 string filepath = get_cd_marker_filename(filename, format);
391 void (ExportHandler::*header_func) (CDMarkerStatus &);
392 void (ExportHandler::*track_func) (CDMarkerStatus &);
393 void (ExportHandler::*index_func) (CDMarkerStatus &);
397 header_func = &ExportHandler::write_toc_header;
398 track_func = &ExportHandler::write_track_info_toc;
399 index_func = &ExportHandler::write_index_info_toc;
402 header_func = &ExportHandler::write_cue_header;
403 track_func = &ExportHandler::write_track_info_cue;
404 index_func = &ExportHandler::write_index_info_cue;
410 CDMarkerStatus status (filepath, timespan, file_format, filename);
413 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
417 (this->*header_func) (status);
419 /* Get locations and sort */
421 Locations::LocationList const & locations (session.locations()->list());
422 Locations::LocationList::const_iterator i;
423 Locations::LocationList temp;
425 for (i = locations.begin(); i != locations.end(); ++i) {
426 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
432 // TODO One index marker for whole thing
436 LocationSortByStart cmp;
438 Locations::LocationList::const_iterator nexti;
440 /* Start actual marker stuff */
442 framepos_t last_end_time = timespan->get_start();
443 status.track_position = 0;
445 for (i = temp.begin(); i != temp.end(); ++i) {
449 if ((*i)->start() < last_end_time) {
450 if ((*i)->is_mark()) {
451 /* Index within track */
453 status.index_position = (*i)->start() - timespan->get_start();
454 (this->*index_func) (status);
460 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
462 status.track_position = last_end_time - timespan->get_start();
463 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
464 status.track_duration = 0;
466 if ((*i)->is_mark()) {
467 // a mark track location needs to look ahead to the next marker's start to determine length
471 if (nexti != temp.end()) {
472 status.track_duration = (*nexti)->start() - last_end_time;
474 last_end_time = (*nexti)->start();
476 // this was the last marker, use timespan end
477 status.track_duration = timespan->get_end() - last_end_time;
479 last_end_time = timespan->get_end();
483 status.track_duration = (*i)->end() - last_end_time;
485 last_end_time = (*i)->end();
488 (this->*track_func) (status);
491 } catch (std::exception& e) {
492 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
493 ::g_unlink (filepath.c_str());
494 } catch (Glib::Exception& e) {
495 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
496 ::g_unlink (filepath.c_str());
501 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
503 /* do not strip file suffix because there may be more than one format,
504 and we do not want the CD marker file from one format to overwrite
505 another (e.g. foo.wav.cue > foo.aiff.cue)
510 return filename + ".toc";
512 return filename + ".cue";
514 return filename + ".marker"; // Should not be reached when actually creating a file
519 ExportHandler::write_cue_header (CDMarkerStatus & status)
521 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
522 string catalog = SessionMetadata::Metadata()->catalog();
524 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
526 status.out << "CATALOG " << catalog << endl;
528 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
530 /* The original cue sheet sepc metions five file types
532 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
533 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
536 We try to use these file types whenever appropriate and
537 default to our own names otherwise.
539 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
540 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
541 status.out << "WAVE";
542 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
543 status.format->sample_format() == ExportFormatBase::SF_16 &&
544 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
545 // Format is RAW 16bit 44.1kHz
546 if (status.format->endianness() == ExportFormatBase::E_Little) {
547 status.out << "BINARY";
549 status.out << "MOTOROLA";
552 // no special case for AIFF format it's name is already "AIFF"
553 status.out << status.format->format_name();
559 ExportHandler::write_toc_header (CDMarkerStatus & status)
561 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
562 string catalog = SessionMetadata::Metadata()->catalog();
565 status.out << "CATALOG " << catalog << endl;
567 status.out << "CD_DA" << endl;
568 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
569 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
570 status.out << " PERFORMER \"\"" << endl << " }" << endl << "}" << endl;
574 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
578 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
579 status.out << buf << endl;
581 status.out << " FLAGS" ;
582 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
583 status.out << " SCMS ";
585 status.out << " DCP ";
588 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
589 status.out << " PRE";
593 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
594 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
597 if (status.marker->name() != "") {
598 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
601 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
602 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
605 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
606 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
609 if (status.track_position != status.track_start_frame) {
610 frames_to_cd_frames_string (buf, status.track_position);
611 status.out << " INDEX 00" << buf << endl;
614 frames_to_cd_frames_string (buf, status.track_start_frame);
615 status.out << " INDEX 01" << buf << endl;
617 status.index_number = 2;
618 status.track_number++;
622 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
626 status.out << endl << "TRACK AUDIO" << endl;
628 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
631 status.out << "COPY" << endl;
633 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
634 status.out << "PRE_EMPHASIS" << endl;
636 status.out << "NO PRE_EMPHASIS" << endl;
639 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
640 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
643 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
644 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
646 status.out << " PERFORMER ";
647 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
648 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
650 status.out << "\"\"" << endl;
653 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
654 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
657 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
658 status.out << " ISRC \"";
659 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
660 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
661 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
662 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
665 status.out << " }" << endl << "}" << endl;
667 frames_to_cd_frames_string (buf, status.track_position);
668 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
670 frames_to_cd_frames_string (buf, status.track_duration);
671 status.out << buf << endl;
673 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
674 status.out << "START" << buf << endl;
678 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
682 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
684 frames_to_cd_frames_string (buf, status.index_position);
685 status.out << buf << endl;
691 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
695 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
696 status.out << "INDEX" << buf << endl;
700 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
702 framecnt_t remainder;
703 framecnt_t fr = session.nominal_frame_rate();
704 int mins, secs, frames;
706 mins = when / (60 * fr);
707 remainder = when - (mins * 60 * fr);
708 secs = remainder / fr;
709 remainder -= secs * fr;
710 frames = remainder / (fr / 75);
711 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
715 ExportHandler::toc_escape_cdtext (const std::string& txt)
717 Glib::ustring check (txt);
719 std::string latin1_txt;
723 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
724 } catch (Glib::ConvertError& err) {
725 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
730 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
734 } else if ((*c) == '\\') {
736 } else if (isprint (*c)) {
739 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
750 ExportHandler::toc_escape_filename (const std::string& txt)
756 // We iterate byte-wise not character-wise over a UTF-8 string here,
757 // because we only want to translate backslashes and double quotes
758 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
762 } else if (*c == '\\') {
775 ExportHandler::cue_escape_cdtext (const std::string& txt)
777 std::string latin1_txt;
781 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
782 } catch (Glib::ConvertError& err) {
783 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
786 // does not do much mor than UTF-8 to Latin1 translation yet, but
787 // that may have to change if cue parsers in burning programs change
788 out = '"' + latin1_txt + '"';
793 } // namespace ARDOUR