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,
363 soundcloud_downloadable,
366 if (path.length() != 0) {
367 if (soundcloud_open_page) {
368 std::cerr << "opening " << path << " ..." << std::endl;
369 open_uri(path.c_str()); // open the soundcloud website to the new file
372 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
374 delete soundcloud_uploader;
376 config_map.erase (config_map.begin());
382 /*** CD Marker stuff ***/
384 struct LocationSortByStart {
385 bool operator() (Location *a, Location *b) {
386 return a->start() < b->start();
391 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
392 std::string filename, CDMarkerFormat format)
394 string filepath = get_cd_marker_filename(filename, format);
397 void (ExportHandler::*header_func) (CDMarkerStatus &);
398 void (ExportHandler::*track_func) (CDMarkerStatus &);
399 void (ExportHandler::*index_func) (CDMarkerStatus &);
403 header_func = &ExportHandler::write_toc_header;
404 track_func = &ExportHandler::write_track_info_toc;
405 index_func = &ExportHandler::write_index_info_toc;
408 header_func = &ExportHandler::write_cue_header;
409 track_func = &ExportHandler::write_track_info_cue;
410 index_func = &ExportHandler::write_index_info_cue;
416 CDMarkerStatus status (filepath, timespan, file_format, filename);
419 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
423 (this->*header_func) (status);
425 /* Get locations and sort */
427 Locations::LocationList const & locations (session.locations()->list());
428 Locations::LocationList::const_iterator i;
429 Locations::LocationList temp;
431 for (i = locations.begin(); i != locations.end(); ++i) {
432 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
438 // TODO One index marker for whole thing
442 LocationSortByStart cmp;
444 Locations::LocationList::const_iterator nexti;
446 /* Start actual marker stuff */
448 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
449 status.track_position = last_start_time - timespan->get_start();
451 for (i = temp.begin(); i != temp.end(); ++i) {
455 if ((*i)->start() < last_end_time) {
456 if ((*i)->is_mark()) {
457 /* Index within track */
459 status.index_position = (*i)->start() - timespan->get_start();
460 (this->*index_func) (status);
466 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
468 status.track_position = last_end_time - timespan->get_start();
469 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
470 status.track_duration = 0;
472 if ((*i)->is_mark()) {
473 // a mark track location needs to look ahead to the next marker's start to determine length
477 if (nexti != temp.end()) {
478 status.track_duration = (*nexti)->start() - last_end_time;
480 last_start_time = (*i)->start();
481 last_end_time = (*nexti)->start();
483 // this was the last marker, use timespan end
484 status.track_duration = timespan->get_end() - last_end_time;
486 last_start_time = (*i)->start();
487 last_end_time = timespan->get_end();
491 status.track_duration = (*i)->end() - last_end_time;
493 last_start_time = (*i)->start();
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";
523 return filename + ".marker"; // Should not be reached when actually creating a file
528 ExportHandler::write_cue_header (CDMarkerStatus & status)
530 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
532 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
533 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
535 /* The original cue sheet sepc metions five file types
537 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
538 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
541 We try to use these file types whenever appropriate and
542 default to our own names otherwise.
544 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
545 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
546 status.out << "WAVE";
547 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
548 status.format->sample_format() == ExportFormatBase::SF_16 &&
549 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
550 // Format is RAW 16bit 44.1kHz
551 if (status.format->endianness() == ExportFormatBase::E_Little) {
552 status.out << "BINARY";
554 status.out << "MOTOROLA";
557 // no special case for AIFF format it's name is already "AIFF"
558 status.out << status.format->format_name();
564 ExportHandler::write_toc_header (CDMarkerStatus & status)
566 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
568 status.out << "CD_DA" << endl;
569 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
570 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
571 status.out << " PERFORMER \"\"" << endl << " }" << endl << "}" << endl;
575 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
579 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
580 status.out << buf << endl;
582 status.out << " FLAGS" ;
583 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
584 status.out << " SCMS ";
586 status.out << " DCP ";
589 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
590 status.out << " PRE";
594 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
595 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
598 if (status.marker->name() != "") {
599 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
602 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
603 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
606 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
607 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
610 if (status.track_position != status.track_start_frame) {
611 frames_to_cd_frames_string (buf, status.track_position);
612 status.out << " INDEX 00" << buf << endl;
615 frames_to_cd_frames_string (buf, status.track_start_frame);
616 status.out << " INDEX 01" << buf << endl;
618 status.index_number = 2;
619 status.track_number++;
623 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
627 status.out << endl << "TRACK AUDIO" << endl;
629 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
632 status.out << "COPY" << endl;
634 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
635 status.out << "PRE_EMPHASIS" << endl;
637 status.out << "NO PRE_EMPHASIS" << endl;
640 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
641 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
644 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
645 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
647 status.out << " PERFORMER ";
648 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
649 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
651 status.out << "\"\"" << endl;
654 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
655 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
658 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
659 status.out << " ISRC \"";
660 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
661 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
662 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
663 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
666 status.out << " }" << endl << "}" << endl;
668 frames_to_cd_frames_string (buf, status.track_position);
669 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
671 frames_to_cd_frames_string (buf, status.track_duration);
672 status.out << buf << endl;
674 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
675 status.out << "START" << buf << endl;
679 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
683 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
685 frames_to_cd_frames_string (buf, status.index_position);
686 status.out << buf << endl;
692 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
696 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
697 status.out << "INDEX" << buf << endl;
701 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
703 framecnt_t remainder;
704 framecnt_t fr = session.nominal_frame_rate();
705 int mins, secs, frames;
707 mins = when / (60 * fr);
708 remainder = when - (mins * 60 * fr);
709 secs = remainder / fr;
710 remainder -= secs * fr;
711 frames = remainder / (fr / 75);
712 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
716 ExportHandler::toc_escape_cdtext (const std::string& txt)
718 Glib::ustring check (txt);
720 std::string latin1_txt;
724 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
725 } catch (Glib::ConvertError& err) {
726 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
731 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
735 } else if ((*c) == '\\') {
737 } else if (isprint (*c)) {
740 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
751 ExportHandler::toc_escape_filename (const std::string& txt)
757 // We iterate byte-wise not character-wise over a UTF-8 string here,
758 // because we only want to translate backslashes and double quotes
759 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
763 } else if (*c == '\\') {
776 ExportHandler::cue_escape_cdtext (const std::string& txt)
778 std::string latin1_txt;
782 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
783 } catch (Glib::ConvertError& err) {
784 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
787 // does not do much mor than UTF-8 to Latin1 translation yet, but
788 // that may have to change if cue parsers in burning programs change
789 out = '"' + latin1_txt + '"';
794 } // namespace ARDOUR