First bits of audio support.
authorCarl Hetherington <cth@carlh.net>
Wed, 15 Mar 2017 23:39:25 +0000 (23:39 +0000)
committerCarl Hetherington <cth@carlh.net>
Wed, 19 Apr 2017 22:04:32 +0000 (23:04 +0100)
cscript
src/lib/butler.cc
src/lib/butler.h
src/lib/config.cc
src/lib/config.h
src/lib/wscript
src/tools/wscript
src/wx/film_viewer.cc
src/wx/film_viewer.h
src/wx/wscript
src/wx/wx_util.h

diff --git a/cscript b/cscript
index 1b87b5463b6c89b723d371210d7bda9cf779882a..e3d36e78ad4111d05f06cc266ba9305b5e864bea 100644 (file)
--- a/cscript
+++ b/cscript
@@ -272,6 +272,7 @@ def dependencies(target):
     return (('ffmpeg-cdist', 'c7df8d5', ffmpeg_options),
             ('libdcp', 'a3032f3'),
             ('libsub', 'a04f1d5'))
+            ('rtaudio-cdist', None))
 
 def configure_options(target):
     opt = ''
index 1dbad61521b65d6927c494e35ae8b6505231d210..175846d632d501b11f0c3feb8d8f19ff62300f3a 100644 (file)
@@ -34,13 +34,16 @@ using boost::optional;
 /** Video readahead in frames */
 #define VIDEO_READAHEAD 10
 
-Butler::Butler (weak_ptr<const Film> film, shared_ptr<Player> player)
+Butler::Butler (weak_ptr<const Film> film, shared_ptr<Player> player, AudioMapping audio_mapping, int audio_channels)
        : _film (film)
        , _player (player)
        , _pending_seek_accurate (false)
        , _finished (false)
+       , _audio_mapping (audio_mapping)
+       , _audio_channels (audio_channels)
 {
        _player_video_connection = _player->Video.connect (bind (&Butler::video, this, _1, _2));
+       _player_audio_connection = _player->Audio.connect (bind (&Butler::audio, this, _1, _2));
        _player_changed_connection = _player->Changed.connect (bind (&Butler::player_changed, this));
        _thread = new boost::thread (bind (&Butler::thread, this));
 }
@@ -134,6 +137,12 @@ Butler::video (shared_ptr<PlayerVideo> video, DCPTime time)
        _video.put (video, time);
 }
 
+void
+Butler::audio (shared_ptr<AudioBuffers> audio, DCPTime time)
+{
+
+}
+
 void
 Butler::player_changed ()
 {
@@ -148,3 +157,10 @@ Butler::player_changed ()
                seek (*t, true);
        }
 }
+
+void
+Butler::get_audio (float* out, Frame frames)
+{
+       _audio.get (reinterpret_cast<float*> (out), _audio_channels, frames);
+       _summon.notify_all ();
+}
index e02351b0fd45fbfa608b20f263b3c94b57dce6f1..d6742dcfa02fdb7f04625423506f9125dd0a643d 100644 (file)
@@ -19,6 +19,8 @@
 */
 
 #include "video_ring_buffers.h"
+#include "audio_ring_buffers.h"
+#include "audio_mapping.h"
 #include <boost/shared_ptr.hpp>
 #include <boost/weak_ptr.hpp>
 #include <boost/thread.hpp>
@@ -32,15 +34,17 @@ class PlayerVideo;
 class Butler : public boost::noncopyable
 {
 public:
-       Butler (boost::weak_ptr<const Film> film, boost::shared_ptr<Player> player);
+       Butler (boost::weak_ptr<const Film> film, boost::shared_ptr<Player> player, AudioMapping map, int audio_channels);
        ~Butler ();
 
        void seek (DCPTime position, bool accurate);
        std::pair<boost::shared_ptr<PlayerVideo>, DCPTime> get_video ();
+       void get_audio (float* out, Frame frames);
 
 private:
        void thread ();
        void video (boost::shared_ptr<PlayerVideo> video, DCPTime time);
+       void audio (boost::shared_ptr<AudioBuffers> audio, DCPTime time);
        void player_changed ();
 
        boost::weak_ptr<const Film> _film;
@@ -48,6 +52,7 @@ private:
        boost::thread* _thread;
 
        VideoRingBuffers _video;
+       AudioRingBuffers _audio;
 
        boost::mutex _mutex;
        boost::condition _summon;
@@ -57,6 +62,10 @@ private:
 
        bool _finished;
 
+       AudioMapping _audio_mapping;
+       int _audio_channels;
+
        boost::signals2::scoped_connection _player_video_connection;
+       boost::signals2::scoped_connection _player_audio_connection;
        boost::signals2::scoped_connection _player_changed_connection;
 };
index 79486c6025966ed67197f7f816f9d0345ef96f1b..4376ff0575c7e4faaa82284301343a34ab550f9a 100644 (file)
@@ -116,6 +116,7 @@ Config::set_defaults ()
        _dcp_metadata_filename_format = dcp::NameFormat ("%t");
        _dcp_asset_filename_format = dcp::NameFormat ("%t");
        _jump_to_selected = true;
+       _sound_output = optional<string> ();
 
        _allowed_dcp_frame_rates.clear ();
        _allowed_dcp_frame_rates.push_back (24);
@@ -308,7 +309,11 @@ try
        _kdm_filename_format = dcp::NameFormat (f.optional_string_child("KDMFilenameFormat").get_value_or ("KDM %f %c %s"));
        _dcp_metadata_filename_format = dcp::NameFormat (f.optional_string_child("DCPMetadataFilenameFormat").get_value_or ("%t"));
        _dcp_asset_filename_format = dcp::NameFormat (f.optional_string_child("DCPAssetFilenameFormat").get_value_or ("%t"));
+<<<<<<< 8de6a5d1d054bab25ab0d86bc48442d9d6adb849
        _jump_to_selected = f.optional_bool_child("JumpToSelected").get_value_or (true);
+=======
+       _sound_output = f.optional_string_child("SoundOutput");
+>>>>>>> First bits of audio support.
 
        /* Replace any cinemas from config.xml with those from the configured file */
        if (boost::filesystem::exists (_cinemas_file)) {
@@ -476,7 +481,13 @@ Config::write_config () const
        root->add_child("KDMContainerNameFormat")->add_child_text (_kdm_container_name_format.specification ());
        root->add_child("DCPMetadataFilenameFormat")->add_child_text (_dcp_metadata_filename_format.specification ());
        root->add_child("DCPAssetFilenameFormat")->add_child_text (_dcp_asset_filename_format.specification ());
+<<<<<<< 8de6a5d1d054bab25ab0d86bc48442d9d6adb849
        root->add_child("JumpToSelected")->add_child_text (_jump_to_selected ? "1" : "0");
+=======
+       if (_sound_output) {
+               root->add_child("SoundOutput")->add_child_text (_sound_output.get());
+       }
+>>>>>>> First bits of audio support.
 
        try {
                doc.write_to_file_formatted (path("config.xml").string ());
index ed29a8f2444356864430e72fed4cc2ddd29e9c90..abfdba3dd56cd2347022a7aaf4d47e12acfcbecb 100644 (file)
@@ -67,6 +67,7 @@ public:
                USE_ANY_SERVERS,
                SERVERS,
                CINEMAS,
+               SOUND_OUTPUT,
                OTHER
        };
 
@@ -306,6 +307,10 @@ public:
                return _jump_to_selected;
        }
 
+       boost::optional<std::string> sound_output () const {
+               return _sound_output;
+       }
+
        /** @param n New number of local encoding threads */
        void set_num_local_encoding_threads (int n) {
                maybe_set (_num_local_encoding_threads, n);
@@ -521,6 +526,21 @@ public:
                maybe_set (_confirm_kdm_email, s);
        }
 
+       void set_sound_output (std::string o)
+       {
+               maybe_set (_sound_output, o, SOUND_OUTPUT);
+       }
+
+       void unset_sound_output ()
+       {
+               if (!_sound_output) {
+                       return;
+               }
+
+               _sound_output = boost::none;
+               changed ();
+       }
+
        void set_kdm_container_name_format (dcp::NameFormat n) {
                maybe_set (_kdm_container_name_format, n);
        }
@@ -582,12 +602,21 @@ private:
        boost::filesystem::path directory_or (boost::optional<boost::filesystem::path> dir, boost::filesystem::path a) const;
 
        template <class T>
-       void maybe_set (T& member, T new_value) {
+       void maybe_set (T& member, T new_value, Property prop = OTHER) {
                if (member == new_value) {
                        return;
                }
                member = new_value;
-               changed ();
+               changed (prop);
+       }
+
+       template <class T>
+       void maybe_set (boost::optional<T>& member, T new_value, Property prop = OTHER) {
+               if (member && member.get() == new_value) {
+                       return;
+               }
+               member = new_value;
+               changed (prop);
        }
 
        /** number of threads to use for J2K encoding on the local machine */
@@ -669,7 +698,12 @@ private:
        dcp::NameFormat _kdm_container_name_format;
        dcp::NameFormat _dcp_metadata_filename_format;
        dcp::NameFormat _dcp_asset_filename_format;
+<<<<<<< 8de6a5d1d054bab25ab0d86bc48442d9d6adb849
        bool _jump_to_selected;
+=======
+       /** name of a specific sound output stream to use for preview */
+       boost::optional<std::string> _sound_output;
+>>>>>>> First bits of audio support.
 
        /** Singleton instance, or 0 */
        static Config* _instance;
index 23e8468123f6b366568631a06b85fa0055bdcc3b..7b19108f591cca0c0053f55eb0d10c396f19717d 100644 (file)
@@ -34,6 +34,7 @@ sources = """
           audio_merger.cc
           audio_point.cc
           audio_processor.cc
+          audio_ring_buffers.cc
           audio_stream.cc
           butler.cc
           case_insensitive_sorter.cc
index 6c4d8bf65c85a10e66a8d7962ba4b9c364e2e981..fa9e9e5bd6f6d3d74627dacd6d938eac60b6cd6d 100644 (file)
@@ -30,7 +30,7 @@ def configure(conf):
 def build(bld):
     uselib =  'BOOST_THREAD BOOST_DATETIME DCP XMLSEC CXML XMLPP AVFORMAT AVFILTER AVCODEC '
     uselib += 'AVUTIL SWSCALE SWRESAMPLE POSTPROC CURL BOOST_FILESYSTEM SSH ZIP CAIROMM FONTCONFIG PANGOMM SUB '
-    uselib += 'MAGICK SNDFILE SAMPLERATE BOOST_REGEX ICU NETTLE '
+    uselib += 'MAGICK SNDFILE SAMPLERATE BOOST_REGEX ICU NETTLE RTAUDIO '
 
     if bld.env.TARGET_WINDOWS:
         uselib += 'WINSOCK2 BFD DBGHELP IBERTY SHLWAPI MSWSOCK BOOST_LOCALE '
index 5e15b4b07f8724eb89d94906c4b24ca53991c494..3d9ec036b2ebb97044fe889a8d5bb25a1a038032 100644 (file)
@@ -65,6 +65,13 @@ using boost::weak_ptr;
 using boost::optional;
 using dcp::Size;
 
+static
+int
+rtaudio_callback (void* out, void *, unsigned int frames, double, RtAudioStreamStatus, void* data)
+{
+       return reinterpret_cast<FilmViewer*>(data)->audio_callback (out, frames);
+}
+
 FilmViewer::FilmViewer (wxWindow* p)
        : wxPanel (p)
        , _panel (new wxPanel (this))
@@ -81,6 +88,10 @@ FilmViewer::FilmViewer (wxWindow* p)
        , _coalesce_player_changes (false)
        , _pending_player_change (false)
        , _last_seek_accurate (true)
+       , _audio (DCPOMATIC_RTAUDIO_API)
+       , _audio_channels (0)
+       , _audio_block_size (1024)
+       , _playing (false)
 {
 #ifndef __WXOSX__
        _panel->SetDoubleBuffered (true);
@@ -143,6 +154,14 @@ FilmViewer::FilmViewer (wxWindow* p)
                );
 
        setup_sensitivity ();
+
+       _config_changed_connection = Config::instance()->Changed.connect (bind (&FilmViewer::config_changed, this, _1));
+       config_changed (Config::SOUND_OUTPUT);
+}
+
+FilmViewer::~FilmViewer ()
+{
+       stop ();
 }
 
 void
@@ -193,13 +212,34 @@ FilmViewer::set_film (shared_ptr<Film> film)
 void
 FilmViewer::recreate_butler ()
 {
+       bool const was_running = stop ();
        _butler.reset ();
 
        if (!_film) {
                return;
        }
 
-       _butler.reset (new Butler (_film, _player));
+       AudioMapping map = AudioMapping (_film->audio_channels(), _audio_channels);
+
+       if (_audio_channels != 2 || _film->audio_channels() < 3) {
+               for (int i = 0; i < min (_film->audio_channels(), _audio_channels); ++i) {
+                       map.set (i, i, 1);
+               }
+       } else {
+               /* Special case: stereo output, at least 3 channel input, map L+R to L/R and
+                  C to both, all 3dB down.
+               */
+               map.set (0, 0, 1 / sqrt(2)); // L -> L
+               map.set (1, 1, 1 / sqrt(2)); // R -> R
+               map.set (2, 0, 1 / sqrt(2)); // C -> L
+               map.set (2, 1, 1 / sqrt(2)); // C -> R
+       }
+
+       _butler.reset (new Butler (_film, _player, map, _audio_channels));
+
+       if (was_running) {
+               start ();
+       }
 }
 
 void
@@ -249,7 +289,7 @@ FilmViewer::get ()
 
        ImageChanged (video.first);
 
-       _position = video.second;
+       _video_position = video.second;
        _inter_position = video.first->inter_position ();
        _inter_size = video.first->inter_size ();
 
@@ -259,17 +299,18 @@ FilmViewer::get ()
 void
 FilmViewer::timer ()
 {
-       DCPTime const frame = DCPTime::from_frames (1, _film->video_frame_rate ());
+       if (!_film) {
+               return;
+       }
 
-       if ((_position + frame) >= _film->length ()) {
-               _play_button->SetValue (false);
-               check_play_state ();
-       } else {
+       if (_audio.isStreamRunning ()) {
+               DCPTime const now = time().ceil (_film->video_frame_rate ());
                get ();
+               update_position_label ();
+               update_position_slider ();
+               DCPTime const next = now + DCPTime::from_frames (1, _film->video_frame_rate ());
+               _timer.Start (max ((next.seconds() - time().seconds()) * 1000, 0.0), wxTIMER_ONE_SHOT);
        }
-
-       update_position_label ();
-       update_position_slider ();
 }
 
 void
@@ -387,6 +428,34 @@ FilmViewer::check_play_state ()
        }
 }
 
+void
+FilmViewer::start ()
+{
+       if (_audio.isStreamOpen()) {
+               _audio.setStreamTime (_video_position.seconds());
+               _audio.startStream ();
+       }
+
+       _playing = true;
+       _timer.Start (0, wxTIMER_ONE_SHOT);
+}
+
+bool
+FilmViewer::stop ()
+{
+       if (_audio.isStreamRunning()) {
+               /* stop stream and discard any remainig queued samples */
+               _audio.abortStream ();
+       }
+
+       if (!_playing) {
+               return false;
+       }
+
+       _playing = false;
+       return true;
+}
+
 void
 FilmViewer::update_position_slider ()
 {
@@ -398,7 +467,7 @@ FilmViewer::update_position_slider ()
        DCPTime const len = _film->length ();
 
        if (len.get ()) {
-               int const new_slider_position = 4096 * _position.get() / len.get();
+               int const new_slider_position = 4096 * _video_position.get() / len.get();
                if (new_slider_position != _slider->GetValue()) {
                        _slider->SetValue (new_slider_position);
                }
@@ -416,8 +485,8 @@ FilmViewer::update_position_label ()
 
        double const fps = _film->video_frame_rate ();
        /* Count frame number from 1 ... not sure if this is the best idea */
-       _frame_number->SetLabel (wxString::Format (wxT("%ld"), lrint (_position.seconds() * fps) + 1));
-       _timecode->SetLabel (time_to_timecode (_position, fps));
+       _frame_number->SetLabel (wxString::Format (wxT("%ld"), lrint (_video_position.seconds() * fps) + 1));
+       _timecode->SetLabel (time_to_timecode (_video_position, fps));
 }
 
 void
@@ -464,14 +533,14 @@ FilmViewer::go_to (DCPTime t)
 void
 FilmViewer::back_clicked (wxMouseEvent& ev)
 {
-       go_to (_position - nudge_amount (ev));
+       go_to (_video_position - nudge_amount (ev));
        ev.Skip ();
 }
 
 void
 FilmViewer::forward_clicked (wxMouseEvent& ev)
 {
-       go_to (_position + nudge_amount (ev));
+       go_to (_video_position + nudge_amount (ev));
        ev.Skip ();
 }
 
@@ -522,13 +591,13 @@ FilmViewer::film_changed (Film::Property p)
 void
 FilmViewer::refresh ()
 {
-       seek (_position, _last_seek_accurate);
+       seek (_video_position, _last_seek_accurate);
 }
 
 void
 FilmViewer::set_position (DCPTime p)
 {
-       _position = p;
+       _video_position = p;
        seek (p, true);
        update_position_label ();
        update_position_slider ();
@@ -585,3 +654,64 @@ FilmViewer::seek (DCPTime t, bool accurate)
        _last_seek_accurate = accurate;
        get ();
 }
+
+void
+FilmViewer::config_changed (Config::Property p)
+{
+       if (p != Config::SOUND_OUTPUT) {
+               return;
+       }
+
+       if (_audio.isStreamOpen ()) {
+               _audio.closeStream ();
+       }
+
+       unsigned int st = 0;
+       if (Config::instance()->sound_output()) {
+               while (st < _audio.getDeviceCount()) {
+                       if (_audio.getDeviceInfo(st).name == Config::instance()->sound_output().get()) {
+                               break;
+                       }
+                       ++st;
+               }
+               if (st == _audio.getDeviceCount()) {
+                       st = _audio.getDefaultOutputDevice();
+               }
+       } else {
+               st = _audio.getDefaultOutputDevice();
+       }
+
+       _audio_channels = _audio.getDeviceInfo(st).outputChannels;
+       recreate_butler ();
+
+       RtAudio::StreamParameters sp;
+       sp.deviceId = st;
+       sp.nChannels = _audio_channels;
+       sp.firstChannel = 0;
+       try {
+               _audio.openStream (&sp, 0, RTAUDIO_FLOAT32, 48000, &_audio_block_size, &rtaudio_callback, this);
+#ifdef DCPOMATIC_USE_RTERROR
+       } catch (RtError& e) {
+#else
+       } catch (RtAudioError& e) {
+#endif
+               error_dialog (this, wxString::Format (_("Could not set up audio output (%s).  There will be no audio during the preview."), e.what()));
+       }
+}
+
+DCPTime
+FilmViewer::time () const
+{
+       if (_audio.isStreamRunning ()) {
+               return DCPTime::from_seconds (const_cast<RtAudio*>(&_audio)->getStreamTime ());
+       }
+
+       return _video_position;
+}
+
+int
+FilmViewer::audio_callback (void* out_p, unsigned int frames)
+{
+       _butler->get_audio (reinterpret_cast<float*> (out_p), frames);
+       return 0;
+}
index d50cc562d565dd03912a675e46e90ddf1276c6ee..ed7795dad3f1a96233a449412caca2d609aea09f 100644 (file)
@@ -23,6 +23,8 @@
  */
 
 #include "lib/film.h"
+#include "lib/config.h"
+#include <RtAudio.h>
 #include <wx/wx.h>
 
 class wxToggleButton;
@@ -40,12 +42,13 @@ class FilmViewer : public wxPanel
 {
 public:
        FilmViewer (wxWindow *);
+       ~FilmViewer ();
 
        void set_film (boost::shared_ptr<Film>);
 
        /** @return our `playhead' position; this may not lie exactly on a frame boundary */
        DCPTime position () const {
-               return _position;
+               return _video_position;
        }
 
        void set_position (DCPTime p);
@@ -53,6 +56,8 @@ public:
 
        void refresh ();
 
+       int audio_callback (void* out, unsigned int frames);
+
        boost::signals2::signal<void (boost::weak_ptr<PlayerVideo>)> ImageChanged;
 
 private:
@@ -80,6 +85,10 @@ private:
        void go_to (DCPTime t);
        void jump_to_selected_clicked ();
        void recreate_butler ();
+       void config_changed (Config::Property);
+       DCPTime time () const;
+       void start ();
+       bool stop ();
 
        boost::shared_ptr<Film> _film;
        boost::shared_ptr<Player> _player;
@@ -101,7 +110,7 @@ private:
        bool _pending_player_change;
 
        boost::shared_ptr<const Image> _frame;
-       DCPTime _position;
+       DCPTime _video_position;
        Position<int> _inter_position;
        dcp::Size _inter_size;
 
@@ -115,5 +124,11 @@ private:
         */
        bool _last_seek_accurate;
 
+       RtAudio _audio;
+       int _audio_channels;
+       unsigned int _audio_block_size;
+       bool _playing;
        boost::shared_ptr<Butler> _butler;
+
+       boost::signals2::scoped_connection _config_changed_connection;
 };
index dc6775ad3e77293601f30b6ed270d31619b2f386..4d4dd0821396710b7a7c418ebd6ba25344896da9 100644 (file)
@@ -155,15 +155,57 @@ def configure(conf):
     if not wx_version.startswith('3.0.'):
         conf.fatal('wxwidgets version 3.0.x is required; %s found' % wx_version)
 
+    try:
+        conf.check_cfg(msg='Checking for RtAudio using pkg-config',
+                       package='rtaudio',
+                       args='--cflags --libs',
+                       uselib_store='RTAUDIO',
+                       mandatory=True)
+    except:
+        conf.check_cfg(msg='Checking for RtAudio headers using rtaudio-config',
+                       package='',
+                       path='rtaudio-config',
+                       args='--cppflags',
+                       uselib_store='RTAUDIO',
+                       mandatory=True)
+
+        conf.check_cfg(msg='Checking for RtAudio libraries using rtaudio-config',
+                       package='',
+                       path='rtaudio-config',
+                       args='--libs',
+                       uselib_store='RTAUDIO',
+                       mandatory=True)
+
+    # Some rtaudio-configs don't include rtaudio as a link library.  Go figure.
+    conf.env.LIB_RTAUDIO.append('rtaudio')
+    # Don't explicitly link with pthread on Windows
+    if conf.env.TARGET_WINDOWS:
+        print conf.env
+        conf.env.CFLAGS_RTAUDIO.remove('-pthread')
+        conf.env.LINKFLAGS_RTAUDIO.remove('-pthread')
+
+    conf.check_cxx(fragment="""
+                            #include <RtAudio.h>\n
+                            int main() { throw RtError("Hello"); }
+                           """,
+                   msg='Checking for RtError class',
+                   libpath='/usr/local/lib',
+                   lib=['rtaudio'],
+                   uselib_store='',
+                   define_name='DCPOMATIC_USE_RTERROR',
+                   mandatory=False)
+
+
+
 def build(bld):
     if bld.env.STATIC_DCPOMATIC:
-        obj = bld(features = 'cxx cxxstlib')
+        obj = bld(features='cxx cxxstlib')
     else:
-        obj = bld(features = 'cxx cxxshlib')
+        obj = bld(features='cxx cxxshlib')
 
     obj.name   = 'libdcpomatic2-wx'
     obj.export_includes = ['..']
-    obj.uselib = 'BOOST_FILESYSTEM BOOST_THREAD BOOST_REGEX WXWIDGETS DCP SUB ZIP CXML '
+    obj.uselib = 'BOOST_FILESYSTEM BOOST_THREAD BOOST_REGEX WXWIDGETS DCP SUB ZIP CXML RTAUDIO '
     if bld.env.TARGET_LINUX:
         obj.uselib += 'GTK '
     if bld.env.TARGET_WINDOWS:
index 44a2a821252853cb54fb2527ea7167a4f317da29..0e12e59de652074c63093bfedeb33e968c377734 100644 (file)
@@ -52,6 +52,16 @@ class wxGridBagSizer;
 #define DCPOMATIC_BUTTON_STACK_GAP 0
 #endif
 
+#ifdef DCPOMATIC_LINUX
+#define DCPOMATIC_RTAUDIO_API RtAudio::LINUX_PULSE
+#endif
+#ifdef DCPOMATIC_WINDOWS
+#define DCPOMATIC_RTAUDIO_API RtAudio::WINDOWS_WASAPI
+#endif
+#ifdef DCPOMATIC_OSX
+#define DCPOMATIC_RTAUDIO_API RtAudio::MACOSX_CORE
+#endif
+
 /** i18n macro to support strings like Context|String
  *  so that `String' can be translated to different things
  *  in different contexts.