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 "pbd/gstdio_compat.h"
25 #include <glibmm/convert.h>
27 #include "pbd/convert.h"
29 #include "ardour/audiofile_tagger.h"
30 #include "ardour/debug.h"
31 #include "ardour/export_graph_builder.h"
32 #include "ardour/export_timespan.h"
33 #include "ardour/export_channel_configuration.h"
34 #include "ardour/export_status.h"
35 #include "ardour/export_format_specification.h"
36 #include "ardour/export_filename.h"
37 #include "ardour/soundcloud_upload.h"
38 #include "ardour/system_exec.h"
39 #include "pbd/openuri.h"
40 #include "pbd/basename.h"
41 #include "ardour/session_metadata.h"
51 /*** ExportElementFactory ***/
53 ExportElementFactory::ExportElementFactory (Session & session) :
59 ExportElementFactory::~ExportElementFactory ()
65 ExportElementFactory::add_timespan ()
67 return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
70 ExportChannelConfigPtr
71 ExportElementFactory::add_channel_config ()
73 return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
77 ExportElementFactory::add_format ()
79 return ExportFormatSpecPtr (new ExportFormatSpecification (session));
83 ExportElementFactory::add_format (XMLNode const & state)
85 return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
89 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
91 return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
95 ExportElementFactory::add_filename ()
97 return ExportFilenamePtr (new ExportFilename (session));
101 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
103 return ExportFilenamePtr (new ExportFilename (*other));
106 /*** ExportHandler ***/
108 ExportHandler::ExportHandler (Session & session)
109 : ExportElementFactory (session)
111 , graph_builder (new ExportGraphBuilder (session))
112 , export_status (session.get_export_status ())
113 , normalizing (false)
119 ExportHandler::~ExportHandler ()
121 graph_builder->cleanup (export_status->aborted () );
124 /** Add an export to the `to-do' list */
126 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
127 ExportFormatSpecPtr format, ExportFilenamePtr filename,
128 BroadcastInfoPtr broadcast_info)
130 FileSpec spec (channel_config, format, filename, broadcast_info);
131 config_map.insert (make_pair (timespan, spec));
137 ExportHandler::do_export ()
139 /* Count timespans */
141 export_status->init();
142 std::set<ExportTimespanPtr> timespan_set;
143 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
144 bool new_timespan = timespan_set.insert (it->first).second;
146 export_status->total_frames += it->first->get_length();
149 export_status->total_timespans = timespan_set.size();
151 if (export_status->total_timespans > 1) {
152 // always include timespan if there's more than one.
153 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
154 FileSpec & spec = it->second;
155 spec.filename->include_timespan = true;
161 Glib::Threads::Mutex::Lock l (export_status->lock());
166 ExportHandler::start_timespan ()
168 export_status->timespan++;
170 if (config_map.empty()) {
171 // freewheeling has to be stopped from outside the process cycle
172 export_status->set_running (false);
176 /* finish_timespan pops the config_map entry that has been done, so
177 this is the timespan to do this time
179 current_timespan = config_map.begin()->first;
181 export_status->total_frames_current_timespan = current_timespan->get_length();
182 export_status->timespan_name = current_timespan->name();
183 export_status->processed_frames_current_timespan = 0;
185 /* Register file configurations to graph builder */
187 /* Here's the config_map entries that use this timespan */
188 timespan_bounds = config_map.equal_range (current_timespan);
189 graph_builder->reset ();
190 graph_builder->set_current_timespan (current_timespan);
191 handle_duplicate_format_extensions();
192 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
193 // Filenames can be shared across timespans
194 FileSpec & spec = it->second;
195 spec.filename->set_timespan (it->first);
196 graph_builder->add_config (spec);
202 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
203 process_position = current_timespan->get_start();
204 session.start_audio_export (process_position);
208 ExportHandler::handle_duplicate_format_extensions()
210 typedef std::map<std::string, int> ExtCountMap;
213 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
214 counts[it->second.format->extension()]++;
217 bool duplicates_found = false;
218 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
219 if (it->second > 1) { duplicates_found = true; }
222 // Set this always, as the filenames are shared...
223 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
224 it->second.filename->include_format_name = duplicates_found;
229 ExportHandler::process (framecnt_t frames)
231 if (!export_status->running ()) {
233 } else if (normalizing) {
234 Glib::Threads::Mutex::Lock l (export_status->lock());
235 return process_normalize ();
237 Glib::Threads::Mutex::Lock l (export_status->lock());
238 return process_timespan (frames);
243 ExportHandler::process_timespan (framecnt_t frames)
245 export_status->active_job = ExportStatus::Exporting;
246 /* update position */
248 framecnt_t frames_to_read = 0;
249 framepos_t const end = current_timespan->get_end();
251 bool const last_cycle = (process_position + frames >= end);
254 frames_to_read = end - process_position;
255 export_status->stop = true;
257 frames_to_read = frames;
260 process_position += frames_to_read;
261 export_status->processed_frames += frames_to_read;
262 export_status->processed_frames_current_timespan += frames_to_read;
264 /* Do actual processing */
265 int ret = graph_builder->process (frames_to_read, last_cycle);
267 /* Start normalizing if necessary */
269 normalizing = graph_builder->will_normalize();
271 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
272 export_status->current_normalize_cycle = 0;
283 ExportHandler::process_normalize ()
285 if (graph_builder->process_normalize ()) {
287 export_status->active_job = ExportStatus::Exporting;
289 export_status->active_job = ExportStatus::Normalizing;
292 export_status->current_normalize_cycle++;
298 ExportHandler::command_output(std::string output, size_t size)
300 std::cerr << "command: " << size << ", " << output << std::endl;
301 info << output << endmsg;
305 ExportHandler::finish_timespan ()
307 graph_builder->get_analysis_results (export_status->result_map);
309 while (config_map.begin() != timespan_bounds.second) {
311 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
312 std::string filename = config_map.begin()->second.filename->get_path(fmt);
313 if (fmt->with_cue()) {
314 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
317 if (fmt->with_toc()) {
318 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
321 if (fmt->with_mp4chaps()) {
322 export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
325 Session::Exported (current_timespan->name(), filename); /* EMIT SIGNAL */
327 /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
328 * The process cannot access the file because it is being used.
329 * ditto for post-export and upload.
331 graph_builder->reset ();
334 /* TODO: check Umlauts and encoding in filename.
335 * TagLib eventually calls CreateFileA(),
337 export_status->active_job = ExportStatus::Tagging;
338 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
341 if (!fmt->command().empty()) {
343 #if 0 // would be nicer with C++11 initialiser...
344 std::map<char, std::string> subs {
346 { 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR },
347 { 'b', PBD::basename_nosuffix(filename) },
351 export_status->active_job = ExportStatus::Command;
352 PBD::ScopedConnection command_connection;
353 std::map<char, std::string> subs;
354 subs.insert (std::pair<char, std::string> ('f', filename));
355 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
356 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
357 subs.insert (std::pair<char, std::string> ('s', session.path ()));
358 subs.insert (std::pair<char, std::string> ('n', session.name ()));
360 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
361 info << "Post-export command line : {" << se->to_s () << "}" << endmsg;
362 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
363 int ret = se->start (2);
365 // successfully started
366 while (se->is_running ()) {
367 // wait for system exec to terminate
371 error << "Post-export command FAILED with Error: " << ret << endmsg;
376 if (fmt->soundcloud_upload()) {
377 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
378 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
379 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
380 "uploading %1 - username=%2, password=%3, token=%4",
381 filename, soundcloud_username, soundcloud_password, token) );
382 std::string path = soundcloud_uploader->Upload (
384 PBD::basename_nosuffix(filename), // title
386 soundcloud_make_public,
387 soundcloud_downloadable,
390 if (path.length() != 0) {
391 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
392 if (soundcloud_open_page) {
393 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
394 open_uri(path.c_str()); // open the soundcloud website to the new file
397 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
399 delete soundcloud_uploader;
401 config_map.erase (config_map.begin());
407 /*** CD Marker stuff ***/
409 struct LocationSortByStart {
410 bool operator() (Location *a, Location *b) {
411 return a->start() < b->start();
416 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
417 std::string filename, CDMarkerFormat format)
419 string filepath = get_cd_marker_filename(filename, format);
422 void (ExportHandler::*header_func) (CDMarkerStatus &);
423 void (ExportHandler::*track_func) (CDMarkerStatus &);
424 void (ExportHandler::*index_func) (CDMarkerStatus &);
428 header_func = &ExportHandler::write_toc_header;
429 track_func = &ExportHandler::write_track_info_toc;
430 index_func = &ExportHandler::write_index_info_toc;
433 header_func = &ExportHandler::write_cue_header;
434 track_func = &ExportHandler::write_track_info_cue;
435 index_func = &ExportHandler::write_index_info_cue;
438 header_func = &ExportHandler::write_mp4ch_header;
439 track_func = &ExportHandler::write_track_info_mp4ch;
440 index_func = &ExportHandler::write_index_info_mp4ch;
446 CDMarkerStatus status (filepath, timespan, file_format, filename);
448 (this->*header_func) (status);
450 /* Get locations and sort */
452 Locations::LocationList const & locations (session.locations()->list());
453 Locations::LocationList::const_iterator i;
454 Locations::LocationList temp;
456 for (i = locations.begin(); i != locations.end(); ++i) {
457 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
463 // TODO One index marker for whole thing
467 LocationSortByStart cmp;
469 Locations::LocationList::const_iterator nexti;
471 /* Start actual marker stuff */
473 framepos_t last_end_time = timespan->get_start();
474 status.track_position = 0;
476 for (i = temp.begin(); i != temp.end(); ++i) {
480 if ((*i)->start() < last_end_time) {
481 if ((*i)->is_mark()) {
482 /* Index within track */
484 status.index_position = (*i)->start() - timespan->get_start();
485 (this->*index_func) (status);
491 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
493 status.track_position = last_end_time - timespan->get_start();
494 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
495 status.track_duration = 0;
497 if ((*i)->is_mark()) {
498 // a mark track location needs to look ahead to the next marker's start to determine length
502 if (nexti != temp.end()) {
503 status.track_duration = (*nexti)->start() - last_end_time;
505 last_end_time = (*nexti)->start();
507 // this was the last marker, use timespan end
508 status.track_duration = timespan->get_end() - last_end_time;
510 last_end_time = timespan->get_end();
514 status.track_duration = (*i)->end() - last_end_time;
516 last_end_time = (*i)->end();
519 (this->*track_func) (status);
522 } catch (std::exception& e) {
523 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
524 ::g_unlink (filepath.c_str());
525 } catch (Glib::Exception& e) {
526 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
527 ::g_unlink (filepath.c_str());
532 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
534 /* do not strip file suffix because there may be more than one format,
535 and we do not want the CD marker file from one format to overwrite
536 another (e.g. foo.wav.cue > foo.aiff.cue)
541 return filename + ".toc";
543 return filename + ".cue";
546 unsigned lastdot = filename.find_last_of('.');
547 return filename.substr(0,lastdot) + ".chapters.txt";
550 return filename + ".marker"; // Should not be reached when actually creating a file
555 ExportHandler::write_cue_header (CDMarkerStatus & status)
557 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
560 string barcode = SessionMetadata::Metadata()->barcode();
561 string album_artist = SessionMetadata::Metadata()->album_artist();
562 string album_title = SessionMetadata::Metadata()->album();
564 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
567 status.out << "CATALOG " << barcode << endl;
569 if (album_artist != "")
570 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
572 if (album_title != "")
575 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
577 /* The original cue sheet spec mentions five file types
579 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
580 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
583 We try to use these file types whenever appropriate and
584 default to our own names otherwise.
586 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
587 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
588 status.out << "WAVE";
589 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
590 status.format->sample_format() == ExportFormatBase::SF_16 &&
591 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
592 // Format is RAW 16bit 44.1kHz
593 if (status.format->endianness() == ExportFormatBase::E_Little) {
594 status.out << "BINARY";
596 status.out << "MOTOROLA";
599 // no special case for AIFF format it's name is already "AIFF"
600 status.out << status.format->format_name();
606 ExportHandler::write_toc_header (CDMarkerStatus & status)
608 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
611 string barcode = SessionMetadata::Metadata()->barcode();
612 string album_artist = SessionMetadata::Metadata()->album_artist();
613 string album_title = SessionMetadata::Metadata()->album();
616 status.out << "CATALOG \"" << barcode << "\"" << endl;
618 if (album_title != "")
621 status.out << "CD_DA" << endl;
622 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
623 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
624 status.out << " PERFORMER " << toc_escape_cdtext (album_artist) << endl;
625 status.out << " }" << endl << "}" << endl;
629 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
631 status.out << "00:00:00.000 Intro" << endl;
635 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
639 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
640 status.out << buf << endl;
642 status.out << " FLAGS" ;
643 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
644 status.out << " SCMS ";
646 status.out << " DCP ";
649 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
650 status.out << " PRE";
654 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
655 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
658 if (status.marker->name() != "") {
659 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
662 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
663 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
666 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
667 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
670 if (status.track_position != status.track_start_frame) {
671 frames_to_cd_frames_string (buf, status.track_position);
672 status.out << " INDEX 00" << buf << endl;
675 frames_to_cd_frames_string (buf, status.track_start_frame);
676 status.out << " INDEX 01" << buf << endl;
678 status.index_number = 2;
679 status.track_number++;
683 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
687 status.out << endl << "TRACK AUDIO" << endl;
689 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
692 status.out << "COPY" << endl;
694 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
695 status.out << "PRE_EMPHASIS" << endl;
697 status.out << "NO PRE_EMPHASIS" << endl;
700 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
701 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
704 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
705 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
707 status.out << " PERFORMER ";
708 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
709 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
711 status.out << "\"\"" << endl;
714 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
715 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
718 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
719 status.out << " ISRC \"";
720 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
721 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
722 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
723 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
726 status.out << " }" << endl << "}" << endl;
728 frames_to_cd_frames_string (buf, status.track_position);
729 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
731 frames_to_cd_frames_string (buf, status.track_duration);
732 status.out << buf << endl;
734 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
735 status.out << "START" << buf << endl;
738 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
742 frames_to_chapter_marks_string(buf, status.track_start_frame);
743 status.out << buf << " " << status.marker->name() << endl;
747 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
751 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
753 frames_to_cd_frames_string (buf, status.index_position);
754 status.out << buf << endl;
760 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
764 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
765 status.out << "INDEX" << buf << endl;
769 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
774 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
776 framecnt_t remainder;
777 framecnt_t fr = session.nominal_frame_rate();
778 int mins, secs, frames;
780 mins = when / (60 * fr);
781 remainder = when - (mins * 60 * fr);
782 secs = remainder / fr;
783 remainder -= secs * fr;
784 frames = remainder / (fr / 75);
785 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
789 ExportHandler::frames_to_chapter_marks_string (char* buf, framepos_t when)
791 framecnt_t remainder;
792 framecnt_t fr = session.nominal_frame_rate();
793 int hours, mins, secs, msecs;
795 hours = when / (3600 * fr);
796 remainder = when - (hours * 3600 * fr);
797 mins = remainder / (60 * fr);
798 remainder -= mins * 60 * fr;
799 secs = remainder / fr;
800 remainder -= secs * fr;
801 msecs = (remainder * 1000) / fr;
802 sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
806 ExportHandler::toc_escape_cdtext (const std::string& txt)
808 Glib::ustring check (txt);
810 std::string latin1_txt;
814 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
815 } catch (Glib::ConvertError& err) {
816 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
821 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
825 } else if ((*c) == '\\') {
827 } else if (isprint (*c)) {
830 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
841 ExportHandler::toc_escape_filename (const std::string& txt)
847 // We iterate byte-wise not character-wise over a UTF-8 string here,
848 // because we only want to translate backslashes and double quotes
849 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
853 } else if (*c == '\\') {
866 ExportHandler::cue_escape_cdtext (const std::string& txt)
868 std::string latin1_txt;
872 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
873 } catch (Glib::ConvertError& err) {
874 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
877 // does not do much mor than UTF-8 to Latin1 translation yet, but
878 // that may have to change if cue parsers in burning programs change
879 out = '"' + latin1_txt + '"';
884 } // namespace ARDOUR