Support multiple audio APIs, allowing ALSA for Linux and ASIO for Windows (#2363).
authorCarl Hetherington <cth@carlh.net>
Wed, 9 Nov 2022 21:06:48 +0000 (22:06 +0100)
committerCarl Hetherington <cth@carlh.net>
Wed, 9 Nov 2022 21:06:48 +0000 (22:06 +0100)
src/lib/config.cc
src/lib/config.h
src/wx/config_dialog.cc
src/wx/config_dialog.h
src/wx/dcp_panel.cc
src/wx/film_viewer.cc
src/wx/wx_util.cc
src/wx/wx_util.h

index 6984c406440d46ae4d795eae9319d6b3f1e2c3e1..c51f729b8dc7990fe69ad4803a13391e04492204 100644 (file)
@@ -152,6 +152,7 @@ Config::set_defaults ()
                _nagged[i] = false;
        }
        _sound = true;
+       _sound_api = boost::none;
        _sound_output = optional<string> ();
        _last_kdm_write_type = KDM_WRITE_FLAT;
        _last_dkdm_write_type = DKDM_WRITE_INTERNAL;
@@ -523,6 +524,7 @@ try
        _jump_to_selected = f.optional_bool_child("JumpToSelected").get_value_or (true);
        /* The variable was renamed but not the XML tag */
        _sound = f.optional_bool_child("PreviewSound").get_value_or (true);
+       _sound_api = f.optional_string_child("PreviewSoundAPI");
        _sound_output = f.optional_string_child("PreviewSoundOutput");
        if (f.optional_string_child("CoverSheet")) {
                _cover_sheet = f.optional_string_child("CoverSheet").get();
@@ -938,6 +940,10 @@ Config::write_config () const
        }
        /* [XML] PreviewSound 1 to use sound in the GUI preview and player, otherwise 0. */
        root->add_child("PreviewSound")->add_child_text (_sound ? "1" : "0");
+       if (_sound_api) {
+               /* [XML:opt] PreviewSoundAPI Name of the audio API to use. */
+               root->add_child("PreviewSoundAPI")->add_child_text(_sound_api.get());
+       }
        if (_sound_output) {
                /* [XML:opt] PreviewSoundOutput Name of the audio output to use. */
                root->add_child("PreviewSoundOutput")->add_child_text (_sound_output.get());
index 463eec47ed6d3f6a0f6bbf3893a1a27de6d75d32..a91d58e641b5c9d5f89e8c0d57070eb50bb7b655 100644 (file)
@@ -86,6 +86,7 @@ public:
                CINEMAS,
                DKDM_RECIPIENTS,
                SOUND,
+               SOUND_API,
                SOUND_OUTPUT,
                PLAYER_CONTENT_DIRECTORY,
                PLAYER_PLAYLIST_DIRECTORY,
@@ -440,6 +441,10 @@ public:
                return _cover_sheet;
        }
 
+       boost::optional<std::string> sound_api() const {
+               return _sound_api;
+       }
+
        boost::optional<std::string> sound_output () const {
                return _sound_output;
        }
@@ -906,6 +911,19 @@ public:
                maybe_set (_sound, s, SOUND);
        }
 
+       void set_sound_api(std::string api) {
+               maybe_set(_sound_api, api, SOUND_API);
+       }
+
+       void unset_sound_api() {
+               if (!_sound_api) {
+                       return;
+               }
+
+               _sound_api = boost::none;
+               changed(SOUND_API);
+       }
+
        void set_sound_output (std::string o) {
                maybe_set (_sound_output, o, SOUND_OUTPUT);
        }
@@ -1356,6 +1374,7 @@ private:
        bool _jump_to_selected;
        bool _nagged[NAG_COUNT];
        bool _sound;
+       boost::optional<std::string> _sound_api;
        /** name of a specific sound output stream to use, or empty to use the default */
        boost::optional<std::string> _sound_output;
        std::string _cover_sheet;
index b23b4bae80542c359aad86bea14b2dabd48cc44e..9e68d8cb3ecbdbad3d3b7551422f81e6d0c1ac57 100644 (file)
@@ -23,6 +23,7 @@
 #include "check_box.h"
 #include "config_dialog.h"
 #include "dcpomatic_button.h"
+#include "dcpomatic_choice.h"
 #include "nag_dialog.h"
 #include "static_text.h"
 #include <dcp/file.h>
@@ -860,9 +861,11 @@ SoundPage::setup ()
 
        _sound = new CheckBox (_panel, _("Play sound via"));
        table->Add (_sound, wxGBPosition (r, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
-       wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
+       auto s = new wxBoxSizer (wxHORIZONTAL);
+       _sound_api = new Choice(_panel);
+       s->Add(_sound_api, 0);
        _sound_output = new wxChoice (_panel, wxID_ANY);
-       s->Add (_sound_output, 0);
+       s->Add(_sound_output, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
        _sound_output_details = new wxStaticText (_panel, wxID_ANY, wxT(""));
        s->Add (_sound_output_details, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, DCPOMATIC_SIZER_X_GAP);
        table->Add (s, wxGBPosition(r, 1));
@@ -883,22 +886,40 @@ SoundPage::setup ()
        font.SetPointSize (font.GetPointSize() - 1);
        _sound_output_details->SetFont (font);
 
-       RtAudio audio (DCPOMATIC_RTAUDIO_API);
+       for (auto const& api: audio_apis()) {
+               _sound_api->add(api.name(), new wxStringClientData(std_to_wx(api.id())));
+       }
+
+       update_sound_outputs();
+
+       _sound->bind(&SoundPage::sound_changed, this);
+       _sound_api->Bind(wxEVT_CHOICE, bind(&SoundPage::sound_api_changed, this));
+       _sound_output->Bind(wxEVT_CHOICE, bind(&SoundPage::sound_output_changed, this));
+       _map->Changed.connect(bind(&SoundPage::map_changed, this, _1));
+       _reset_to_default->Bind(wxEVT_BUTTON, bind(&SoundPage::reset_to_default, this));
+}
+
+
+void
+SoundPage::update_sound_outputs()
+{
+       _sound_output->Clear();
+
+       RtAudio audio(id_to_audio_api(Config::instance()->sound_api()).rtaudio_id());
        for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
                try {
-                       auto dev = audio.getDeviceInfo (i);
+                       auto dev = audio.getDeviceInfo(i);
                        if (dev.probed && dev.outputChannels > 0) {
-                               _sound_output->Append (std_to_wx (dev.name));
+                               _sound_output->Append(std_to_wx(dev.name));
                        }
                } catch (RtAudioError&) {
                        /* Something went wrong so let's just ignore that device */
                }
        }
 
-       _sound->bind(&SoundPage::sound_changed, this);
-       _sound_output->Bind (wxEVT_CHOICE,   bind(&SoundPage::sound_output_changed, this));
-       _map->Changed.connect (bind(&SoundPage::map_changed, this, _1));
-       _reset_to_default->Bind (wxEVT_BUTTON,   bind(&SoundPage::reset_to_default, this));
+       if (!_sound_output->IsEmpty()) {
+               _sound_output->SetSelection(0);
+       }
 }
 
 void
@@ -913,16 +934,28 @@ SoundPage::map_changed (AudioMapping m)
        Config::instance()->set_audio_mapping (m);
 }
 
+
 void
 SoundPage::sound_changed ()
 {
        Config::instance()->set_sound (_sound->GetValue ());
 }
 
+
+void
+SoundPage::sound_api_changed()
+{
+       auto config = Config::instance();
+       config->set_sound_api(index_to_audio_api(_sound_api->get().get_value_or(0)).id());
+       config->unset_sound_output();
+       update_sound_outputs();
+}
+
+
 void
 SoundPage::sound_output_changed ()
 {
-       RtAudio audio (DCPOMATIC_RTAUDIO_API);
+       RtAudio audio(id_to_audio_api(Config::instance()->sound_api()).rtaudio_id());
        auto const so = get_sound_output();
        string default_device;
        try {
@@ -944,6 +977,28 @@ SoundPage::config_changed ()
 
        checked_set (_sound, config->sound ());
 
+       auto const current_api = get_sound_api();
+       optional<string> configured_api;
+
+       if (config->sound_api()) {
+               configured_api = config->sound_api().get();
+       } else {
+               configured_api = audio_apis()[0].id();
+       }
+
+       if (current_api != *configured_api) {
+               unsigned int i = 0;
+               while (i < _sound_api->GetCount()) {
+                       if (string_client_data(_sound_api->GetClientObject(i)) == std_to_wx(*configured_api)) {
+                               _sound_api->SetSelection(i);
+                               break;
+                       }
+                       ++i;
+               }
+       }
+
+       RtAudio audio(id_to_audio_api(configured_api).rtaudio_id());
+
        auto const current_so = get_sound_output ();
        optional<string> configured_so;
 
@@ -951,10 +1006,9 @@ SoundPage::config_changed ()
                configured_so = config->sound_output().get();
        } else {
                /* No configured output means we should use the default */
-               RtAudio audio (DCPOMATIC_RTAUDIO_API);
                try {
                        configured_so = audio.getDeviceInfo(audio.getDefaultOutputDevice()).name;
-               } catch (RtAudioError&) {
+               } catch (RtAudioError& e) {
                        /* Probably no audio devices at all */
                }
        }
@@ -971,19 +1025,6 @@ SoundPage::config_changed ()
                }
        }
 
-       RtAudio audio (DCPOMATIC_RTAUDIO_API);
-
-       map<int, wxString> apis;
-       apis[RtAudio::MACOSX_CORE]    = _("CoreAudio");
-       apis[RtAudio::WINDOWS_ASIO]   = _("ASIO");
-       apis[RtAudio::WINDOWS_DS]     = _("Direct Sound");
-       apis[RtAudio::WINDOWS_WASAPI] = _("WASAPI");
-       apis[RtAudio::UNIX_JACK]      = _("JACK");
-       apis[RtAudio::LINUX_ALSA]     = _("ALSA");
-       apis[RtAudio::LINUX_PULSE]    = _("PulseAudio");
-       apis[RtAudio::LINUX_OSS]      = _("OSS");
-       apis[RtAudio::RTAUDIO_DUMMY]  = _("Dummy");
-
        int channels = 0;
        if (configured_so) {
                for (unsigned int i = 0; i < audio.getDeviceCount(); ++i) {
@@ -998,9 +1039,7 @@ SoundPage::config_changed ()
                }
        }
 
-       _sound_output_details->SetLabel (
-               wxString::Format(_("%d channels on %s"), channels, apis[audio.getCurrentApi()])
-               );
+       _sound_output_details->SetLabel(wxString::Format(_("%d channels"), channels));
 
        _map->set (Config::instance()->audio_mapping(channels));
 
@@ -1025,13 +1064,26 @@ SoundPage::setup_sensitivity ()
        _sound_output->Enable (_sound->GetValue());
 }
 
+
+optional<string>
+SoundPage::get_sound_api()
+{
+       auto const sel = _sound_api->GetSelection();
+       if (sel == wxNOT_FOUND) {
+               return {};
+       }
+
+       return wx_to_std(selection_string_client_data(_sound_api));
+}
+
+
 /** @return Currently-selected preview sound output in the dialogue */
 optional<string>
 SoundPage::get_sound_output ()
 {
        int const sel = _sound_output->GetSelection ();
        if (sel == wxNOT_FOUND) {
-               return optional<string> ();
+               return {};
        }
 
        return wx_to_std (_sound_output->GetString (sel));
index e0d7f15b8447cc1c9f59f35f6ab5113882671a7d..e74acb76f6db5a3b0f7b633552d2a1dc3ba1ca3d 100644 (file)
@@ -50,6 +50,7 @@ LIBDCP_ENABLE_WARNINGS
 
 class AudioMappingView;
 class CheckBox;
+class Choice;
 
 
 class Page : public wxPreferencesPage
@@ -206,14 +207,18 @@ private:
 
        void setup () override;
        void config_changed () override;
+        boost::optional<std::string> get_sound_api();
         boost::optional<std::string> get_sound_output ();
        void sound_changed ();
+       void sound_api_changed();
        void sound_output_changed ();
        void setup_sensitivity ();
        void map_changed (AudioMapping m);
        void reset_to_default ();
+       void update_sound_outputs();
 
        CheckBox* _sound;
+       Choice* _sound_api;
        wxChoice* _sound_output;
        wxStaticText* _sound_output_details;
        AudioMappingView* _map;
index d8aa3ac460986acb4e096cdd2b73ac2c31531184..c7528d9422239d3e6052cb7af97dfaab2a61f0d1 100644 (file)
@@ -967,7 +967,7 @@ DCPPanel::audio_processor_changed ()
                return;
        }
 
-       auto const s = string_client_data(_audio_processor->GetClientObject(*_audio_processor->get()));
+       auto const s = selection_string_client_data(_audio_processor);
        _film->set_audio_processor (AudioProcessor::from_id (s));
 }
 
index fb02f0a0ff41074015ebc9c6b9b939aa5698f6b1..77017722807a96e37996484b90e310ad13b3db58 100644 (file)
@@ -86,7 +86,7 @@ rtaudio_callback (void* out, void *, unsigned int frames, double, RtAudioStreamS
 
 
 FilmViewer::FilmViewer (wxWindow* p)
-       : _audio (DCPOMATIC_RTAUDIO_API)
+       : _audio(id_to_audio_api(Config::instance()->sound_api()).rtaudio_id())
        , _closed_captions_dialog (new ClosedCaptionsDialog(p, this))
 {
 #if wxCHECK_VERSION(3, 1, 0)
index 074f47d61bebd2a61ff77ee4d76bbe730ee7aff5..dc6bbfdc15dc72de53a119cc465bda63301a54a9 100644 (file)
@@ -42,6 +42,7 @@ LIBDCP_DISABLE_WARNINGS
 #include <wx/progdlg.h>
 #include <wx/filepicker.h>
 #include <wx/sizer.h>
+#include <RtAudio.h>
 LIBDCP_ENABLE_WARNINGS
 #include <boost/thread.hpp>
 
@@ -220,6 +221,13 @@ string_client_data (wxClientData* o)
 }
 
 
+string
+selection_string_client_data(wxChoice* choice)
+{
+       return string_client_data(choice->GetClientObject(choice->GetSelection()));
+}
+
+
 void
 checked_set (FilePickerCtrl* widget, boost::filesystem::path value)
 {
@@ -732,3 +740,64 @@ report_config_load_failure(wxWindow* parent, Config::LoadFailure what)
        }
 }
 
+
+static AudioAPI wasapi(_("WASAPI"), "wasapi", RtAudio::WINDOWS_WASAPI);
+static AudioAPI asio(_("ASIO"), "asio", RtAudio::WINDOWS_ASIO);
+static AudioAPI pulse(_("PulseAudio"), "pulse", RtAudio::LINUX_PULSE);
+static AudioAPI alsa(_("ALSA"), "alsa", RtAudio::LINUX_ALSA);
+static AudioAPI core(_("CoreAudio"), "core", RtAudio::MACOSX_CORE);
+
+
+vector<AudioAPI>
+audio_apis()
+{
+#ifdef DCPOMATIC_WINDOWS
+       return { wasapi, asio };
+       };
+#endif
+#ifdef DCPOMATIC_LINUX
+       return { pulse, alsa };
+#endif
+#ifdef DCPOMATIC_OSX
+       return { core };
+#endif
+}
+
+
+AudioAPI
+id_to_audio_api(string id)
+{
+       const auto apis = audio_apis();
+       auto iter = std::find_if(apis.begin(), apis.end(), [&id](AudioAPI const& api) { return api.id() == id; });
+       if (iter == apis.end()) {
+               return apis[0];
+       }
+
+       return *iter;
+}
+
+
+AudioAPI
+id_to_audio_api(optional<string> id)
+{
+       const auto apis = audio_apis();
+       if (!id) {
+               return apis[0];
+       }
+
+       return id_to_audio_api(*id);
+}
+
+
+AudioAPI
+index_to_audio_api(int index)
+{
+       const auto apis = audio_apis();
+       if (index < 0 || index >= static_cast<int>(apis.size())) {
+               return apis[0];
+       }
+
+       return apis[index];
+}
+
+
index 50fb7268f9d0724314276e98c51869132d0dcc75..caf814a7dfa9c1e33119493731003a673935f483 100644 (file)
@@ -34,6 +34,7 @@
 LIBDCP_DISABLE_WARNINGS
 #include <wx/gbsizer.h>
 #include <wx/wx.h>
+#include <RtAudio.h>
 LIBDCP_ENABLE_WARNINGS
 #include <boost/signals2.hpp>
 #include <boost/thread.hpp>
@@ -81,16 +82,6 @@ class PasswordEntry;
 #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::UNSPECIFIED
-#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
@@ -112,6 +103,7 @@ extern wxString std_to_wx (std::string);
 extern void dcpomatic_setup_i18n ();
 extern wxString context_translation (wxString);
 extern std::string string_client_data (wxClientData* o);
+extern std::string selection_string_client_data(wxChoice* choice);
 extern wxString time_to_timecode (dcpomatic::DCPTime t, double fps);
 extern void setup_audio_channels_choice (wxChoice* choice, int minimum);
 extern wxSplashScreen* maybe_show_splash ();
@@ -142,6 +134,40 @@ struct Offset
 extern int get_offsets (std::vector<Offset>& offsets);
 
 
+class AudioAPI
+{
+public:
+       AudioAPI(wxString name, std::string id, RtAudio::Api rtaudio_id)
+               : _name(name)
+               , _id(id)
+               , _rtaudio_id(rtaudio_id)
+       {}
+
+       wxString name() const {
+               return _name;
+       }
+
+       std::string id() const {
+               return _id;
+       }
+
+       RtAudio::Api rtaudio_id() const {
+               return _rtaudio_id;
+       }
+
+private:
+       wxString _name;
+       std::string _id;
+       RtAudio::Api _rtaudio_id;
+};
+
+
+std::vector<AudioAPI> audio_apis();
+AudioAPI id_to_audio_api(std::string id);
+AudioAPI id_to_audio_api(boost::optional<std::string> id);
+AudioAPI index_to_audio_api(int index);
+
+
 extern void checked_set (FilePickerCtrl* widget, boost::filesystem::path value);
 extern void checked_set (wxDirPickerCtrl* widget, boost::filesystem::path value);
 extern void checked_set (wxSpinCtrl* widget, int value);