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 ()));
332 std::cerr << "running command: " << fmt->command() << "..." << std::endl;
333 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
334 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
335 if (se->start (2) == 0) {
336 // successfully started
337 std::cerr << "started!" << std::endl;
338 while (se->is_running ()) {
339 // wait for system exec to terminate
340 // std::cerr << "waiting..." << std::endl;
344 std::cerr << "done! deleting..." << std::endl;
348 if (fmt->soundcloud_upload()) {
349 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
350 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
351 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
352 "uploading %1 - username=%2, password=%3, token=%4",
353 filename, soundcloud_username, soundcloud_password, token) );
354 std::string path = soundcloud_uploader->Upload (
356 PBD::basename_nosuffix(filename), // title
358 soundcloud_make_public,
359 soundcloud_downloadable,
362 if (path.length() != 0) {
363 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
364 if (soundcloud_open_page) {
365 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
366 open_uri(path.c_str()); // open the soundcloud website to the new file
369 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
371 delete soundcloud_uploader;
373 config_map.erase (config_map.begin());
379 /*** CD Marker stuff ***/
381 struct LocationSortByStart {
382 bool operator() (Location *a, Location *b) {
383 return a->start() < b->start();
388 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
389 std::string filename, CDMarkerFormat format)
391 string filepath = get_cd_marker_filename(filename, format);
394 void (ExportHandler::*header_func) (CDMarkerStatus &);
395 void (ExportHandler::*track_func) (CDMarkerStatus &);
396 void (ExportHandler::*index_func) (CDMarkerStatus &);
400 header_func = &ExportHandler::write_toc_header;
401 track_func = &ExportHandler::write_track_info_toc;
402 index_func = &ExportHandler::write_index_info_toc;
405 header_func = &ExportHandler::write_cue_header;
406 track_func = &ExportHandler::write_track_info_cue;
407 index_func = &ExportHandler::write_index_info_cue;
413 CDMarkerStatus status (filepath, timespan, file_format, filename);
416 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
420 (this->*header_func) (status);
422 /* Get locations and sort */
424 Locations::LocationList const & locations (session.locations()->list());
425 Locations::LocationList::const_iterator i;
426 Locations::LocationList temp;
428 for (i = locations.begin(); i != locations.end(); ++i) {
429 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
435 // TODO One index marker for whole thing
439 LocationSortByStart cmp;
441 Locations::LocationList::const_iterator nexti;
443 /* Start actual marker stuff */
445 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
446 status.track_position = last_start_time - timespan->get_start();
448 for (i = temp.begin(); i != temp.end(); ++i) {
452 if ((*i)->start() < last_end_time) {
453 if ((*i)->is_mark()) {
454 /* Index within track */
456 status.index_position = (*i)->start() - timespan->get_start();
457 (this->*index_func) (status);
463 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
465 status.track_position = last_end_time - timespan->get_start();
466 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
467 status.track_duration = 0;
469 if ((*i)->is_mark()) {
470 // a mark track location needs to look ahead to the next marker's start to determine length
474 if (nexti != temp.end()) {
475 status.track_duration = (*nexti)->start() - last_end_time;
477 last_start_time = (*i)->start();
478 last_end_time = (*nexti)->start();
480 // this was the last marker, use timespan end
481 status.track_duration = timespan->get_end() - last_end_time;
483 last_start_time = (*i)->start();
484 last_end_time = timespan->get_end();
488 status.track_duration = (*i)->end() - last_end_time;
490 last_start_time = (*i)->start();
491 last_end_time = (*i)->end();
494 (this->*track_func) (status);
497 } catch (std::exception& e) {
498 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
499 ::g_unlink (filepath.c_str());
500 } catch (Glib::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());
507 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
509 /* do not strip file suffix because there may be more than one format,
510 and we do not want the CD marker file from one format to overwrite
511 another (e.g. foo.wav.cue > foo.aiff.cue)
516 return filename + ".toc";
518 return filename + ".cue";
520 return filename + ".marker"; // Should not be reached when actually creating a file
525 ExportHandler::write_cue_header (CDMarkerStatus & status)
527 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
529 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
530 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
532 /* The original cue sheet sepc metions five file types
534 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
535 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
538 We try to use these file types whenever appropriate and
539 default to our own names otherwise.
541 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
542 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
543 status.out << "WAVE";
544 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
545 status.format->sample_format() == ExportFormatBase::SF_16 &&
546 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
547 // Format is RAW 16bit 44.1kHz
548 if (status.format->endianness() == ExportFormatBase::E_Little) {
549 status.out << "BINARY";
551 status.out << "MOTOROLA";
554 // no special case for AIFF format it's name is already "AIFF"
555 status.out << status.format->format_name();
561 ExportHandler::write_toc_header (CDMarkerStatus & status)
563 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
565 status.out << "CD_DA" << endl;
566 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
567 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
568 status.out << " PERFORMER \"\"" << endl << " }" << endl << "}" << endl;
572 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
576 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
577 status.out << buf << endl;
579 status.out << " FLAGS" ;
580 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
581 status.out << " SCMS ";
583 status.out << " DCP ";
586 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
587 status.out << " PRE";
591 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
592 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
595 if (status.marker->name() != "") {
596 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
599 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
600 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
603 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
604 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
607 if (status.track_position != status.track_start_frame) {
608 frames_to_cd_frames_string (buf, status.track_position);
609 status.out << " INDEX 00" << buf << endl;
612 frames_to_cd_frames_string (buf, status.track_start_frame);
613 status.out << " INDEX 01" << buf << endl;
615 status.index_number = 2;
616 status.track_number++;
620 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
624 status.out << endl << "TRACK AUDIO" << endl;
626 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
629 status.out << "COPY" << endl;
631 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
632 status.out << "PRE_EMPHASIS" << endl;
634 status.out << "NO PRE_EMPHASIS" << endl;
637 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
638 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
641 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
642 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
644 status.out << " PERFORMER ";
645 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
646 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
648 status.out << "\"\"" << endl;
651 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
652 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
655 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
656 status.out << " ISRC \"";
657 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
658 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
659 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
660 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
663 status.out << " }" << endl << "}" << endl;
665 frames_to_cd_frames_string (buf, status.track_position);
666 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
668 frames_to_cd_frames_string (buf, status.track_duration);
669 status.out << buf << endl;
671 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
672 status.out << "START" << buf << endl;
676 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
680 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
682 frames_to_cd_frames_string (buf, status.index_position);
683 status.out << buf << endl;
689 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
693 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
694 status.out << "INDEX" << buf << endl;
698 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
700 framecnt_t remainder;
701 framecnt_t fr = session.nominal_frame_rate();
702 int mins, secs, frames;
704 mins = when / (60 * fr);
705 remainder = when - (mins * 60 * fr);
706 secs = remainder / fr;
707 remainder -= secs * fr;
708 frames = remainder / (fr / 75);
709 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
713 ExportHandler::toc_escape_cdtext (const std::string& txt)
715 Glib::ustring check (txt);
717 std::string latin1_txt;
721 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
722 } catch (Glib::ConvertError& err) {
723 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
728 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
732 } else if ((*c) == '\\') {
734 } else if (isprint (*c)) {
737 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
748 ExportHandler::toc_escape_filename (const std::string& txt)
754 // We iterate byte-wise not character-wise over a UTF-8 string here,
755 // because we only want to translate backslashes and double quotes
756 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
760 } else if (*c == '\\') {
773 ExportHandler::cue_escape_cdtext (const std::string& txt)
775 std::string latin1_txt;
779 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
780 } catch (Glib::ConvertError& err) {
781 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
784 // does not do much mor than UTF-8 to Latin1 translation yet, but
785 // that may have to change if cue parsers in burning programs change
786 out = '"' + latin1_txt + '"';
791 } // namespace ARDOUR