/* Copyright (C) 2008 Paul Davis Author: Sakari Bergen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ #include "ardour/export_handler.h" #include #include "pbd/convert.h" #include "pbd/filesystem.h" #include "ardour/ardour.h" #include "ardour/configuration.h" #include "ardour/export_timespan.h" #include "ardour/export_channel_configuration.h" #include "ardour/export_status.h" #include "ardour/export_format_specification.h" #include "ardour/export_filename.h" #include "ardour/export_processor.h" #include "ardour/export_failed.h" using namespace std; using namespace PBD; namespace ARDOUR { /*** ExportElementFactory ***/ ExportElementFactory::ExportElementFactory (Session & session) : session (session) { } ExportElementFactory::~ExportElementFactory () { } ExportElementFactory::TimespanPtr ExportElementFactory::add_timespan () { return TimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate())); } ExportElementFactory::ChannelConfigPtr ExportElementFactory::add_channel_config () { return ChannelConfigPtr (new ExportChannelConfiguration (session)); } ExportElementFactory::FormatPtr ExportElementFactory::add_format () { return FormatPtr (new ExportFormatSpecification (session)); } ExportElementFactory::FormatPtr ExportElementFactory::add_format (XMLNode const & state) { return FormatPtr (new ExportFormatSpecification (session, state)); } ExportElementFactory::FormatPtr ExportElementFactory::add_format_copy (FormatPtr other) { return FormatPtr (new ExportFormatSpecification (*other)); } ExportElementFactory::FilenamePtr ExportElementFactory::add_filename () { return FilenamePtr (new ExportFilename (session)); } ExportElementFactory::FilenamePtr ExportElementFactory::add_filename_copy (FilenamePtr other) { return FilenamePtr (new ExportFilename (*other)); } /*** ExportHandler ***/ ExportHandler::ExportHandler (Session & session) : ExportElementFactory (session), session (session), export_status (session.get_export_status ()), realtime (false) { processor.reset (new ExportProcessor (session)); files_written_connection = ExportProcessor::WritingFile.connect (sigc::mem_fun (files_written, &std::list::push_back)); } ExportHandler::~ExportHandler () { if (export_status->aborted()) { for (std::list::iterator it = files_written.begin(); it != files_written.end(); ++it) { sys::remove (sys::path (*it)); } } channel_config_connection.disconnect(); files_written_connection.disconnect(); } bool ExportHandler::add_export_config (TimespanPtr timespan, ChannelConfigPtr channel_config, FormatPtr format, FilenamePtr filename) { FileSpec spec (channel_config, format, filename); ConfigPair pair (timespan, spec); config_map.insert (pair); return true; } /// Starts exporting the registered configurations /** The following happens, when do_export is called: * 1. Session is prepared in do_export * 2. start_timespan is called, which then registers all necessary channel configs to a timespan * 3. The timespan reads each unique channel into a tempfile and calls Session::stop_export when the end is reached * 4. stop_export emits ExportReadFinished after stopping the transport, this ends up calling finish_timespan * 5. finish_timespan registers all the relevant formats and filenames to relevant channel configurations * 6. finish_timespan does a manual call to timespan_thread_finished, which gets the next channel configuration * for the current timespan, calling write_files for it * 7. write_files writes the actual export files, composing them from the individual channels from tempfiles and * emits FilesWritten when it is done, which ends up calling timespan_thread_finished * 8. Once all channel configs are written, a new timespan is started by calling start_timespan * 9. When all timespans are written the session is taken out of export. */ void ExportHandler::do_export (bool rt) { /* Count timespans */ export_status->init(); std::set timespan_set; for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) { timespan_set.insert (it->first); } export_status->total_timespans = timespan_set.size(); /* Start export */ realtime = rt; session.ExportReadFinished.connect (sigc::mem_fun (*this, &ExportHandler::finish_timespan)); start_timespan (); } struct LocationSortByStart { bool operator() (Location *a, Location *b) { return a->start() < b->start(); } }; void ExportHandler::export_cd_marker_file (TimespanPtr timespan, FormatPtr file_format, std::string filename, CDMarkerFormat format) { string filepath; string basename = Glib::path_get_basename(filename); size_t ext_pos = basename.rfind('.'); if (ext_pos != string::npos) { basename = basename.substr(0, ext_pos); /* strip file extension, if there is one */ } void (ExportHandler::*header_func) (CDMarkerStatus &); void (ExportHandler::*track_func) (CDMarkerStatus &); void (ExportHandler::*index_func) (CDMarkerStatus &); switch (format) { case CDMarkerTOC: filepath = Glib::build_filename(Glib::path_get_dirname(filename), basename + ".toc"); header_func = &ExportHandler::write_toc_header; track_func = &ExportHandler::write_track_info_toc; index_func = &ExportHandler::write_index_info_toc; break; case CDMarkerCUE: filepath = Glib::build_filename(Glib::path_get_dirname(filename), basename + ".cue"); header_func = &ExportHandler::write_cue_header; track_func = &ExportHandler::write_track_info_cue; index_func = &ExportHandler::write_index_info_cue; break; default: return; } CDMarkerStatus status (filepath, timespan, file_format, filename); if (!status.out) { error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg; return; } (this->*header_func) (status); /* Get locations and sort */ Locations::LocationList const & locations (session.locations()->list()); Locations::LocationList::const_iterator i; Locations::LocationList temp; for (i = locations.begin(); i != locations.end(); ++i) { if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_end()) { temp.push_back (*i); } } if (temp.empty()) { // TODO One index marker for whole thing return; } LocationSortByStart cmp; temp.sort (cmp); Locations::LocationList::const_iterator nexti; /* Start actual marker stuff */ nframes_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start(); status.track_position = last_start_time - timespan->get_start(); for (i = temp.begin(); i != temp.end(); ++i) { status.marker = *i; if ((*i)->start() < last_end_time) { if ((*i)->is_mark()) { /* Index within track */ status.index_position = (*i)->start() - timespan->get_start(); (this->*index_func) (status); } continue; } /* A track, defined by a cd range marker or a cd location marker outside of a cd range */ status.track_position = last_end_time - timespan->get_start(); status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap status.track_duration = 0; if ((*i)->is_mark()) { // a mark track location needs to look ahead to the next marker's start to determine length nexti = i; ++nexti; if (nexti != temp.end()) { status.track_duration = (*nexti)->start() - last_end_time; last_start_time = (*i)->start(); last_end_time = (*nexti)->start(); } else { // this was the last marker, use timespan end status.track_duration = timespan->get_end() - last_end_time; last_start_time = (*i)->start(); last_end_time = timespan->get_end(); } } else { // range status.track_duration = (*i)->end() - last_end_time; last_start_time = (*i)->start(); last_end_time = (*i)->end(); } (this->*track_func) (status); } } void ExportHandler::write_cue_header (CDMarkerStatus & status) { Glib::ustring title = status.timespan->name().compare ("Session") ? status.timespan->name() : (Glib::ustring) session.name(); status.out << "REM Cue file generated by Ardour" << endl; status.out << "TITLE \"" << title << "\"" << endl; /* The cue sheet syntax has originally five file types: WAVE : 44.1 kHz, 16 Bit (little endian) AIFF : 44.1 kHz, 16 Bit (big endian) BINARY : 44.1 kHz, 16 Bit (little endian) MOTOROLA : 44.1 kHz, 16 Bit (big endian) MP3 We want to use cue sheets not only as CD images but also as general playlyist format, thus for WAVE and AIFF we don't care if it's really 44.1 kHz/16 Bit, the soundfile's header shows it anyway. But for the raw formats, i.e. BINARY and MOTOROLA we do care, because no header would tell us about a different format. For all other formats we just make up our own file type. MP3 is not supported at the moment. */ status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" "; if (!status.format->format_name().compare ("WAV")) { status.out << "WAVE"; } else if (status.format->format_name() == ExportFormatBase::F_RAW && status.format->sample_format() == ExportFormatBase::SF_16 && status.format->sample_rate() == ExportFormatBase::SR_44_1) { // Format is RAW 16bit 44.1kHz if (status.format->endianness() == ExportFormatBase::E_Little) { status.out << "BINARY"; } else { status.out << "MOTOROLA"; } } else { // AIFF should return "AIFF" status.out << status.format->format_name(); } status.out << endl; } void ExportHandler::write_toc_header (CDMarkerStatus & status) { Glib::ustring title = status.timespan->name().compare ("Session") ? status.timespan->name() : (Glib::ustring) session.name(); status.out << "CD_DA" << endl; status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl; status.out << " LANGUAGE 0 {" << endl << " TITLE \"" << title << "\"" << endl << " }" << endl << "}" << endl; } void ExportHandler::write_track_info_cue (CDMarkerStatus & status) { gchar buf[18]; snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number); status.out << buf << endl; status.out << " FLAGS" ; if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) { status.out << " SCMS "; } else { status.out << " DCP "; } if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) { status.out << " PRE"; } status.out << endl; if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) { status.out << " ISRC " << status.marker->cd_info["isrc"] << endl; } if (status.marker->name() != "") { status.out << " TITLE \"" << status.marker->name() << "\"" << endl; } if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) { status.out << " PERFORMER \"" << status.marker->cd_info["performer"] << "\"" << endl; } if (status.marker->cd_info.find("string_composer") != status.marker->cd_info.end()) { status.out << " SONGWRITER \"" << status.marker->cd_info["string_composer"] << "\"" << endl; } if (status.track_position != status.track_start_frame) { frames_to_cd_frames_string (buf, status.track_position); status.out << " INDEX 00" << buf << endl; } frames_to_cd_frames_string (buf, status.track_start_frame); status.out << " INDEX 01" << buf << endl; status.index_number = 2; status.track_number++; } void ExportHandler::write_track_info_toc (CDMarkerStatus & status) { gchar buf[18]; status.out << endl << "TRACK AUDIO" << endl; if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) { status.out << "NO "; } status.out << "COPY" << endl; if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) { status.out << "PRE_EMPHASIS" << endl; } else { status.out << "NO PRE_EMPHASIS" << endl; } if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) { status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl; } status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl << " TITLE \"" << status.marker->name() << "\"" << endl; if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) { status.out << " PERFORMER \"" << status.marker->cd_info["performer"] << "\"" << endl; } if (status.marker->cd_info.find("string_composer") != status.marker->cd_info.end()) { status.out << " COMPOSER \"" << status.marker->cd_info["string_composer"] << "\"" << endl; } if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) { status.out << " ISRC \""; status.out << status.marker->cd_info["isrc"].substr(0,2) << "-"; status.out << status.marker->cd_info["isrc"].substr(2,3) << "-"; status.out << status.marker->cd_info["isrc"].substr(5,2) << "-"; status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl; } status.out << " }" << endl << "}" << endl; frames_to_cd_frames_string (buf, status.track_position); status.out << "FILE \"" << status.filename << "\" " << buf; frames_to_cd_frames_string (buf, status.track_duration); status.out << buf << endl; frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position); status.out << "START" << buf << endl; } void ExportHandler::write_index_info_cue (CDMarkerStatus & status) { gchar buf[18]; snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum); status.out << buf; frames_to_cd_frames_string (buf, status.index_position); status.out << buf << endl; cue_indexnum++; } void ExportHandler::write_index_info_toc (CDMarkerStatus & status) { gchar buf[18]; frames_to_cd_frames_string (buf, status.index_position - status.track_position); status.out << "INDEX" << buf << endl; } void ExportHandler::frames_to_cd_frames_string (char* buf, nframes_t when) { nframes_t remainder; nframes_t fr = session.nominal_frame_rate(); int mins, secs, frames; mins = when / (60 * fr); remainder = when - (mins * 60 * fr); secs = remainder / fr; remainder -= secs * fr; frames = remainder / (fr / 75); sprintf (buf, " %02d:%02d:%02d", mins, secs, frames); } void ExportHandler::start_timespan () { export_status->timespan++; if (config_map.empty()) { export_status->finish (); return; } current_timespan = config_map.begin()->first; /* Register channel configs with timespan */ timespan_bounds = config_map.equal_range (current_timespan); for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) { it->second.channel_config->register_with_timespan (current_timespan); } /* connect stuff and start export */ current_timespan->process_connection = session.ProcessExport.connect (sigc::mem_fun (*current_timespan, &ExportTimespan::process)); session.start_audio_export (current_timespan->get_start(), realtime); } void ExportHandler::finish_timespan () { current_timespan->process_connection.disconnect (); /* Register formats and filenames to relevant channel configs */ export_status->total_formats = 0; export_status->format = 0; for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) { export_status->total_formats++; /* Setup filename */ it->second.filename->set_timespan (current_timespan); it->second.filename->set_channel_config (it->second.channel_config); /* Do actual registration */ ChannelConfigPtr chan_config = it->second.channel_config; chan_config->register_file_config (it->second.format, it->second.filename); } /* Start writing files by doing a manual call to timespan_thread_finished */ current_map_it = timespan_bounds.first; timespan_thread_finished (); } void ExportHandler::timespan_thread_finished () { channel_config_connection.disconnect(); if (current_map_it != timespan_bounds.second) { /* Get next configuration as long as no new export process is started */ ChannelConfigPtr cc = current_map_it->second.channel_config; while (!cc->write_files(processor)) { ++current_map_it; if (current_map_it == timespan_bounds.second) { /* reached end of bounds, this call will end up in the else block below */ timespan_thread_finished (); return; } cc = current_map_it->second.channel_config; } channel_config_connection = cc->FilesWritten.connect (sigc::mem_fun (*this, &ExportHandler::timespan_thread_finished)); ++current_map_it; } else { /* All files are written from current timespan, reset timespan and start new */ /* Unregister configs and remove configs with this timespan */ for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second;) { it->second.channel_config->unregister_all (); ConfigMap::iterator to_erase = it; ++it; config_map.erase (to_erase); } /* Start new timespan */ start_timespan (); } } } // namespace ARDOUR