From b228c1131187f3822ece3bd6ed26869a68fb0dff Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Wed, 16 Dec 2015 00:25:07 +0100 Subject: [PATCH] commandline session utilities --- session_utils/README | 29 ++++ session_utils/ardour-util.sh.in | 17 +++ session_utils/common.cc | 161 ++++++++++++++++++++++ session_utils/common.h | 38 ++++++ session_utils/debug | 14 ++ session_utils/example.cc | 30 ++++ session_utils/export.cc | 235 ++++++++++++++++++++++++++++++++ session_utils/run | 6 + session_utils/wscript | 94 +++++++++++++ wscript | 1 + 10 files changed, 625 insertions(+) create mode 100644 session_utils/README create mode 100644 session_utils/ardour-util.sh.in create mode 100644 session_utils/common.cc create mode 100644 session_utils/common.h create mode 100755 session_utils/debug create mode 100644 session_utils/example.cc create mode 100644 session_utils/export.cc create mode 100755 session_utils/run create mode 100644 session_utils/wscript diff --git a/session_utils/README b/session_utils/README new file mode 100644 index 0000000000..510e3a4e1e --- /dev/null +++ b/session_utils/README @@ -0,0 +1,29 @@ +Ardour Session Utilities +======================== + +This folder contains some tools which directly use libardour to access ardour +sessions. + +The overall goal it to provide some non-interactive unix-style commandline +tools, which are installed along with DAW. + + +Adding new tools +---------------- + +One c++ source per tool, see "example.cc" and "export.cc" + + cp session_utils/example.cc session_utils/your_new_tool_name.cc + edit session_utils/new_tool_name.cc + ./waf + +The tool is automatically compiled and deployed when installing, using the +program-name as prefix. e.g. "export.cc" becomes "ardour4-export". +(or "mixbus3-export", depending on the project configuration) + + +Test run from the source +------------------------ + + cd session_utils + ./run ardour4-your_new_tool_name diff --git a/session_utils/ardour-util.sh.in b/session_utils/ardour-util.sh.in new file mode 100644 index 0000000000..3ffc53d966 --- /dev/null +++ b/session_utils/ardour-util.sh.in @@ -0,0 +1,17 @@ +#!/bin/sh + +# This is a wrapper script for using the ardour-session utilities +# it is intended to be symlinked into $PATH for every session-tool + +export GTK_PATH=@CONFDIR@:@LIBDIR@${GTK_PATH:+:$GTK_PATH} +export LD_LIBRARY_PATH=@LIBDIR@${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} + +# Running Ardour requires these 3 variables to be set + +export ARDOUR_DATA_PATH=@DATADIR@ +export ARDOUR_CONFIG_PATH=@CONFDIR@ +export ARDOUR_DLL_PATH=@LIBDIR@ +export VAMP_PATH=@LIBDIR@/vamp + +SELF=`basename $0` +exec "@LIBDIR@/utils/$SELF" "$@" diff --git a/session_utils/common.cc b/session_utils/common.cc new file mode 100644 index 0000000000..d7f92871b9 --- /dev/null +++ b/session_utils/common.cc @@ -0,0 +1,161 @@ +#include +#include + + +#include "pbd/debug.h" +#include "pbd/event_loop.h" +#include "pbd/error.h" +#include "pbd/failed_constructor.h" +#include "pbd/pthread_utils.h" + +#include "ardour/audioengine.h" + +#include "common.h" + +using namespace std; +using namespace ARDOUR; +using namespace PBD; + +static const char* localedir = LOCALEDIR; +TestReceiver test_receiver; + +void +TestReceiver::receive (Transmitter::Channel chn, const char * str) +{ + const char *prefix = ""; + + switch (chn) { + case Transmitter::Error: + prefix = ": [ERROR]: "; + break; + case Transmitter::Info: + /* ignore */ + return; + case Transmitter::Warning: + prefix = ": [WARNING]: "; + break; + case Transmitter::Fatal: + prefix = ": [FATAL]: "; + break; + case Transmitter::Throw: + /* this isn't supposed to happen */ + abort (); + } + + /* note: iostreams are already thread-safe: no external + lock required. + */ + + std::cout << prefix << str << std::endl; + + if (chn == Transmitter::Fatal) { + ::exit (9); + } +} + +/* temporarily required due to some code design confusion (Feb 2014) */ + +#include "ardour/vst_types.h" + +int vstfx_init (void*) { return 0; } +void vstfx_exit () {} +void vstfx_destroy_editor (VSTState*) {} + +class MyEventLoop : public sigc::trackable, public EventLoop +{ + public: + MyEventLoop (std::string const& name) : EventLoop (name) { + run_loop_thread = Glib::Threads::Thread::self(); + } + + void call_slot (InvalidationRecord*, const boost::function& f) { + if (Glib::Threads::Thread::self() == run_loop_thread) { + f (); + } + } + + Glib::Threads::Mutex& slot_invalidation_mutex() { return request_buffer_map_lock; } + + private: + Glib::Threads::Thread* run_loop_thread; + Glib::Threads::Mutex request_buffer_map_lock; +}; + +static MyEventLoop *event_loop; + +void +SessionUtils::init () +{ + if (!ARDOUR::init (false, true, localedir)) { + cerr << "Ardour failed to initialize\n" << endl; + ::exit (1); + } + + event_loop = new MyEventLoop ("util"); + EventLoop::set_event_loop_for_thread (event_loop); + SessionEvent::create_per_thread_pool ("util", 512); + + test_receiver.listen_to (error); + test_receiver.listen_to (info); + test_receiver.listen_to (fatal); + test_receiver.listen_to (warning); +} + +static Session * _load_session (string dir, string state) +{ + AudioEngine* engine = AudioEngine::create (); + + if (!engine->set_backend ("None (Dummy)", "Unit-Test", "")) { + std::cerr << "Cannot create Audio/MIDI engine\n"; + ::exit (EXIT_FAILURE); + } + + init_post_engine (); + + if (engine->start () != 0) { + std::cerr << "Cannot start Audio/MIDI engine\n"; + ::exit (EXIT_FAILURE); + } + + Session* session = new Session (*engine, dir, state); + engine->set_session (session); + return session; +} + +Session * +SessionUtils::load_session (string dir, string state) +{ + Session* s = 0; + try { + s = _load_session (dir, state); + } catch (failed_constructor& e) { + cerr << "failed_constructor: " << e.what() << "\n"; + exit (EXIT_FAILURE); + } catch (AudioEngine::PortRegistrationFailure& e) { + cerr << "PortRegistrationFailure: " << e.what() << "\n"; + exit (EXIT_FAILURE); + } catch (exception& e) { + cerr << "exception: " << e.what() << "\n"; + exit (EXIT_FAILURE); + } catch (...) { + cerr << "unknown exception.\n"; + exit (EXIT_FAILURE); + } + return s; +} + +void +SessionUtils::unload_session (Session *s) +{ + delete s; + AudioEngine::instance()->stop (); + AudioEngine::destroy (); +} + +void +SessionUtils::cleanup () +{ + ARDOUR::cleanup (); + delete event_loop; + pthread_cancel_all (); +} diff --git a/session_utils/common.h b/session_utils/common.h new file mode 100644 index 0000000000..5263e84b5a --- /dev/null +++ b/session_utils/common.h @@ -0,0 +1,38 @@ +#ifndef _session_utils_common_h_ +#define _session_utils_common_h_ + +#include "pbd/transmitter.h" +#include "pbd/receiver.h" + +#include "ardour/ardour.h" +#include "ardour/session.h" + +class TestReceiver : public Receiver +{ + protected: + void receive (Transmitter::Channel chn, const char * str); +}; + +namespace SessionUtils { + + /** initialize libardour */ + void init (); + + /** clean up, stop Processing Engine + * @param s Session to close (may me NULL) + */ + void cleanup (); + + /** @param dir Session directory. + * @param state Session state file, without .ardour suffix. + */ + ARDOUR::Session * load_session (std::string dir, std::string state); + + /** close session and stop engine + * @param s Session to close (may me NULL) + */ + void unload_session (ARDOUR::Session *s); + +}; + +#endif /* _session_utils_misc_h_ */ diff --git a/session_utils/debug b/session_utils/debug new file mode 100755 index 0000000000..6182ef28fd --- /dev/null +++ b/session_utils/debug @@ -0,0 +1,14 @@ +#!/bin/sh +TOP=`dirname "$0"`/.. +. "$TOP/build/gtk2_ardour/ardev_common_waf.sh" +SELF=$1 +shift + +export ARDOUR_INSIDE_GDB=1 + +if test -n "`which gdb`"; then + exec gdb --args "$TOP/build/session_utils/$SELF" "$@" +fi +if test -n "`which lldb`"; then + exec lldb -- "$TOP/build/session_utils/$SELF" "$@" +fi diff --git a/session_utils/example.cc b/session_utils/example.cc new file mode 100644 index 0000000000..044d40e17c --- /dev/null +++ b/session_utils/example.cc @@ -0,0 +1,30 @@ +#include +#include + +#include "common.h" + +using namespace std; +using namespace ARDOUR; +using namespace SessionUtils; + +int main (int argc, char* argv[]) +{ + SessionUtils::init(); + Session* s = 0; + + s = SessionUtils::load_session ( + "/home/rgareus/Documents/ArdourSessions/TestA/", + "TestA" + ); + + printf ("SESSION INFO: routes: %lu\n", s->get_routes()->size ()); + + sleep(2); + + //s->save_state (""); + + SessionUtils::unload_session(s); + SessionUtils::cleanup(); + + return 0; +} diff --git a/session_utils/export.cc b/session_utils/export.cc new file mode 100644 index 0000000000..e7d169a0d3 --- /dev/null +++ b/session_utils/export.cc @@ -0,0 +1,235 @@ +#include +#include +#include +#include + +#include "common.h" + +#include "pbd/basename.h" + +#include "ardour/export_handler.h" +#include "ardour/export_status.h" +#include "ardour/export_timespan.h" +#include "ardour/export_channel_configuration.h" +#include "ardour/export_format_specification.h" +#include "ardour/export_filename.h" +#include "ardour/route.h" +#include "ardour/session_metadata.h" +#include "ardour/broadcast_info.h" + +using namespace std; +using namespace ARDOUR; +using namespace SessionUtils; + +static int export_session (Session *session, + std::string outfile, + std::string samplerate, + bool normalize) +{ + ExportTimespanPtr tsp = session->get_export_handler()->add_timespan(); + boost::shared_ptr ccp = session->get_export_handler()->add_channel_config(); + boost::shared_ptr fnp = session->get_export_handler()->add_filename(); + boost::shared_ptr b; + + XMLTree tree; + + tree.read_buffer(std::string( +"" +"" +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +"" +)); + + boost::shared_ptr fmp = session->get_export_handler()->add_format(*tree.root()); + + /* set up range */ + framepos_t start, end; + start = session->current_start_frame(); + end = session->current_end_frame(); + tsp->set_range (start, end); + tsp->set_range_id ("session"); + + /* add master outs as default */ + IO* master_out = session->master_out()->output().get(); + if (!master_out) { + PBD::warning << _("Export Util: No Master Out Ports to Connect for Audio Export") << endmsg; + return -1; + } + + for (uint32_t n = 0; n < master_out->n_ports().n_audio(); ++n) { + PortExportChannel * channel = new PortExportChannel (); + channel->add_port (master_out->audio (n)); + ExportChannelPtr chan_ptr (channel); + ccp->register_channel (chan_ptr); + } + + /* output filename */ + if (outfile.empty ()) { + tsp->set_name ("session"); + } else { + std::string dirname = Glib::path_get_dirname (outfile); + std::string basename = Glib::path_get_basename (outfile); + + if (basename.size() > 4 && !basename.compare (basename.size() - 4, 4, ".wav")) { + basename = PBD::basename_nosuffix (basename); + } + + fnp->set_folder(dirname); + tsp->set_name (basename); + } + + cout << "* Writing " << Glib::build_filename (fnp->get_folder(), tsp->name() + ".wav") << endl; + + + /* output */ + fnp->set_timespan(tsp); + fnp->include_label = false; + + /* do audio export */ + fmp->set_soundcloud_upload(false); + session->get_export_handler()->add_export_config (tsp, ccp, fmp, fnp, b); + session->get_export_handler()->do_export(); + + boost::shared_ptr status = session->get_export_status (); + + // TODO trap SIGINT -> status->abort(); + + while (status->running) { + if (status->normalizing) { + double progress = ((float) status->current_normalize_cycle) / status->total_normalize_cycles; + printf ("* Normalizing %.1f%% \r", 100. * progress); fflush (stdout); + } else { + double progress = ((float) status->processed_frames_current_timespan) / status->total_frames_current_timespan; + printf ("* Exporting Audio %.1f%% \r", 100. * progress); fflush (stdout); + } + Glib::usleep (1000000); + } + printf("\n"); + + status->finish (); + + printf ("* Done.\n"); + return 0; +} + +static void usage (int status) { + // help2man compatible format (standard GNU help-text) + printf ("export - export an ardour session from the commandline.\n\n"); + printf ("Usage: export [ OPTIONS ] \n\n"); + printf ("Options:\n\ + -h, --help display this help and exit\n\ + -n, --normalize normalize signal level (to 0dBFS)\n\ + -o, --output set expected [initial] framerate\n\ + -s, --samplerate samplerate to use (default: 48000)\n\ + -V, --version print version information and exit\n\ +\n"); + printf ("\n\ +The session is exported as 16bit wav.\n\ +If the no output file is given, the session's export dir is used.\n\ +\n"); + + printf ("Report bugs to \n" + "Website: \n"); + ::exit (status); +} + +int main (int argc, char* argv[]) +{ + std::string rate = "48000"; + std::string outfile; + bool normalize = false; + + const char *optstring = "hno:r:V"; + + const struct option longopts[] = { + { "help", 0, 0, 'h' }, + { "normalize", 0, 0, 'n' }, + { "output", 1, 0, 'o' }, + { "samplerate", 1, 0, 'r' }, + { "version", 0, 0, 'V' }, + }; + + int c = 0; + while (EOF != (c = getopt_long (argc, argv, + optstring, longopts, (int *) 0))) { + switch (c) { + + case 'n': + normalize = true; + break; + + case 'o': + outfile = optarg; + break; + + case 's': + { + const int sr = atoi (optarg); + if (sr >= 8000 && sr <= 192000) { + stringstream ss; + ss << sr; + rate = ss.str(); + } else { + fprintf(stderr, "Invalid Samplerate\n"); + } + } + break; + + case 'V': + printf ("ardour-utils version %s\n\n", VERSIONSTRING); + printf ("Copyright (C) GPL 2015 Robin Gareus \n"); + exit (0); + break; + + case 'h': + usage (0); + break; + + default: + usage (EXIT_FAILURE); + break; + } + } + + if (optind + 2 > argc) { + usage (EXIT_FAILURE); + } + + SessionUtils::init(); + Session* s = 0; + + s = SessionUtils::load_session (argv[optind], argv[optind+1]); + + export_session (s, outfile, rate, normalize); + + SessionUtils::unload_session(s); + SessionUtils::cleanup(); + + return 0; +} diff --git a/session_utils/run b/session_utils/run new file mode 100755 index 0000000000..1abec31a51 --- /dev/null +++ b/session_utils/run @@ -0,0 +1,6 @@ +#!/bin/sh +TOP=`dirname "$0"`/.. +. "$TOP/build/gtk2_ardour/ardev_common_waf.sh" +SELF=$1 +shift +exec "$TOP/build/session_utils/$SELF" "$@" diff --git a/session_utils/wscript b/session_utils/wscript new file mode 100644 index 0000000000..de9111e3e5 --- /dev/null +++ b/session_utils/wscript @@ -0,0 +1,94 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf +from waflib import Options, TaskGen +import waflib.Logs as Logs, waflib.Utils as Utils +import os +import shutil +import sys +import re +import time +from waflib.Task import Task + +# Mandatory variables +top = '.' +out = 'build' + +def options(opt): + autowaf.set_options(opt) + +def configure(conf): + conf.load('misc') + conf.load('compiler_cxx') + autowaf.configure(conf) + +def build_ardour_util(bld, util): + pgmprefix = bld.env['PROGRAM_NAME'].lower() + str(bld.env['MAJOR']) + + # just the normal executable version of the GTK GUI + obj = bld (features = 'cxx c cxxprogram') + # this program does not do the whole hidden symbols thing + obj.cxxflags = [ '-fvisibility=default' ] + obj.source = ['common.cc', util + '.cc' ] + obj.target = pgmprefix + '-' + util + obj.includes = ['.'] + obj.use = [ 'libpbd', + 'libardour', + 'libardour_cp', + 'libtimecode', + 'libmidipp', + ] + obj.defines = [ + 'VERSIONSTRING="' + str(bld.env['VERSION']) + '"', + 'DATA_DIR="' + os.path.normpath(bld.env['DATADIR']) + '"', + 'CONFIG_DIR="' + os.path.normpath(bld.env['SYSCONFDIR']) + '"', + 'LOCALEDIR="' + os.path.join(os.path.normpath(bld.env['DATADIR']), 'locale') + '"', + 'PACKAGE="' + "ARDOURUTILS" + '"', + ] + obj.install_path = bld.env['LIBDIR'] + '/utils' + obj.uselib = 'UUID FLAC FONTCONFIG GLIBMM GTHREAD OGG CURL DL' + obj.uselib += ' FFTW3F' + obj.uselib += ' AUDIOUNITS OSX LO ' + obj.uselib += ' TAGLIB ' + + if sys.platform == 'darwin': + obj.uselib += ' AUDIOUNITS OSX' + obj.use += ' libappleutility' + obj.includes += ['../libs'] + + if bld.env['build_target'] == 'mingw': + if bld.env['DEBUG'] == False: + obj.linkflags = ['-mwindows'] + + if bld.is_defined('NEED_INTL'): + obj.linkflags = ' -lintl' + + +def build(bld): + VERSION = "%s.%s" % (bld.env['MAJOR'], bld.env['MINOR']) + # no wine + if bld.is_defined('WINDOWS_VST_SUPPORT') and bld.env['build_target'] != 'mingw': + return + + # don't build/install windows version just yet. + # tools/x-win/package.sh uses 'waf install'. The symlinks + # and shell wrapper script won't work on windows. + if bld.env['build_target'] == 'mingw': + return + + pgmprefix = bld.env['PROGRAM_NAME'].lower() + str(bld.env['MAJOR']) + + utils = bld.path.ant_glob('*.cc', excl=['example.cc', 'common.cc']) + + for util in utils: + fn = str(util)[:-3] + build_ardour_util(bld, fn) + bld.symlink_as(bld.env['BINDIR'] + '/' + pgmprefix + "-" + fn, bld.env['LIBDIR'] + '/utils/ardour-util.sh') + + obj = bld(features = 'subst') + obj.source = 'ardour-util.sh.in' + obj.target = 'ardour-util.sh' + obj.chmod = Utils.O755 + obj.install_path = bld.env['LIBDIR'] + '/utils' + obj.LIBDIR = os.path.normpath(bld.env['DLLDIR']) + obj.DATADIR = os.path.normpath(bld.env['DATADIR']) + obj.CONFDIR = os.path.normpath(bld.env['CONFDIR']) diff --git a/wscript b/wscript index 4f4723bc02..a98adf7e95 100644 --- a/wscript +++ b/wscript @@ -223,6 +223,7 @@ children = [ 'mcp', 'patchfiles', 'headless', + 'session_utils', # shared helper binaries (plugin-scanner, exec-wrapper) 'libs/fst', 'libs/vfork',