From: Carl Hetherington Date: Wed, 9 Nov 2022 21:06:48 +0000 (+0100) Subject: Support multiple audio APIs, allowing ALSA for Linux and ASIO for Windows (#2363). X-Git-Url: https://git.carlh.net/gitweb/?a=commitdiff_plain;h=2b7a83a4bb3a7874444629987f992fbef78cde86;p=dcpomatic.git Support multiple audio APIs, allowing ALSA for Linux and ASIO for Windows (#2363). --- diff --git a/src/lib/config.cc b/src/lib/config.cc index 6984c4064..c51f729b8 100644 --- a/src/lib/config.cc +++ b/src/lib/config.cc @@ -152,6 +152,7 @@ Config::set_defaults () _nagged[i] = false; } _sound = true; + _sound_api = boost::none; _sound_output = optional (); _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()); diff --git a/src/lib/config.h b/src/lib/config.h index 463eec47e..a91d58e64 100644 --- a/src/lib/config.h +++ b/src/lib/config.h @@ -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 sound_api() const { + return _sound_api; + } + boost::optional 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 _sound_api; /** name of a specific sound output stream to use, or empty to use the default */ boost::optional _sound_output; std::string _cover_sheet; diff --git a/src/wx/config_dialog.cc b/src/wx/config_dialog.cc index b23b4bae8..9e68d8cb3 100644 --- a/src/wx/config_dialog.cc +++ b/src/wx/config_dialog.cc @@ -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 @@ -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 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 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 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 +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 SoundPage::get_sound_output () { int const sel = _sound_output->GetSelection (); if (sel == wxNOT_FOUND) { - return optional (); + return {}; } return wx_to_std (_sound_output->GetString (sel)); diff --git a/src/wx/config_dialog.h b/src/wx/config_dialog.h index e0d7f15b8..e74acb76f 100644 --- a/src/wx/config_dialog.h +++ b/src/wx/config_dialog.h @@ -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 get_sound_api(); boost::optional 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; diff --git a/src/wx/dcp_panel.cc b/src/wx/dcp_panel.cc index d8aa3ac46..c7528d942 100644 --- a/src/wx/dcp_panel.cc +++ b/src/wx/dcp_panel.cc @@ -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)); } diff --git a/src/wx/film_viewer.cc b/src/wx/film_viewer.cc index fb02f0a0f..770177228 100644 --- a/src/wx/film_viewer.cc +++ b/src/wx/film_viewer.cc @@ -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) diff --git a/src/wx/wx_util.cc b/src/wx/wx_util.cc index 074f47d61..dc6bbfdc1 100644 --- a/src/wx/wx_util.cc +++ b/src/wx/wx_util.cc @@ -42,6 +42,7 @@ LIBDCP_DISABLE_WARNINGS #include #include #include +#include LIBDCP_ENABLE_WARNINGS #include @@ -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 +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 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(apis.size())) { + return apis[0]; + } + + return apis[index]; +} + + diff --git a/src/wx/wx_util.h b/src/wx/wx_util.h index 50fb7268f..caf814a7d 100644 --- a/src/wx/wx_util.h +++ b/src/wx/wx_util.h @@ -34,6 +34,7 @@ LIBDCP_DISABLE_WARNINGS #include #include +#include LIBDCP_ENABLE_WARNINGS #include #include @@ -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& 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 audio_apis(); +AudioAPI id_to_audio_api(std::string id); +AudioAPI id_to_audio_api(boost::optional 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);