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 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
368 if (soundcloud_open_page) {
369 std::cerr << "opening " << path << " ..." << std::endl;
370 open_uri(path.c_str()); // open the soundcloud website to the new file
373 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
375 delete soundcloud_uploader;
377 config_map.erase (config_map.begin());
383 /*** CD Marker stuff ***/
385 struct LocationSortByStart {
386 bool operator() (Location *a, Location *b) {
387 return a->start() < b->start();
392 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
393 std::string filename, CDMarkerFormat format)
395 string filepath = get_cd_marker_filename(filename, format);
398 void (ExportHandler::*header_func) (CDMarkerStatus &);
399 void (ExportHandler::*track_func) (CDMarkerStatus &);
400 void (ExportHandler::*index_func) (CDMarkerStatus &);
404 header_func = &ExportHandler::write_toc_header;
405 track_func = &ExportHandler::write_track_info_toc;
406 index_func = &ExportHandler::write_index_info_toc;
409 header_func = &ExportHandler::write_cue_header;
410 track_func = &ExportHandler::write_track_info_cue;
411 index_func = &ExportHandler::write_index_info_cue;
417 CDMarkerStatus status (filepath, timespan, file_format, filename);
420 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
424 (this->*header_func) (status);
426 /* Get locations and sort */
428 Locations::LocationList const & locations (session.locations()->list());
429 Locations::LocationList::const_iterator i;
430 Locations::LocationList temp;
432 for (i = locations.begin(); i != locations.end(); ++i) {
433 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
439 // TODO One index marker for whole thing
443 LocationSortByStart cmp;
445 Locations::LocationList::const_iterator nexti;
447 /* Start actual marker stuff */
449 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
450 status.track_position = last_start_time - timespan->get_start();
452 for (i = temp.begin(); i != temp.end(); ++i) {
456 if ((*i)->start() < last_end_time) {
457 if ((*i)->is_mark()) {
458 /* Index within track */
460 status.index_position = (*i)->start() - timespan->get_start();
461 (this->*index_func) (status);
467 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
469 status.track_position = last_end_time - timespan->get_start();
470 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
471 status.track_duration = 0;
473 if ((*i)->is_mark()) {
474 // a mark track location needs to look ahead to the next marker's start to determine length
478 if (nexti != temp.end()) {
479 status.track_duration = (*nexti)->start() - last_end_time;
481 last_start_time = (*i)->start();
482 last_end_time = (*nexti)->start();
484 // this was the last marker, use timespan end
485 status.track_duration = timespan->get_end() - last_end_time;
487 last_start_time = (*i)->start();
488 last_end_time = timespan->get_end();
492 status.track_duration = (*i)->end() - last_end_time;
494 last_start_time = (*i)->start();
495 last_end_time = (*i)->end();
498 (this->*track_func) (status);
501 } catch (std::exception& e) {
502 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
503 ::g_unlink (filepath.c_str());
504 } catch (Glib::Exception& e) {
505 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
506 ::g_unlink (filepath.c_str());
511 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
513 /* do not strip file suffix because there may be more than one format,
514 and we do not want the CD marker file from one format to overwrite
515 another (e.g. foo.wav.cue > foo.aiff.cue)
520 return filename + ".toc";
522 return filename + ".cue";
524 return filename + ".marker"; // Should not be reached when actually creating a file
529 ExportHandler::write_cue_header (CDMarkerStatus & status)
531 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
533 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
534 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
536 /* The original cue sheet sepc metions five file types
538 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
539 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
542 We try to use these file types whenever appropriate and
543 default to our own names otherwise.
545 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
546 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
547 status.out << "WAVE";
548 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
549 status.format->sample_format() == ExportFormatBase::SF_16 &&
550 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
551 // Format is RAW 16bit 44.1kHz
552 if (status.format->endianness() == ExportFormatBase::E_Little) {
553 status.out << "BINARY";
555 status.out << "MOTOROLA";
558 // no special case for AIFF format it's name is already "AIFF"
559 status.out << status.format->format_name();
565 ExportHandler::write_toc_header (CDMarkerStatus & status)
567 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
569 status.out << "CD_DA" << endl;
570 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
571 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
572 status.out << " PERFORMER \"\"" << endl << " }" << endl << "}" << endl;
576 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
580 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
581 status.out << buf << endl;
583 status.out << " FLAGS" ;
584 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
585 status.out << " SCMS ";
587 status.out << " DCP ";
590 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
591 status.out << " PRE";
595 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
596 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
599 if (status.marker->name() != "") {
600 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
603 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
604 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
607 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
608 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
611 if (status.track_position != status.track_start_frame) {
612 frames_to_cd_frames_string (buf, status.track_position);
613 status.out << " INDEX 00" << buf << endl;
616 frames_to_cd_frames_string (buf, status.track_start_frame);
617 status.out << " INDEX 01" << buf << endl;
619 status.index_number = 2;
620 status.track_number++;
624 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
628 status.out << endl << "TRACK AUDIO" << endl;
630 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
633 status.out << "COPY" << endl;
635 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
636 status.out << "PRE_EMPHASIS" << endl;
638 status.out << "NO PRE_EMPHASIS" << endl;
641 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
642 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
645 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
646 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
648 status.out << " PERFORMER ";
649 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
650 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
652 status.out << "\"\"" << endl;
655 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
656 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
659 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
660 status.out << " ISRC \"";
661 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
662 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
663 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
664 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
667 status.out << " }" << endl << "}" << endl;
669 frames_to_cd_frames_string (buf, status.track_position);
670 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
672 frames_to_cd_frames_string (buf, status.track_duration);
673 status.out << buf << endl;
675 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
676 status.out << "START" << buf << endl;
680 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
684 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
686 frames_to_cd_frames_string (buf, status.index_position);
687 status.out << buf << endl;
693 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
697 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
698 status.out << "INDEX" << buf << endl;
702 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
704 framecnt_t remainder;
705 framecnt_t fr = session.nominal_frame_rate();
706 int mins, secs, frames;
708 mins = when / (60 * fr);
709 remainder = when - (mins * 60 * fr);
710 secs = remainder / fr;
711 remainder -= secs * fr;
712 frames = remainder / (fr / 75);
713 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
717 ExportHandler::toc_escape_cdtext (const std::string& txt)
719 Glib::ustring check (txt);
721 std::string latin1_txt;
725 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
726 } catch (Glib::ConvertError& err) {
727 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
732 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
736 } else if ((*c) == '\\') {
738 } else if (isprint (*c)) {
741 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
752 ExportHandler::toc_escape_filename (const std::string& txt)
758 // We iterate byte-wise not character-wise over a UTF-8 string here,
759 // because we only want to translate backslashes and double quotes
760 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
764 } else if (*c == '\\') {
777 ExportHandler::cue_escape_cdtext (const std::string& txt)
779 std::string latin1_txt;
783 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
784 } catch (Glib::ConvertError& err) {
785 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
788 // does not do much mor than UTF-8 to Latin1 translation yet, but
789 // that may have to change if cue parsers in burning programs change
790 out = '"' + latin1_txt + '"';
795 } // namespace ARDOUR