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/audiofile_tagger.h"
29 #include "ardour/export_graph_builder.h"
30 #include "ardour/export_timespan.h"
31 #include "ardour/export_channel_configuration.h"
32 #include "ardour/export_status.h"
33 #include "ardour/export_format_specification.h"
34 #include "ardour/export_filename.h"
35 #include "ardour/soundcloud_upload.h"
36 #include "ardour/system_exec.h"
37 #include "pbd/openuri.h"
38 #include "pbd/basename.h"
39 #include "ardour/session_metadata.h"
49 /*** ExportElementFactory ***/
51 ExportElementFactory::ExportElementFactory (Session & session) :
57 ExportElementFactory::~ExportElementFactory ()
63 ExportElementFactory::add_timespan ()
65 return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
68 ExportChannelConfigPtr
69 ExportElementFactory::add_channel_config ()
71 return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
75 ExportElementFactory::add_format ()
77 return ExportFormatSpecPtr (new ExportFormatSpecification (session));
81 ExportElementFactory::add_format (XMLNode const & state)
83 return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
87 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
89 return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
93 ExportElementFactory::add_filename ()
95 return ExportFilenamePtr (new ExportFilename (session));
99 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
101 return ExportFilenamePtr (new ExportFilename (*other));
104 /*** ExportHandler ***/
106 ExportHandler::ExportHandler (Session & session)
107 : ExportElementFactory (session)
109 , graph_builder (new ExportGraphBuilder (session))
110 , export_status (session.get_export_status ())
111 , normalizing (false)
117 ExportHandler::~ExportHandler ()
119 // TODO remove files that were written but not finished
122 /** Add an export to the `to-do' list */
124 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
125 ExportFormatSpecPtr format, ExportFilenamePtr filename,
126 BroadcastInfoPtr broadcast_info)
128 FileSpec spec (channel_config, format, filename, broadcast_info);
129 config_map.insert (make_pair (timespan, spec));
135 ExportHandler::do_export ()
137 /* Count timespans */
139 export_status->init();
140 std::set<ExportTimespanPtr> timespan_set;
141 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
142 bool new_timespan = timespan_set.insert (it->first).second;
144 export_status->total_frames += it->first->get_length();
147 export_status->total_timespans = timespan_set.size();
155 ExportHandler::start_timespan ()
157 export_status->timespan++;
159 if (config_map.empty()) {
160 // freewheeling has to be stopped from outside the process cycle
161 export_status->running = false;
165 /* finish_timespan pops the config_map entry that has been done, so
166 this is the timespan to do this time
168 current_timespan = config_map.begin()->first;
170 export_status->total_frames_current_timespan = current_timespan->get_length();
171 export_status->timespan_name = current_timespan->name();
172 export_status->processed_frames_current_timespan = 0;
174 /* Register file configurations to graph builder */
176 /* Here's the config_map entries that use this timespan */
177 timespan_bounds = config_map.equal_range (current_timespan);
178 graph_builder->reset ();
179 graph_builder->set_current_timespan (current_timespan);
180 handle_duplicate_format_extensions();
181 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
182 // Filenames can be shared across timespans
183 FileSpec & spec = it->second;
184 spec.filename->set_timespan (it->first);
185 graph_builder->add_config (spec);
191 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
192 process_position = current_timespan->get_start();
193 session.start_audio_export (process_position);
197 ExportHandler::handle_duplicate_format_extensions()
199 typedef std::map<std::string, int> ExtCountMap;
202 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
203 counts[it->second.format->extension()]++;
206 bool duplicates_found = false;
207 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
208 if (it->second > 1) { duplicates_found = true; }
211 // Set this always, as the filenames are shared...
212 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
213 it->second.filename->include_format_name = duplicates_found;
218 ExportHandler::process (framecnt_t frames)
220 if (!export_status->running) {
222 } else if (normalizing) {
223 return process_normalize ();
225 return process_timespan (frames);
230 ExportHandler::process_timespan (framecnt_t frames)
232 /* update position */
234 framecnt_t frames_to_read = 0;
235 framepos_t const end = current_timespan->get_end();
237 bool const last_cycle = (process_position + frames >= end);
240 frames_to_read = end - process_position;
241 export_status->stop = true;
243 frames_to_read = frames;
246 process_position += frames_to_read;
247 export_status->processed_frames += frames_to_read;
248 export_status->processed_frames_current_timespan += frames_to_read;
250 /* Do actual processing */
251 int ret = graph_builder->process (frames_to_read, last_cycle);
253 /* Start normalizing if necessary */
255 normalizing = graph_builder->will_normalize();
257 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
258 export_status->current_normalize_cycle = 0;
269 ExportHandler::process_normalize ()
271 if (graph_builder->process_normalize ()) {
273 export_status->normalizing = false;
275 export_status->normalizing = true;
278 export_status->current_normalize_cycle++;
284 ExportHandler::command_output(std::string output, size_t size)
286 std::cerr << "command: " << size << ", " << output << std::endl;
287 info << output << endmsg;
291 ExportHandler::finish_timespan ()
293 while (config_map.begin() != timespan_bounds.second) {
295 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
296 std::string filename = config_map.begin()->second.filename->get_path(fmt);
298 if (fmt->with_cue()) {
299 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
302 if (fmt->with_toc()) {
303 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
307 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
310 if (!fmt->command().empty()) {
312 #if 0 // would be nicer with C++11 initialiser...
313 std::map<char, std::string> subs {
315 { 'd', Glib::path_get_dirname(filename) },
316 { 'b', PBD::basename_nosuffix(filename) },
317 { 'u', upload_username },
318 { 'p', upload_password}
322 PBD::ScopedConnection command_connection;
323 std::map<char, std::string> subs;
324 subs.insert (std::pair<char, std::string> ('f', filename));
325 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname(filename)));
326 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix(filename)));
327 subs.insert (std::pair<char, std::string> ('u', upload_username));
328 subs.insert (std::pair<char, std::string> ('p', upload_password));
331 std::cerr << "running command: " << fmt->command() << "..." << std::endl;
332 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
333 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
334 if (se->start (2) == 0) {
335 // successfully started
336 std::cerr << "started!" << std::endl;
337 while (se->is_running ()) {
338 // wait for system exec to terminate
339 // std::cerr << "waiting..." << std::endl;
343 std::cerr << "done! deleting..." << std::endl;
348 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
349 std::string token = soundcloud_uploader->Get_Auth_Token(upload_username, upload_password);
352 << filename << std::endl
353 << "username = " << upload_username
354 << ", password = " << upload_password
355 << " - token = " << token << " ..."
357 std::string path = soundcloud_uploader->Upload (
359 PBD::basename_nosuffix(filename), // title
364 if (path.length() != 0) {
366 std::cerr << "opening " << path << " ..." << std::endl;
367 open_uri(path.c_str()); // open the soundcloud website to the new file
370 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
372 delete soundcloud_uploader;
374 config_map.erase (config_map.begin());
380 /*** CD Marker stuff ***/
382 struct LocationSortByStart {
383 bool operator() (Location *a, Location *b) {
384 return a->start() < b->start();
389 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
390 std::string filename, CDMarkerFormat format)
392 string filepath = get_cd_marker_filename(filename, format);
395 void (ExportHandler::*header_func) (CDMarkerStatus &);
396 void (ExportHandler::*track_func) (CDMarkerStatus &);
397 void (ExportHandler::*index_func) (CDMarkerStatus &);
401 header_func = &ExportHandler::write_toc_header;
402 track_func = &ExportHandler::write_track_info_toc;
403 index_func = &ExportHandler::write_index_info_toc;
406 header_func = &ExportHandler::write_cue_header;
407 track_func = &ExportHandler::write_track_info_cue;
408 index_func = &ExportHandler::write_index_info_cue;
414 CDMarkerStatus status (filepath, timespan, file_format, filename);
417 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
421 (this->*header_func) (status);
423 /* Get locations and sort */
425 Locations::LocationList const & locations (session.locations()->list());
426 Locations::LocationList::const_iterator i;
427 Locations::LocationList temp;
429 for (i = locations.begin(); i != locations.end(); ++i) {
430 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
436 // TODO One index marker for whole thing
440 LocationSortByStart cmp;
442 Locations::LocationList::const_iterator nexti;
444 /* Start actual marker stuff */
446 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
447 status.track_position = last_start_time - timespan->get_start();
449 for (i = temp.begin(); i != temp.end(); ++i) {
453 if ((*i)->start() < last_end_time) {
454 if ((*i)->is_mark()) {
455 /* Index within track */
457 status.index_position = (*i)->start() - timespan->get_start();
458 (this->*index_func) (status);
464 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
466 status.track_position = last_end_time - timespan->get_start();
467 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
468 status.track_duration = 0;
470 if ((*i)->is_mark()) {
471 // a mark track location needs to look ahead to the next marker's start to determine length
475 if (nexti != temp.end()) {
476 status.track_duration = (*nexti)->start() - last_end_time;
478 last_start_time = (*i)->start();
479 last_end_time = (*nexti)->start();
481 // this was the last marker, use timespan end
482 status.track_duration = timespan->get_end() - last_end_time;
484 last_start_time = (*i)->start();
485 last_end_time = timespan->get_end();
489 status.track_duration = (*i)->end() - last_end_time;
491 last_start_time = (*i)->start();
492 last_end_time = (*i)->end();
495 (this->*track_func) (status);
498 } catch (std::exception& e) {
499 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
500 ::unlink (filepath.c_str());
501 } catch (Glib::Exception& e) {
502 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
503 ::unlink (filepath.c_str());
508 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
510 /* do not strip file suffix because there may be more than one format,
511 and we do not want the CD marker file from one format to overwrite
512 another (e.g. foo.wav.cue > foo.aiff.cue)
517 return filename + ".toc";
519 return filename + ".cue";
521 return filename + ".marker"; // Should not be reached when actually creating a file
526 ExportHandler::write_cue_header (CDMarkerStatus & status)
528 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
530 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
531 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
533 /* The original cue sheet sepc metions five file types
535 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
536 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
539 We try to use these file types whenever appropriate and
540 default to our own names otherwise.
542 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
543 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
544 status.out << "WAVE";
545 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
546 status.format->sample_format() == ExportFormatBase::SF_16 &&
547 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
548 // Format is RAW 16bit 44.1kHz
549 if (status.format->endianness() == ExportFormatBase::E_Little) {
550 status.out << "BINARY";
552 status.out << "MOTOROLA";
555 // no special case for AIFF format it's name is already "AIFF"
556 status.out << status.format->format_name();
562 ExportHandler::write_toc_header (CDMarkerStatus & status)
564 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
566 status.out << "CD_DA" << endl;
567 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
568 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
569 status.out << " PERFORMER \"\"" << endl << " }" << endl << "}" << endl;
573 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
577 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
578 status.out << buf << endl;
580 status.out << " FLAGS" ;
581 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
582 status.out << " SCMS ";
584 status.out << " DCP ";
587 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
588 status.out << " PRE";
592 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
593 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
596 if (status.marker->name() != "") {
597 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
600 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
601 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
604 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
605 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
608 if (status.track_position != status.track_start_frame) {
609 frames_to_cd_frames_string (buf, status.track_position);
610 status.out << " INDEX 00" << buf << endl;
613 frames_to_cd_frames_string (buf, status.track_start_frame);
614 status.out << " INDEX 01" << buf << endl;
616 status.index_number = 2;
617 status.track_number++;
621 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
625 status.out << endl << "TRACK AUDIO" << endl;
627 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
630 status.out << "COPY" << endl;
632 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
633 status.out << "PRE_EMPHASIS" << endl;
635 status.out << "NO PRE_EMPHASIS" << endl;
638 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
639 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
642 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
643 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
645 status.out << " PERFORMER ";
646 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
647 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
649 status.out << "\"\"" << endl;
652 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
653 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
656 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
657 status.out << " ISRC \"";
658 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
659 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
660 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
661 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
664 status.out << " }" << endl << "}" << endl;
666 frames_to_cd_frames_string (buf, status.track_position);
667 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
669 frames_to_cd_frames_string (buf, status.track_duration);
670 status.out << buf << endl;
672 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
673 status.out << "START" << buf << endl;
677 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
681 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
683 frames_to_cd_frames_string (buf, status.index_position);
684 status.out << buf << endl;
690 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
694 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
695 status.out << "INDEX" << buf << endl;
699 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
701 framecnt_t remainder;
702 framecnt_t fr = session.nominal_frame_rate();
703 int mins, secs, frames;
705 mins = when / (60 * fr);
706 remainder = when - (mins * 60 * fr);
707 secs = remainder / fr;
708 remainder -= secs * fr;
709 frames = remainder / (fr / 75);
710 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
714 ExportHandler::toc_escape_cdtext (const std::string& txt)
716 Glib::ustring check (txt);
718 std::string latin1_txt;
722 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
723 } catch (Glib::ConvertError& err) {
724 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
729 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
733 } else if ((*c) == '\\') {
735 } else if (isprint (*c)) {
738 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
749 ExportHandler::toc_escape_filename (const std::string& txt)
755 // We iterate byte-wise not character-wise over a UTF-8 string here,
756 // because we only want to translate backslashes and double quotes
757 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
761 } else if (*c == '\\') {
774 ExportHandler::cue_escape_cdtext (const std::string& txt)
776 std::string latin1_txt;
780 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
781 } catch (Glib::ConvertError& err) {
782 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
785 // does not do much mor than UTF-8 to Latin1 translation yet, but
786 // that may have to change if cue parsers in burning programs change
787 out = '"' + latin1_txt + '"';
792 } // namespace ARDOUR