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"
24 #include <glibmm/convert.h>
26 #include "pbd/convert.h"
28 #include "ardour/export_graph_builder.h"
29 #include "ardour/export_timespan.h"
30 #include "ardour/export_channel_configuration.h"
31 #include "ardour/export_status.h"
32 #include "ardour/export_format_specification.h"
33 #include "ardour/export_filename.h"
34 #include "ardour/soundcloud_upload.h"
35 #include "pbd/openuri.h"
36 #include "pbd/basename.h"
46 /*** ExportElementFactory ***/
48 ExportElementFactory::ExportElementFactory (Session & session) :
54 ExportElementFactory::~ExportElementFactory ()
60 ExportElementFactory::add_timespan ()
62 return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
65 ExportChannelConfigPtr
66 ExportElementFactory::add_channel_config ()
68 return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
72 ExportElementFactory::add_format ()
74 return ExportFormatSpecPtr (new ExportFormatSpecification (session));
78 ExportElementFactory::add_format (XMLNode const & state)
80 return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
84 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
86 return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
90 ExportElementFactory::add_filename ()
92 return ExportFilenamePtr (new ExportFilename (session));
96 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
98 return ExportFilenamePtr (new ExportFilename (*other));
101 /*** ExportHandler ***/
103 ExportHandler::ExportHandler (Session & session)
104 : ExportElementFactory (session)
106 , graph_builder (new ExportGraphBuilder (session))
107 , export_status (session.get_export_status ())
108 , normalizing (false)
114 ExportHandler::~ExportHandler ()
116 // TODO remove files that were written but not finished
119 /** Add an export to the `to-do' list */
121 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
122 ExportFormatSpecPtr format, ExportFilenamePtr filename,
123 BroadcastInfoPtr broadcast_info)
125 FileSpec spec (channel_config, format, filename, broadcast_info);
126 config_map.insert (make_pair (timespan, spec));
132 ExportHandler::do_export ()
134 /* Count timespans */
136 export_status->init();
137 std::set<ExportTimespanPtr> timespan_set;
138 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
139 bool new_timespan = timespan_set.insert (it->first).second;
141 export_status->total_frames += it->first->get_length();
144 export_status->total_timespans = timespan_set.size();
152 ExportHandler::start_timespan ()
154 export_status->timespan++;
156 if (config_map.empty()) {
157 // freewheeling has to be stopped from outside the process cycle
158 export_status->running = false;
162 /* finish_timespan pops the config_map entry that has been done, so
163 this is the timespan to do this time
165 current_timespan = config_map.begin()->first;
167 export_status->total_frames_current_timespan = current_timespan->get_length();
168 export_status->timespan_name = current_timespan->name();
169 export_status->processed_frames_current_timespan = 0;
171 /* Register file configurations to graph builder */
173 /* Here's the config_map entries that use this timespan */
174 timespan_bounds = config_map.equal_range (current_timespan);
175 graph_builder->reset ();
176 graph_builder->set_current_timespan (current_timespan);
177 handle_duplicate_format_extensions();
178 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
179 // Filenames can be shared across timespans
180 FileSpec & spec = it->second;
181 spec.filename->set_timespan (it->first);
182 graph_builder->add_config (spec);
188 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
189 process_position = current_timespan->get_start();
190 session.start_audio_export (process_position);
194 ExportHandler::handle_duplicate_format_extensions()
196 typedef std::map<std::string, int> ExtCountMap;
199 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
200 counts[it->second.format->extension()]++;
203 bool duplicates_found = false;
204 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
205 if (it->second > 1) { duplicates_found = true; }
208 // Set this always, as the filenames are shared...
209 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
210 it->second.filename->include_format_name = duplicates_found;
215 ExportHandler::process (framecnt_t frames)
217 if (!export_status->running) {
219 } else if (normalizing) {
220 return process_normalize ();
222 return process_timespan (frames);
227 ExportHandler::process_timespan (framecnt_t frames)
229 /* update position */
231 framecnt_t frames_to_read = 0;
232 framepos_t const end = current_timespan->get_end();
234 bool const last_cycle = (process_position + frames >= end);
237 frames_to_read = end - process_position;
238 export_status->stop = true;
240 frames_to_read = frames;
243 process_position += frames_to_read;
244 export_status->processed_frames += frames_to_read;
245 export_status->processed_frames_current_timespan += frames_to_read;
247 /* Do actual processing */
248 int ret = graph_builder->process (frames_to_read, last_cycle);
250 /* Start normalizing if necessary */
252 normalizing = graph_builder->will_normalize();
254 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
255 export_status->current_normalize_cycle = 0;
266 ExportHandler::process_normalize ()
268 if (graph_builder->process_normalize ()) {
270 export_status->normalizing = false;
272 export_status->normalizing = true;
275 export_status->current_normalize_cycle++;
281 ExportHandler::finish_timespan ()
283 while (config_map.begin() != timespan_bounds.second) {
285 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
286 std::string filepath = config_map.begin()->second.filename->get_path(fmt);
288 if (fmt->with_cue()) {
289 export_cd_marker_file (current_timespan, fmt, filepath, CDMarkerCUE);
292 if (fmt->with_toc()) {
293 export_cd_marker_file (current_timespan, fmt, filepath, CDMarkerTOC);
297 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
298 std::string token = soundcloud_uploader->Get_Auth_Token(upload_username, upload_password);
301 << filepath << std::endl
302 << "username = " << upload_username
303 << ", password = " << upload_password
304 << " - token = " << token << " ..."
306 std::string path = soundcloud_uploader->Upload (
308 PBD::basename_nosuffix(filepath), // title
313 if (path.length() != 0) {
315 std::cerr << "opening " << path << " ..." << std::endl;
316 open_uri(path.c_str()); // open the soundcloud website to the new file
319 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
321 delete soundcloud_uploader;
323 config_map.erase (config_map.begin());
329 /*** CD Marker stuff ***/
331 struct LocationSortByStart {
332 bool operator() (Location *a, Location *b) {
333 return a->start() < b->start();
338 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
339 std::string filename, CDMarkerFormat format)
341 string filepath = get_cd_marker_filename(filename, format);
344 void (ExportHandler::*header_func) (CDMarkerStatus &);
345 void (ExportHandler::*track_func) (CDMarkerStatus &);
346 void (ExportHandler::*index_func) (CDMarkerStatus &);
350 header_func = &ExportHandler::write_toc_header;
351 track_func = &ExportHandler::write_track_info_toc;
352 index_func = &ExportHandler::write_index_info_toc;
355 header_func = &ExportHandler::write_cue_header;
356 track_func = &ExportHandler::write_track_info_cue;
357 index_func = &ExportHandler::write_index_info_cue;
363 CDMarkerStatus status (filepath, timespan, file_format, filename);
366 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
370 (this->*header_func) (status);
372 /* Get locations and sort */
374 Locations::LocationList const & locations (session.locations()->list());
375 Locations::LocationList::const_iterator i;
376 Locations::LocationList temp;
378 for (i = locations.begin(); i != locations.end(); ++i) {
379 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
385 // TODO One index marker for whole thing
389 LocationSortByStart cmp;
391 Locations::LocationList::const_iterator nexti;
393 /* Start actual marker stuff */
395 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
396 status.track_position = last_start_time - timespan->get_start();
398 for (i = temp.begin(); i != temp.end(); ++i) {
402 if ((*i)->start() < last_end_time) {
403 if ((*i)->is_mark()) {
404 /* Index within track */
406 status.index_position = (*i)->start() - timespan->get_start();
407 (this->*index_func) (status);
413 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
415 status.track_position = last_end_time - timespan->get_start();
416 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
417 status.track_duration = 0;
419 if ((*i)->is_mark()) {
420 // a mark track location needs to look ahead to the next marker's start to determine length
424 if (nexti != temp.end()) {
425 status.track_duration = (*nexti)->start() - last_end_time;
427 last_start_time = (*i)->start();
428 last_end_time = (*nexti)->start();
430 // this was the last marker, use timespan end
431 status.track_duration = timespan->get_end() - last_end_time;
433 last_start_time = (*i)->start();
434 last_end_time = timespan->get_end();
438 status.track_duration = (*i)->end() - last_end_time;
440 last_start_time = (*i)->start();
441 last_end_time = (*i)->end();
444 (this->*track_func) (status);
447 } catch (std::exception& e) {
448 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
449 ::unlink (filepath.c_str());
450 } catch (Glib::Exception& e) {
451 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
452 ::unlink (filepath.c_str());
457 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
459 /* do not strip file suffix because there may be more than one format,
460 and we do not want the CD marker file from one format to overwrite
461 another (e.g. foo.wav.cue > foo.aiff.cue)
466 return filename + ".toc";
468 return filename + ".cue";
470 return filename + ".marker"; // Should not be reached when actually creating a file
475 ExportHandler::write_cue_header (CDMarkerStatus & status)
477 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
479 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
480 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
482 /* The original cue sheet sepc metions five file types
484 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
485 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
488 We try to use these file types whenever appropriate and
489 default to our own names otherwise.
491 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
492 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
493 status.out << "WAVE";
494 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
495 status.format->sample_format() == ExportFormatBase::SF_16 &&
496 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
497 // Format is RAW 16bit 44.1kHz
498 if (status.format->endianness() == ExportFormatBase::E_Little) {
499 status.out << "BINARY";
501 status.out << "MOTOROLA";
504 // no special case for AIFF format it's name is already "AIFF"
505 status.out << status.format->format_name();
511 ExportHandler::write_toc_header (CDMarkerStatus & status)
513 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
515 status.out << "CD_DA" << endl;
516 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
517 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
518 status.out << " PERFORMER \"\"" << endl << " }" << endl << "}" << endl;
522 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
526 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
527 status.out << buf << endl;
529 status.out << " FLAGS" ;
530 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
531 status.out << " SCMS ";
533 status.out << " DCP ";
536 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
537 status.out << " PRE";
541 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
542 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
545 if (status.marker->name() != "") {
546 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
549 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
550 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
553 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
554 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
557 if (status.track_position != status.track_start_frame) {
558 frames_to_cd_frames_string (buf, status.track_position);
559 status.out << " INDEX 00" << buf << endl;
562 frames_to_cd_frames_string (buf, status.track_start_frame);
563 status.out << " INDEX 01" << buf << endl;
565 status.index_number = 2;
566 status.track_number++;
570 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
574 status.out << endl << "TRACK AUDIO" << endl;
576 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
579 status.out << "COPY" << endl;
581 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
582 status.out << "PRE_EMPHASIS" << endl;
584 status.out << "NO PRE_EMPHASIS" << endl;
587 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
588 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
591 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
592 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
594 status.out << " PERFORMER ";
595 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
596 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
598 status.out << "\"\"" << endl;
601 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
602 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
605 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
606 status.out << " ISRC \"";
607 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
608 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
609 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
610 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
613 status.out << " }" << endl << "}" << endl;
615 frames_to_cd_frames_string (buf, status.track_position);
616 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
618 frames_to_cd_frames_string (buf, status.track_duration);
619 status.out << buf << endl;
621 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
622 status.out << "START" << buf << endl;
626 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
630 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
632 frames_to_cd_frames_string (buf, status.index_position);
633 status.out << buf << endl;
639 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
643 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
644 status.out << "INDEX" << buf << endl;
648 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
650 framecnt_t remainder;
651 framecnt_t fr = session.nominal_frame_rate();
652 int mins, secs, frames;
654 mins = when / (60 * fr);
655 remainder = when - (mins * 60 * fr);
656 secs = remainder / fr;
657 remainder -= secs * fr;
658 frames = remainder / (fr / 75);
659 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
663 ExportHandler::toc_escape_cdtext (const std::string& txt)
665 Glib::ustring check (txt);
667 std::string latin1_txt;
671 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
672 } catch (Glib::ConvertError& err) {
673 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
678 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
682 } else if ((*c) == '\\') {
684 } else if (isprint (*c)) {
687 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
698 ExportHandler::toc_escape_filename (const std::string& txt)
704 // We iterate byte-wise not character-wise over a UTF-8 string here,
705 // because we only want to translate backslashes and double quotes
706 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
710 } else if (*c == '\\') {
723 ExportHandler::cue_escape_cdtext (const std::string& txt)
725 std::string latin1_txt;
729 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
730 } catch (Glib::ConvertError& err) {
731 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
734 // does not do much mor than UTF-8 to Latin1 translation yet, but
735 // that may have to change if cue parsers in burning programs change
736 out = '"' + latin1_txt + '"';
741 } // namespace ARDOUR