Robin Gareus
57118c2370
This cuts reverb tails and synth sounds after export. Disabling freewheeling, continues normal processing where export left off. This previously kept notes ringing, or reverbs audible.
995 lines
30 KiB
C++
995 lines
30 KiB
C++
/*
|
|
* Copyright (C) 2008-2013 Sakari Bergen <sakari.bergen@beatwaves.net>
|
|
* Copyright (C) 2008-2017 Paul Davis <paul@linuxaudiosystems.com>
|
|
* Copyright (C) 2009-2012 Carl Hetherington <carl@carlh.net>
|
|
* Copyright (C) 2009-2014 David Robillard <d@drobilla.net>
|
|
* Copyright (C) 2013-2015 Colin Fletcher <colin.m.fletcher@googlemail.com>
|
|
* Copyright (C) 2015-2019 Robin Gareus <robin@gareus.org>
|
|
* Copyright (C) 2015 Johannes Mueller <github@johannes-mueller.org>
|
|
*
|
|
* 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.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
#include "pbd/gstdio_compat.h"
|
|
#include <glibmm.h>
|
|
#include <glibmm/convert.h>
|
|
|
|
#include "pbd/convert.h"
|
|
|
|
#include "ardour/audioengine.h"
|
|
#include "ardour/audiofile_tagger.h"
|
|
#include "ardour/audio_port.h"
|
|
#include "ardour/debug.h"
|
|
#include "ardour/export_graph_builder.h"
|
|
#include "ardour/export_handler.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/soundcloud_upload.h"
|
|
#include "ardour/system_exec.h"
|
|
#include "pbd/openuri.h"
|
|
#include "pbd/basename.h"
|
|
#include "ardour/session_metadata.h"
|
|
|
|
#include "pbd/i18n.h"
|
|
|
|
using namespace std;
|
|
using namespace PBD;
|
|
|
|
namespace ARDOUR
|
|
{
|
|
|
|
/*** ExportElementFactory ***/
|
|
|
|
ExportElementFactory::ExportElementFactory (Session & session) :
|
|
session (session)
|
|
{
|
|
|
|
}
|
|
|
|
ExportElementFactory::~ExportElementFactory ()
|
|
{
|
|
|
|
}
|
|
|
|
ExportTimespanPtr
|
|
ExportElementFactory::add_timespan ()
|
|
{
|
|
return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.sample_rate()));
|
|
}
|
|
|
|
ExportChannelConfigPtr
|
|
ExportElementFactory::add_channel_config ()
|
|
{
|
|
return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
|
|
}
|
|
|
|
ExportFormatSpecPtr
|
|
ExportElementFactory::add_format ()
|
|
{
|
|
return ExportFormatSpecPtr (new ExportFormatSpecification (session));
|
|
}
|
|
|
|
ExportFormatSpecPtr
|
|
ExportElementFactory::add_format (XMLNode const & state)
|
|
{
|
|
return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
|
|
}
|
|
|
|
ExportFormatSpecPtr
|
|
ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
|
|
{
|
|
return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
|
|
}
|
|
|
|
ExportFilenamePtr
|
|
ExportElementFactory::add_filename ()
|
|
{
|
|
return ExportFilenamePtr (new ExportFilename (session));
|
|
}
|
|
|
|
ExportFilenamePtr
|
|
ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
|
|
{
|
|
return ExportFilenamePtr (new ExportFilename (*other));
|
|
}
|
|
|
|
/*** ExportHandler ***/
|
|
|
|
ExportHandler::ExportHandler (Session & session)
|
|
: ExportElementFactory (session)
|
|
, session (session)
|
|
, graph_builder (new ExportGraphBuilder (session))
|
|
, export_status (session.get_export_status ())
|
|
, post_processing (false)
|
|
, cue_tracknum (0)
|
|
, cue_indexnum (0)
|
|
{
|
|
}
|
|
|
|
ExportHandler::~ExportHandler ()
|
|
{
|
|
graph_builder->cleanup (export_status->aborted () );
|
|
}
|
|
|
|
/** Add an export to the `to-do' list */
|
|
bool
|
|
ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
|
|
ExportFormatSpecPtr format, ExportFilenamePtr filename,
|
|
BroadcastInfoPtr broadcast_info)
|
|
{
|
|
FileSpec spec (channel_config, format, filename, broadcast_info);
|
|
config_map.insert (make_pair (timespan, spec));
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
ExportHandler::do_export ()
|
|
{
|
|
/* Count timespans */
|
|
|
|
export_status->init();
|
|
std::set<ExportTimespanPtr> timespan_set;
|
|
for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
|
|
bool new_timespan = timespan_set.insert (it->first).second;
|
|
if (new_timespan) {
|
|
export_status->total_samples += it->first->get_length();
|
|
}
|
|
}
|
|
export_status->total_timespans = timespan_set.size();
|
|
|
|
if (export_status->total_timespans > 1) {
|
|
// always include timespan if there's more than one.
|
|
for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
|
|
FileSpec & spec = it->second;
|
|
spec.filename->include_timespan = true;
|
|
}
|
|
}
|
|
|
|
/* Start export */
|
|
|
|
Glib::Threads::Mutex::Lock l (export_status->lock());
|
|
start_timespan ();
|
|
}
|
|
|
|
void
|
|
ExportHandler::start_timespan ()
|
|
{
|
|
export_status->timespan++;
|
|
|
|
/* stop freewheeling and wait for latency callbacks */
|
|
if (AudioEngine::instance()->freewheeling ()) {
|
|
AudioEngine::instance()->freewheel (false);
|
|
do {
|
|
Glib::usleep (AudioEngine::instance()->usecs_per_cycle ());
|
|
} while (AudioEngine::instance()->freewheeling ());
|
|
}
|
|
|
|
if (config_map.empty()) {
|
|
// freewheeling has to be stopped from outside the process cycle
|
|
export_status->set_running (false);
|
|
return;
|
|
}
|
|
|
|
/* finish_timespan pops the config_map entry that has been done, so
|
|
this is the timespan to do this time
|
|
*/
|
|
current_timespan = config_map.begin()->first;
|
|
|
|
export_status->total_samples_current_timespan = current_timespan->get_length();
|
|
export_status->timespan_name = current_timespan->name();
|
|
export_status->processed_samples_current_timespan = 0;
|
|
|
|
/* Register file configurations to graph builder */
|
|
|
|
/* Here's the config_map entries that use this timespan */
|
|
timespan_bounds = config_map.equal_range (current_timespan);
|
|
graph_builder->reset ();
|
|
graph_builder->set_current_timespan (current_timespan);
|
|
handle_duplicate_format_extensions();
|
|
bool realtime = current_timespan->realtime ();
|
|
bool region_export = true;
|
|
for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
|
|
// Filenames can be shared across timespans
|
|
FileSpec & spec = it->second;
|
|
spec.filename->set_timespan (it->first);
|
|
switch (spec.channel_config->region_processing_type ()) {
|
|
case RegionExportChannelFactory::None:
|
|
region_export = false;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
graph_builder->add_config (spec, realtime);
|
|
}
|
|
|
|
// ExportDialog::update_realtime_selection does not allow this
|
|
assert (!region_export || !realtime);
|
|
|
|
/* start export */
|
|
|
|
post_processing = false;
|
|
session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
|
|
process_position = current_timespan->get_start();
|
|
// TODO check if it's a RegionExport.. set flag to skip process_without_events()
|
|
session.start_audio_export (process_position, realtime, region_export);
|
|
}
|
|
|
|
void
|
|
ExportHandler::handle_duplicate_format_extensions()
|
|
{
|
|
typedef std::map<std::string, int> ExtCountMap;
|
|
|
|
ExtCountMap counts;
|
|
for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
|
|
if (it->second.filename->include_channel_config && it->second.channel_config) {
|
|
/* stem-export has multiple files in the same timestamp, but a different channel_config for each.
|
|
* However channel_config is only set in ExportGraphBuilder::Encoder::init_writer()
|
|
* so we cannot yet use it->second.filename->get_path(it->second.format).
|
|
* We have to explicily check uniqueness of "channel-config + extension" here:
|
|
*/
|
|
counts[it->second.channel_config->name() + it->second.format->extension()]++;
|
|
} else {
|
|
counts[it->second.format->extension()]++;
|
|
}
|
|
}
|
|
|
|
bool duplicates_found = false;
|
|
for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
|
|
if (it->second > 1) { duplicates_found = true; }
|
|
}
|
|
|
|
// Set this always, as the filenames are shared...
|
|
for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
|
|
it->second.filename->include_format_name = duplicates_found;
|
|
}
|
|
}
|
|
|
|
int
|
|
ExportHandler::process (samplecnt_t samples)
|
|
{
|
|
if (!export_status->running ()) {
|
|
return 0;
|
|
} else if (post_processing) {
|
|
Glib::Threads::Mutex::Lock l (export_status->lock());
|
|
if (AudioEngine::instance()->freewheeling ()) {
|
|
return post_process ();
|
|
} else {
|
|
// wait until we're freewheeling
|
|
return 0;
|
|
}
|
|
} else if (samples > 0) {
|
|
Glib::Threads::Mutex::Lock l (export_status->lock());
|
|
return process_timespan (samples);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
ExportHandler::process_timespan (samplecnt_t samples)
|
|
{
|
|
export_status->active_job = ExportStatus::Exporting;
|
|
/* update position */
|
|
|
|
samplecnt_t samples_to_read = 0;
|
|
samplepos_t const end = current_timespan->get_end();
|
|
|
|
bool const last_cycle = (process_position + samples >= end);
|
|
|
|
if (last_cycle) {
|
|
samples_to_read = end - process_position;
|
|
export_status->stop = true;
|
|
} else {
|
|
samples_to_read = samples;
|
|
}
|
|
|
|
/* Do actual processing */
|
|
samplecnt_t ret = graph_builder->process (samples_to_read, last_cycle);
|
|
if (ret > 0) {
|
|
process_position += ret;
|
|
export_status->processed_samples += ret;
|
|
export_status->processed_samples_current_timespan += ret;
|
|
}
|
|
|
|
/* Start post-processing/normalizing if necessary */
|
|
if (last_cycle) {
|
|
post_processing = graph_builder->need_postprocessing ();
|
|
if (post_processing) {
|
|
export_status->total_postprocessing_cycles = graph_builder->get_postprocessing_cycle_count();
|
|
export_status->current_postprocessing_cycle = 0;
|
|
} else {
|
|
finish_timespan ();
|
|
}
|
|
return 1; /* trigger realtime_stop() */
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
ExportHandler::post_process ()
|
|
{
|
|
if (graph_builder->post_process ()) {
|
|
finish_timespan ();
|
|
export_status->active_job = ExportStatus::Exporting;
|
|
} else {
|
|
if (graph_builder->realtime ()) {
|
|
export_status->active_job = ExportStatus::Encoding;
|
|
} else {
|
|
export_status->active_job = ExportStatus::Normalizing;
|
|
}
|
|
}
|
|
|
|
export_status->current_postprocessing_cycle++;
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
ExportHandler::command_output(std::string output, size_t size)
|
|
{
|
|
std::cerr << "command: " << size << ", " << output << std::endl;
|
|
info << output << endmsg;
|
|
}
|
|
|
|
void*
|
|
ExportHandler::start_timespan_bg (void* eh)
|
|
{
|
|
ExportHandler* self = static_cast<ExportHandler*> (eh);
|
|
self->process_connection.disconnect ();
|
|
Glib::Threads::Mutex::Lock l (self->export_status->lock());
|
|
self->start_timespan ();
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
ExportHandler::finish_timespan ()
|
|
{
|
|
graph_builder->get_analysis_results (export_status->result_map);
|
|
|
|
while (config_map.begin() != timespan_bounds.second) {
|
|
|
|
ExportFormatSpecPtr fmt = config_map.begin()->second.format;
|
|
std::string filename = config_map.begin()->second.filename->get_path(fmt);
|
|
if (fmt->with_cue()) {
|
|
export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
|
|
}
|
|
|
|
if (fmt->with_toc()) {
|
|
export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
|
|
}
|
|
|
|
if (fmt->with_mp4chaps()) {
|
|
export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
|
|
}
|
|
|
|
Session::Exported (current_timespan->name(), filename); /* EMIT SIGNAL */
|
|
|
|
/* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
|
|
* The process cannot access the file because it is being used.
|
|
* ditto for post-export and upload.
|
|
*/
|
|
graph_builder->reset ();
|
|
|
|
if (fmt->tag()) {
|
|
/* TODO: check Umlauts and encoding in filename.
|
|
* TagLib eventually calls CreateFileA(),
|
|
*/
|
|
export_status->active_job = ExportStatus::Tagging;
|
|
AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
|
|
}
|
|
|
|
if (!fmt->command().empty()) {
|
|
SessionMetadata const & metadata (*SessionMetadata::Metadata());
|
|
|
|
#if 0 // would be nicer with C++11 initialiser...
|
|
std::map<char, std::string> subs {
|
|
{ 'f', filename },
|
|
{ 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR },
|
|
{ 'b', PBD::basename_nosuffix(filename) },
|
|
...
|
|
};
|
|
#endif
|
|
export_status->active_job = ExportStatus::Command;
|
|
PBD::ScopedConnection command_connection;
|
|
std::map<char, std::string> subs;
|
|
|
|
std::stringstream track_number;
|
|
track_number << metadata.track_number ();
|
|
std::stringstream total_tracks;
|
|
total_tracks << metadata.total_tracks ();
|
|
std::stringstream year;
|
|
year << metadata.year ();
|
|
|
|
subs.insert (std::pair<char, std::string> ('a', metadata.artist ()));
|
|
subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
|
|
subs.insert (std::pair<char, std::string> ('c', metadata.copyright ()));
|
|
subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
|
|
subs.insert (std::pair<char, std::string> ('f', filename));
|
|
subs.insert (std::pair<char, std::string> ('l', metadata.lyricist ()));
|
|
subs.insert (std::pair<char, std::string> ('n', session.name ()));
|
|
subs.insert (std::pair<char, std::string> ('s', session.path ()));
|
|
subs.insert (std::pair<char, std::string> ('o', metadata.conductor ()));
|
|
subs.insert (std::pair<char, std::string> ('t', metadata.title ()));
|
|
subs.insert (std::pair<char, std::string> ('z', metadata.organization ()));
|
|
subs.insert (std::pair<char, std::string> ('A', metadata.album ()));
|
|
subs.insert (std::pair<char, std::string> ('C', metadata.comment ()));
|
|
subs.insert (std::pair<char, std::string> ('E', metadata.engineer ()));
|
|
subs.insert (std::pair<char, std::string> ('G', metadata.genre ()));
|
|
subs.insert (std::pair<char, std::string> ('L', total_tracks.str ()));
|
|
subs.insert (std::pair<char, std::string> ('M', metadata.mixer ()));
|
|
subs.insert (std::pair<char, std::string> ('N', current_timespan->name())); // =?= config_map.begin()->first->name ()
|
|
subs.insert (std::pair<char, std::string> ('O', metadata.composer ()));
|
|
subs.insert (std::pair<char, std::string> ('P', metadata.producer ()));
|
|
subs.insert (std::pair<char, std::string> ('S', metadata.disc_subtitle ()));
|
|
subs.insert (std::pair<char, std::string> ('T', track_number.str ()));
|
|
subs.insert (std::pair<char, std::string> ('Y', year.str ()));
|
|
subs.insert (std::pair<char, std::string> ('Z', metadata.country ()));
|
|
|
|
ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
|
|
info << "Post-export command line : {" << se->to_s () << "}" << endmsg;
|
|
se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
|
|
int ret = se->start (SystemExec::MergeWithStdin);
|
|
if (ret == 0) {
|
|
// successfully started
|
|
while (se->is_running ()) {
|
|
// wait for system exec to terminate
|
|
Glib::usleep (1000);
|
|
}
|
|
} else {
|
|
error << "Post-export command FAILED with Error: " << ret << endmsg;
|
|
}
|
|
delete (se);
|
|
}
|
|
|
|
// XXX THIS IS IN REALTIME CONTEXT, CALLED FROM
|
|
// AudioEngine::process_callback()
|
|
// freewheeling, yes, but still uploading here is NOT
|
|
// a good idea.
|
|
//
|
|
// even less so, since SoundcloudProgress is using
|
|
// connect_same_thread() - GUI updates from the RT thread
|
|
// will cause crashes. http://pastebin.com/UJKYNGHR
|
|
if (fmt->soundcloud_upload()) {
|
|
SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
|
|
std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
|
|
DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
|
|
"uploading %1 - username=%2, password=%3, token=%4",
|
|
filename, soundcloud_username, soundcloud_password, token) );
|
|
std::string path = soundcloud_uploader->Upload (
|
|
filename,
|
|
PBD::basename_nosuffix(filename), // title
|
|
token,
|
|
soundcloud_make_public,
|
|
soundcloud_downloadable,
|
|
this);
|
|
|
|
if (path.length() != 0) {
|
|
info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
|
|
if (soundcloud_open_page) {
|
|
DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
|
|
open_uri(path.c_str()); // open the soundcloud website to the new file
|
|
}
|
|
} else {
|
|
error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
|
|
}
|
|
delete soundcloud_uploader;
|
|
}
|
|
config_map.erase (config_map.begin());
|
|
}
|
|
|
|
/* finish timespan is called in freewheeling rt-context,
|
|
* we cannot start a new export from here */
|
|
assert (AudioEngine::instance()->freewheeling ());
|
|
pthread_t tid;
|
|
pthread_create (&tid, NULL, ExportHandler::start_timespan_bg, this);
|
|
pthread_detach (tid);
|
|
}
|
|
|
|
void
|
|
ExportHandler::reset ()
|
|
{
|
|
config_map.clear ();
|
|
graph_builder->reset ();
|
|
}
|
|
|
|
/*** CD Marker stuff ***/
|
|
|
|
struct LocationSortByStart {
|
|
bool operator() (Location *a, Location *b) {
|
|
return a->start() < b->start();
|
|
}
|
|
};
|
|
|
|
void
|
|
ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
|
|
std::string filename, CDMarkerFormat format)
|
|
{
|
|
string filepath = get_cd_marker_filename(filename, format);
|
|
|
|
try {
|
|
void (ExportHandler::*header_func) (CDMarkerStatus &);
|
|
void (ExportHandler::*track_func) (CDMarkerStatus &);
|
|
void (ExportHandler::*index_func) (CDMarkerStatus &);
|
|
|
|
switch (format) {
|
|
case CDMarkerTOC:
|
|
header_func = &ExportHandler::write_toc_header;
|
|
track_func = &ExportHandler::write_track_info_toc;
|
|
index_func = &ExportHandler::write_index_info_toc;
|
|
break;
|
|
case CDMarkerCUE:
|
|
header_func = &ExportHandler::write_cue_header;
|
|
track_func = &ExportHandler::write_track_info_cue;
|
|
index_func = &ExportHandler::write_index_info_cue;
|
|
break;
|
|
case MP4Chaps:
|
|
header_func = &ExportHandler::write_mp4ch_header;
|
|
track_func = &ExportHandler::write_track_info_mp4ch;
|
|
index_func = &ExportHandler::write_index_info_mp4ch;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
CDMarkerStatus status (filepath, timespan, file_format, filename);
|
|
|
|
(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_session_range()) {
|
|
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 */
|
|
|
|
samplepos_t last_end_time = timespan->get_start();
|
|
status.track_position = 0;
|
|
|
|
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_sample = (*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_end_time = (*nexti)->start();
|
|
} else {
|
|
// this was the last marker, use timespan end
|
|
status.track_duration = timespan->get_end() - last_end_time;
|
|
|
|
last_end_time = timespan->get_end();
|
|
}
|
|
} else {
|
|
// range
|
|
status.track_duration = (*i)->end() - last_end_time;
|
|
|
|
last_end_time = (*i)->end();
|
|
}
|
|
|
|
(this->*track_func) (status);
|
|
}
|
|
|
|
} catch (std::exception& e) {
|
|
error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
|
|
::g_unlink (filepath.c_str());
|
|
} catch (Glib::Exception& e) {
|
|
error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
|
|
::g_unlink (filepath.c_str());
|
|
}
|
|
}
|
|
|
|
string
|
|
ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
|
|
{
|
|
/* do not strip file suffix because there may be more than one format,
|
|
and we do not want the CD marker file from one format to overwrite
|
|
another (e.g. foo.wav.cue > foo.aiff.cue)
|
|
*/
|
|
|
|
switch (format) {
|
|
case CDMarkerTOC:
|
|
return filename + ".toc";
|
|
case CDMarkerCUE:
|
|
return filename + ".cue";
|
|
case MP4Chaps:
|
|
{
|
|
unsigned lastdot = filename.find_last_of('.');
|
|
return filename.substr(0,lastdot) + ".chapters.txt";
|
|
}
|
|
default:
|
|
return filename + ".marker"; // Should not be reached when actually creating a file
|
|
}
|
|
}
|
|
|
|
void
|
|
ExportHandler::write_cue_header (CDMarkerStatus & status)
|
|
{
|
|
string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
|
|
|
|
// Album metadata
|
|
string barcode = SessionMetadata::Metadata()->barcode();
|
|
string album_artist = SessionMetadata::Metadata()->album_artist();
|
|
string album_title = SessionMetadata::Metadata()->album();
|
|
|
|
status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
|
|
|
|
if (barcode != "")
|
|
status.out << "CATALOG " << barcode << endl;
|
|
|
|
if (album_artist != "")
|
|
status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
|
|
|
|
if (album_title != "")
|
|
title = album_title;
|
|
|
|
status.out << "TITLE " << cue_escape_cdtext (title) << endl;
|
|
|
|
/* The original cue sheet spec mentions five file types
|
|
WAVE, AIFF,
|
|
BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
|
|
MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
|
|
and MP3
|
|
|
|
We try to use these file types whenever appropriate and
|
|
default to our own names otherwise.
|
|
*/
|
|
status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
|
|
if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
|
|
status.out << "WAVE";
|
|
} else if (status.format->format_id() == 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 {
|
|
// no special case for AIFF format it's name is already "AIFF"
|
|
status.out << status.format->format_name();
|
|
}
|
|
status.out << endl;
|
|
}
|
|
|
|
void
|
|
ExportHandler::write_toc_header (CDMarkerStatus & status)
|
|
{
|
|
string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
|
|
|
|
// Album metadata
|
|
string barcode = SessionMetadata::Metadata()->barcode();
|
|
string album_artist = SessionMetadata::Metadata()->album_artist();
|
|
string album_title = SessionMetadata::Metadata()->album();
|
|
|
|
if (barcode != "")
|
|
status.out << "CATALOG \"" << barcode << "\"" << endl;
|
|
|
|
if (album_title != "")
|
|
title = album_title;
|
|
|
|
status.out << "CD_DA" << endl;
|
|
status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
|
|
status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
|
|
status.out << " PERFORMER " << toc_escape_cdtext (album_artist) << endl;
|
|
status.out << " }" << endl << "}" << endl;
|
|
}
|
|
|
|
void
|
|
ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
|
|
{
|
|
status.out << "00:00:00.000 Intro" << 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 " << cue_escape_cdtext (status.marker->name()) << endl;
|
|
}
|
|
|
|
if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
|
|
status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
|
|
}
|
|
|
|
if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
|
|
status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
|
|
}
|
|
|
|
if (status.track_position != status.track_start_sample) {
|
|
samples_to_cd_frame_string (buf, status.track_position);
|
|
status.out << " INDEX 00" << buf << endl;
|
|
}
|
|
|
|
samples_to_cd_frame_string (buf, status.track_start_sample);
|
|
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;
|
|
status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
|
|
|
|
status.out << " PERFORMER ";
|
|
if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
|
|
status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
|
|
} else {
|
|
status.out << "\"\"" << endl;
|
|
}
|
|
|
|
if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
|
|
status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["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;
|
|
|
|
samples_to_cd_frame_string (buf, status.track_position);
|
|
status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
|
|
|
|
samples_to_cd_frame_string (buf, status.track_duration);
|
|
status.out << buf << endl;
|
|
|
|
samples_to_cd_frame_string (buf, status.track_start_sample - status.track_position);
|
|
status.out << "START" << buf << endl;
|
|
}
|
|
|
|
void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
|
|
{
|
|
gchar buf[18];
|
|
|
|
samples_to_chapter_marks_string(buf, status.track_start_sample);
|
|
status.out << buf << " " << status.marker->name() << endl;
|
|
}
|
|
|
|
void
|
|
ExportHandler::write_index_info_cue (CDMarkerStatus & status)
|
|
{
|
|
gchar buf[18];
|
|
|
|
snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
|
|
status.out << buf;
|
|
samples_to_cd_frame_string (buf, status.index_position);
|
|
status.out << buf << endl;
|
|
|
|
cue_indexnum++;
|
|
}
|
|
|
|
void
|
|
ExportHandler::write_index_info_toc (CDMarkerStatus & status)
|
|
{
|
|
gchar buf[18];
|
|
|
|
samples_to_cd_frame_string (buf, status.index_position - status.track_start_sample);
|
|
status.out << "INDEX" << buf << endl;
|
|
}
|
|
|
|
void
|
|
ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
|
|
{
|
|
}
|
|
|
|
void
|
|
ExportHandler::samples_to_cd_frame_string (char* buf, samplepos_t when)
|
|
{
|
|
samplecnt_t remainder;
|
|
samplecnt_t fr = session.nominal_sample_rate();
|
|
int mins, secs, samples;
|
|
|
|
mins = when / (60 * fr);
|
|
remainder = when - (mins * 60 * fr);
|
|
secs = remainder / fr;
|
|
remainder -= secs * fr;
|
|
samples = remainder / (fr / 75);
|
|
sprintf (buf, " %02d:%02d:%02d", mins, secs, samples);
|
|
}
|
|
|
|
void
|
|
ExportHandler::samples_to_chapter_marks_string (char* buf, samplepos_t when)
|
|
{
|
|
samplecnt_t remainder;
|
|
samplecnt_t fr = session.nominal_sample_rate();
|
|
int hours, mins, secs, msecs;
|
|
|
|
hours = when / (3600 * fr);
|
|
remainder = when - (hours * 3600 * fr);
|
|
mins = remainder / (60 * fr);
|
|
remainder -= mins * 60 * fr;
|
|
secs = remainder / fr;
|
|
remainder -= secs * fr;
|
|
msecs = (remainder * 1000) / fr;
|
|
sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
|
|
}
|
|
|
|
std::string
|
|
ExportHandler::toc_escape_cdtext (const std::string& txt)
|
|
{
|
|
Glib::ustring check (txt);
|
|
std::string out;
|
|
std::string latin1_txt;
|
|
char buf[5];
|
|
|
|
try {
|
|
latin1_txt = Glib::convert_with_fallback (txt, "ISO-8859-1", "UTF-8", "_");
|
|
} catch (Glib::ConvertError& err) {
|
|
throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
|
|
}
|
|
|
|
out = '"';
|
|
|
|
for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
|
|
|
|
if ((*c) == '"') {
|
|
out += "\\\"";
|
|
} else if ((*c) == '\\') {
|
|
out += "\\134";
|
|
} else if (isprint (*c)) {
|
|
out += *c;
|
|
} else {
|
|
snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
|
|
out += buf;
|
|
}
|
|
}
|
|
|
|
out += '"';
|
|
|
|
return out;
|
|
}
|
|
|
|
std::string
|
|
ExportHandler::toc_escape_filename (const std::string& txt)
|
|
{
|
|
std::string out;
|
|
|
|
out = '"';
|
|
|
|
// We iterate byte-wise not character-wise over a UTF-8 string here,
|
|
// because we only want to translate backslashes and double quotes
|
|
for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
|
|
|
|
if (*c == '"') {
|
|
out += "\\\"";
|
|
} else if (*c == '\\') {
|
|
out += "\\134";
|
|
} else {
|
|
out += *c;
|
|
}
|
|
}
|
|
|
|
out += '"';
|
|
|
|
return out;
|
|
}
|
|
|
|
std::string
|
|
ExportHandler::cue_escape_cdtext (const std::string& txt)
|
|
{
|
|
std::string latin1_txt;
|
|
std::string out;
|
|
|
|
try {
|
|
latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
|
|
} catch (Glib::ConvertError& err) {
|
|
throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
|
|
}
|
|
|
|
// does not do much mor than UTF-8 to Latin1 translation yet, but
|
|
// that may have to change if cue parsers in burning programs change
|
|
out = '"' + latin1_txt + '"';
|
|
|
|
return out;
|
|
}
|
|
|
|
ExportHandler::CDMarkerStatus::~CDMarkerStatus () {
|
|
if (!g_file_set_contents (path.c_str(), out.str().c_str(), -1, NULL)) {
|
|
PBD::error << string_compose(("Editor: cannot open \"%1\" as export file for CD marker file"), path) << endmsg;
|
|
}
|
|
}
|
|
|
|
} // namespace ARDOUR
|