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/export_graph_builder.h"
31 #include "ardour/export_timespan.h"
32 #include "ardour/export_channel_configuration.h"
33 #include "ardour/export_status.h"
34 #include "ardour/export_format_specification.h"
35 #include "ardour/export_filename.h"
36 #include "ardour/soundcloud_upload.h"
37 #include "ardour/system_exec.h"
38 #include "pbd/openuri.h"
39 #include "pbd/basename.h"
40 #include "ardour/session_metadata.h"
50 /*** ExportElementFactory ***/
52 ExportElementFactory::ExportElementFactory (Session & session) :
58 ExportElementFactory::~ExportElementFactory ()
64 ExportElementFactory::add_timespan ()
66 return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
69 ExportChannelConfigPtr
70 ExportElementFactory::add_channel_config ()
72 return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
76 ExportElementFactory::add_format ()
78 return ExportFormatSpecPtr (new ExportFormatSpecification (session));
82 ExportElementFactory::add_format (XMLNode const & state)
84 return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
88 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
90 return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
94 ExportElementFactory::add_filename ()
96 return ExportFilenamePtr (new ExportFilename (session));
100 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
102 return ExportFilenamePtr (new ExportFilename (*other));
105 /*** ExportHandler ***/
107 ExportHandler::ExportHandler (Session & session)
108 : ExportElementFactory (session)
110 , graph_builder (new ExportGraphBuilder (session))
111 , export_status (session.get_export_status ())
112 , normalizing (false)
118 ExportHandler::~ExportHandler ()
120 // TODO remove files that were written but not finished
123 /** Add an export to the `to-do' list */
125 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
126 ExportFormatSpecPtr format, ExportFilenamePtr filename,
127 BroadcastInfoPtr broadcast_info)
129 FileSpec spec (channel_config, format, filename, broadcast_info);
130 config_map.insert (make_pair (timespan, spec));
136 ExportHandler::do_export ()
138 /* Count timespans */
140 export_status->init();
141 std::set<ExportTimespanPtr> timespan_set;
142 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
143 bool new_timespan = timespan_set.insert (it->first).second;
145 export_status->total_frames += it->first->get_length();
148 export_status->total_timespans = timespan_set.size();
156 ExportHandler::start_timespan ()
158 export_status->timespan++;
160 if (config_map.empty()) {
161 // freewheeling has to be stopped from outside the process cycle
162 export_status->running = false;
166 /* finish_timespan pops the config_map entry that has been done, so
167 this is the timespan to do this time
169 current_timespan = config_map.begin()->first;
171 export_status->total_frames_current_timespan = current_timespan->get_length();
172 export_status->timespan_name = current_timespan->name();
173 export_status->processed_frames_current_timespan = 0;
175 /* Register file configurations to graph builder */
177 /* Here's the config_map entries that use this timespan */
178 timespan_bounds = config_map.equal_range (current_timespan);
179 graph_builder->reset ();
180 graph_builder->set_current_timespan (current_timespan);
181 handle_duplicate_format_extensions();
182 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
183 // Filenames can be shared across timespans
184 FileSpec & spec = it->second;
185 spec.filename->set_timespan (it->first);
186 graph_builder->add_config (spec);
192 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
193 process_position = current_timespan->get_start();
194 session.start_audio_export (process_position);
198 ExportHandler::handle_duplicate_format_extensions()
200 typedef std::map<std::string, int> ExtCountMap;
203 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
204 counts[it->second.format->extension()]++;
207 bool duplicates_found = false;
208 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
209 if (it->second > 1) { duplicates_found = true; }
212 // Set this always, as the filenames are shared...
213 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
214 it->second.filename->include_format_name = duplicates_found;
219 ExportHandler::process (framecnt_t frames)
221 if (!export_status->running) {
223 } else if (normalizing) {
224 return process_normalize ();
226 return process_timespan (frames);
231 ExportHandler::process_timespan (framecnt_t frames)
233 /* update position */
235 framecnt_t frames_to_read = 0;
236 framepos_t const end = current_timespan->get_end();
238 bool const last_cycle = (process_position + frames >= end);
241 frames_to_read = end - process_position;
242 export_status->stop = true;
244 frames_to_read = frames;
247 process_position += frames_to_read;
248 export_status->processed_frames += frames_to_read;
249 export_status->processed_frames_current_timespan += frames_to_read;
251 /* Do actual processing */
252 int ret = graph_builder->process (frames_to_read, last_cycle);
254 /* Start normalizing if necessary */
256 normalizing = graph_builder->will_normalize();
258 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
259 export_status->current_normalize_cycle = 0;
270 ExportHandler::process_normalize ()
272 if (graph_builder->process_normalize ()) {
274 export_status->normalizing = false;
276 export_status->normalizing = true;
279 export_status->current_normalize_cycle++;
285 ExportHandler::command_output(std::string output, size_t size)
287 std::cerr << "command: " << size << ", " << output << std::endl;
288 info << output << endmsg;
292 ExportHandler::finish_timespan ()
294 while (config_map.begin() != timespan_bounds.second) {
296 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
297 std::string filename = config_map.begin()->second.filename->get_path(fmt);
299 if (fmt->with_cue()) {
300 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
303 if (fmt->with_toc()) {
304 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
308 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
311 if (!fmt->command().empty()) {
313 #if 0 // would be nicer with C++11 initialiser...
314 std::map<char, std::string> subs {
316 { 'd', Glib::path_get_dirname(filename) },
317 { 'b', PBD::basename_nosuffix(filename) },
318 { 'u', upload_username },
319 { 'p', upload_password}
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)));
327 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix(filename)));
328 subs.insert (std::pair<char, std::string> ('u', soundcloud_username));
329 subs.insert (std::pair<char, std::string> ('p', soundcloud_password));
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);
353 << filename << std::endl
354 << "username = " << soundcloud_username
355 << ", password = " << soundcloud_password
356 << " - token = " << token << " ..."
358 std::string path = soundcloud_uploader->Upload (
360 PBD::basename_nosuffix(filename), // title
362 soundcloud_make_public,
365 if (path.length() != 0) {
366 if (soundcloud_open_page) {
367 std::cerr << "opening " << path << " ..." << std::endl;
368 open_uri(path.c_str()); // open the soundcloud website to the new file
371 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
373 delete soundcloud_uploader;
375 config_map.erase (config_map.begin());
381 /*** CD Marker stuff ***/
383 struct LocationSortByStart {
384 bool operator() (Location *a, Location *b) {
385 return a->start() < b->start();
390 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
391 std::string filename, CDMarkerFormat format)
393 string filepath = get_cd_marker_filename(filename, format);
396 void (ExportHandler::*header_func) (CDMarkerStatus &);
397 void (ExportHandler::*track_func) (CDMarkerStatus &);
398 void (ExportHandler::*index_func) (CDMarkerStatus &);
402 header_func = &ExportHandler::write_toc_header;
403 track_func = &ExportHandler::write_track_info_toc;
404 index_func = &ExportHandler::write_index_info_toc;
407 header_func = &ExportHandler::write_cue_header;
408 track_func = &ExportHandler::write_track_info_cue;
409 index_func = &ExportHandler::write_index_info_cue;
415 CDMarkerStatus status (filepath, timespan, file_format, filename);
418 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
422 (this->*header_func) (status);
424 /* Get locations and sort */
426 Locations::LocationList const & locations (session.locations()->list());
427 Locations::LocationList::const_iterator i;
428 Locations::LocationList temp;
430 for (i = locations.begin(); i != locations.end(); ++i) {
431 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
437 // TODO One index marker for whole thing
441 LocationSortByStart cmp;
443 Locations::LocationList::const_iterator nexti;
445 /* Start actual marker stuff */
447 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
448 status.track_position = last_start_time - timespan->get_start();
450 for (i = temp.begin(); i != temp.end(); ++i) {
454 if ((*i)->start() < last_end_time) {
455 if ((*i)->is_mark()) {
456 /* Index within track */
458 status.index_position = (*i)->start() - timespan->get_start();
459 (this->*index_func) (status);
465 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
467 status.track_position = last_end_time - timespan->get_start();
468 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
469 status.track_duration = 0;
471 if ((*i)->is_mark()) {
472 // a mark track location needs to look ahead to the next marker's start to determine length
476 if (nexti != temp.end()) {
477 status.track_duration = (*nexti)->start() - last_end_time;
479 last_start_time = (*i)->start();
480 last_end_time = (*nexti)->start();
482 // this was the last marker, use timespan end
483 status.track_duration = timespan->get_end() - last_end_time;
485 last_start_time = (*i)->start();
486 last_end_time = timespan->get_end();
490 status.track_duration = (*i)->end() - last_end_time;
492 last_start_time = (*i)->start();
493 last_end_time = (*i)->end();
496 (this->*track_func) (status);
499 } catch (std::exception& e) {
500 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
501 ::g_unlink (filepath.c_str());
502 } catch (Glib::Exception& e) {
503 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
504 ::g_unlink (filepath.c_str());
509 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
511 /* do not strip file suffix because there may be more than one format,
512 and we do not want the CD marker file from one format to overwrite
513 another (e.g. foo.wav.cue > foo.aiff.cue)
518 return filename + ".toc";
520 return filename + ".cue";
522 return filename + ".marker"; // Should not be reached when actually creating a file
527 ExportHandler::write_cue_header (CDMarkerStatus & status)
529 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
531 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
532 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
534 /* The original cue sheet sepc metions five file types
536 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
537 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
540 We try to use these file types whenever appropriate and
541 default to our own names otherwise.
543 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
544 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
545 status.out << "WAVE";
546 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
547 status.format->sample_format() == ExportFormatBase::SF_16 &&
548 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
549 // Format is RAW 16bit 44.1kHz
550 if (status.format->endianness() == ExportFormatBase::E_Little) {
551 status.out << "BINARY";
553 status.out << "MOTOROLA";
556 // no special case for AIFF format it's name is already "AIFF"
557 status.out << status.format->format_name();
563 ExportHandler::write_toc_header (CDMarkerStatus & status)
565 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
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