diff --git a/CMakeLists.txt b/CMakeLists.txt index 0dcbe718..ffa12301 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,6 +97,7 @@ else() find_package(FFmpeg REQUIRED) set(PLATFORM_TARGET_FILES sunshine/platform/linux/display.cpp + sunshine/platform/linux/audio.cpp sunshine/platform/linux/input.cpp) set(PLATFORM_LIBRARIES diff --git a/assets/sunshine.conf b/assets/sunshine.conf index 12377eda..c4ea57a4 100644 --- a/assets/sunshine.conf +++ b/assets/sunshine.conf @@ -94,8 +94,8 @@ # output_name = \\.\DISPLAY1 # !! Linux only !! -# Set the display number to stream. I have no idea how they are numbered. They start from 1, usually. -# output_name = 1 +# Set the display number to stream. I have no idea how they are numbered. They start from 0, usually. +# output_name = 0 ############################################### # FFmpeg software encoding parameters diff --git a/sunshine/audio.cpp b/sunshine/audio.cpp index c9ddab76..5960562d 100644 --- a/sunshine/audio.cpp +++ b/sunshine/audio.cpp @@ -25,7 +25,9 @@ struct opus_stream_config_t { constexpr std::uint8_t map_stereo[] { 0, 1 }; constexpr std::uint8_t map_surround51[] { 0, 4, 1, 5, 2, 3 }; constexpr std::uint8_t map_high_surround51[] { 0, 1, 2, 3, 4, 5 }; -constexpr auto SAMPLE_RATE = 48000; + +constexpr auto SAMPLE_RATE = 48000; + static opus_stream_config_t stereo = { SAMPLE_RATE, 2, @@ -79,7 +81,78 @@ void encodeThread(packet_queue_t packets, sample_queue_t samples, config_t confi } } +const platf::card_t *active_card(const std::vector &cards) { + for(auto &card : cards) { + if(card.active_profile) { + return &card; + } + } + + return nullptr; +} + void capture(safe::signal_t *shutdown_event, packet_queue_t packets, config_t config, void *channel_data) { + //FIXME: Pick correct opus_stream_config_t based on config.channels + auto stream = &stereo; + + auto control = platf::audio_control(); + if(!control) { + BOOST_LOG(error) << "Couldn't create audio control"sv; + + return; + } + + auto cards = control->card_info(); + if(cards.empty()) { + return; + } + + auto card = active_card(cards); + if(!card || (card->stereo.empty() && card->surround51.empty() && card->surround71.empty())) { + return; + } + + const platf::profile_t *profile; + switch(config.channels) { + case 2: + if(!card->stereo.empty()) { + profile = &card->stereo[0]; + } + else if(!card->surround51.empty()) { + profile = &card->surround51[0]; + } + else if(!card->surround71.empty()) { + profile = &card->surround71[0]; + } + break; + case 6: + if(!card->surround51.empty()) { + profile = &card->surround51[0]; + } + else if(!card->surround71.empty()) { + profile = &card->surround71[0]; + } + else { + profile = &card->stereo[0]; + } + break; + case 8: + if(!card->surround71.empty()) { + profile = &card->surround71[0]; + } + else if(!card->surround51.empty()) { + profile = &card->surround51[0]; + } + else { + profile = &card->stereo[0]; + } + break; + } + + if(control->set_output(*card, *profile)) { + return; + } + auto samples = std::make_shared(30); std::thread thread { encodeThread, packets, samples, config, channel_data }; @@ -90,13 +163,10 @@ void capture(safe::signal_t *shutdown_event, packet_queue_t packets, config_t co shutdown_event->view(); }); - //FIXME: Pick correct opus_stream_config_t based on config.channels - auto stream = &stereo; - auto frame_size = config.packetDuration * stream->sampleRate / 1000; int samples_per_frame = frame_size * stream->channelCount; - auto mic = platf::microphone(stream->sampleRate, frame_size); + auto mic = control->create_mic(stream->sampleRate, frame_size); if(!mic) { BOOST_LOG(error) << "Couldn't create audio input"sv; @@ -115,7 +185,7 @@ void capture(safe::signal_t *shutdown_event, packet_queue_t packets, config_t co continue; case platf::capture_e::reinit: mic.reset(); - mic = platf::microphone(stream->sampleRate, frame_size); + mic = control->create_mic(stream->sampleRate, frame_size); if(!mic) { BOOST_LOG(error) << "Couldn't re-initialize audio input"sv; diff --git a/sunshine/config.h b/sunshine/config.h index be75f3d2..3bced756 100644 --- a/sunshine/config.h +++ b/sunshine/config.h @@ -77,8 +77,8 @@ namespace flag { enum flag_e : std::size_t { PIN_STDIN = 0, // Read PIN from stdin instead of http FRESH_STATE, // Do not load or save state - FLAG_SIZE, - CONST_PIN = 4 // Use "universal" pin + CONST_PIN = 4, // Use "universal" pin + FLAG_SIZE }; } diff --git a/sunshine/platform/common.h b/sunshine/platform/common.h index c80fc750..c044aa53 100644 --- a/sunshine/platform/common.h +++ b/sunshine/platform/common.h @@ -6,6 +6,7 @@ #define SUNSHINE_COMMON_H #include "sunshine/utility.h" +#include #include #include @@ -101,6 +102,23 @@ public: virtual ~img_t() = default; }; +struct profile_t { + std::string name; + std::string description; + + bool available; +}; + +struct card_t { + std::string name; + std::string description; + + std::optional active_profile; + std::vector stereo; + std::vector surround51; + std::vector surround71; +}; + struct hwdevice_t { void *data {}; platf::img_t *img {}; @@ -148,6 +166,16 @@ public: virtual ~mic_t() = default; }; +class audio_control_t { +public: + virtual int set_output(const card_t &card, const profile_t &) = 0; + + virtual std::unique_ptr create_mic(std::uint32_t sample_rate, std::uint32_t frame_size) = 0; + + virtual std::vector card_info() = 0; + + virtual ~audio_control_t() = default; +}; void freeInput(void *); @@ -158,6 +186,7 @@ std::string get_mac_address(const std::string_view &address); std::string from_sockaddr(const sockaddr *const); std::pair from_sockaddr_ex(const sockaddr *const); +std::unique_ptr audio_control(); std::unique_ptr microphone(std::uint32_t sample_rate, std::uint32_t frame_size); std::shared_ptr display(dev_type_e hwdevice_type); diff --git a/sunshine/platform/linux/audio.cpp b/sunshine/platform/linux/audio.cpp new file mode 100644 index 00000000..86cac399 --- /dev/null +++ b/sunshine/platform/linux/audio.cpp @@ -0,0 +1,361 @@ +// +// Created by loki on 5/16/21. +// +#include +#include + +#include +#include +#include + +#include "sunshine/platform/common.h" + +#include "sunshine/config.h" +#include "sunshine/main.h" +#include "sunshine/thread_safe.h" + +namespace platf { +using namespace std::literals; + +struct mic_attr_t : public mic_t { + pa_sample_spec ss; + util::safe_ptr mic; + + explicit mic_attr_t(pa_sample_format format, std::uint32_t sample_rate, + std::uint8_t channels) : ss { format, sample_rate, channels }, mic {} {} + + capture_e sample(std::vector &sample_buf) override { + auto sample_size = sample_buf.size(); + + auto buf = sample_buf.data(); + int status; + if(pa_simple_read(mic.get(), buf, sample_size * 2, &status)) { + BOOST_LOG(error) << "pa_simple_read() failed: "sv << pa_strerror(status); + + return capture_e::error; + } + + return capture_e::ok; + } +}; + +std::unique_ptr microphone(std::uint32_t sample_rate, std::uint32_t) { + auto mic = std::make_unique(PA_SAMPLE_S16LE, sample_rate, 2); + + int status; + + const char *audio_sink = "@DEFAULT_MONITOR@"; + if(!config::audio.sink.empty()) { + audio_sink = config::audio.sink.c_str(); + } + + mic->mic.reset( + pa_simple_new(nullptr, "sunshine", + pa_stream_direction_t::PA_STREAM_RECORD, audio_sink, + "sunshine-record", &mic->ss, nullptr, nullptr, &status)); + + if(!mic->mic) { + auto err_str = pa_strerror(status); + BOOST_LOG(error) << "pa_simple_new() failed: "sv << err_str; + + log_flush(); + std::abort(); + } + + return mic; +} + +namespace pa { +template +void pa_free(T *p) { + pa_xfree(p); +} +using ctx_t = util::safe_ptr; +using loop_t = util::safe_ptr; +using op_t = util::safe_ptr; +using string_t = util::safe_ptr>; + +template +using cb_t = std::function; + +template +void cb(ctx_t::pointer ctx, const T *i, int eol, void *userdata) { + auto &f = *(cb_t *)userdata; + + // For some reason, pulseaudio calls this callback after disconnecting + if(i && eol) { + return; + } + + f(ctx, i, eol); +} + +void ctx_state_cb(ctx_t::pointer ctx, void *userdata) { + auto &f = *(std::function *)userdata; + + f(ctx); +} + +void success_cb(ctx_t::pointer ctx, int status, void *userdata) { + assert(userdata != nullptr); + + auto alarm = (safe::alarm_raw_t *)userdata; + alarm->ring(status ? 0 : 1); +} + +profile_t make(pa_card_profile_info2 *profile) { + return profile_t { + profile->name, + profile->description, + profile->available == 1 + }; +} + +card_t make(const pa_card_info *card) { + boost::regex stereo_expr { + ".*output(?!.*surround).*stereo.*" + }; + + boost::regex surround51_expr { + ".*output.*surround(.*(^\\d|51).*|$)" + }; + + boost::regex surround71_expr { + ".*output.*surround.*7.?1.*" + }; + + std::vector stereo; + std::vector surround51; + std::vector surround71; + + std::for_each_n(card->profiles2, card->n_profiles, [&](pa_card_profile_info2 *profile) { + if(boost::regex_match(profile->name, stereo_expr)) { + stereo.emplace_back(make(profile)); + } + if(boost::regex_match(profile->name, surround51_expr)) { + surround51.emplace_back(make(profile)); + } + if(boost::regex_match(profile->name, surround71_expr)) { + surround71.emplace_back(make(profile)); + } + }); + + std::optional active_profile; + if(card->active_profile2->name != "off"sv) { + active_profile = make(card->active_profile2); + } + + return card_t { + card->name, + card->driver, + std::move(active_profile), + std::move(stereo), + std::move(surround51), + std::move(surround71), + }; +} + +const card_t *active_card(const std::vector &cards) { + for(auto &card : cards) { + if(card.active_profile) { + return &card; + } + } + + return nullptr; +} +class server_t : public audio_control_t { + enum ctx_event_e : int { + ready, + terminated, + failed + }; + +public: + loop_t loop; + ctx_t ctx; + + std::unique_ptr> events; + std::unique_ptr> events_cb; + + std::thread worker; + int init() { + events = std::make_unique>(); + loop.reset(pa_mainloop_new()); + ctx.reset(pa_context_new(pa_mainloop_get_api(loop.get()), "sunshine")); + + events_cb = std::make_unique>([this](ctx_t::pointer ctx) { + switch(pa_context_get_state(ctx)) { + case PA_CONTEXT_READY: + events->raise(ready); + break; + case PA_CONTEXT_TERMINATED: + BOOST_LOG(debug) << "Pulseadio context terminated"sv; + events->raise(terminated); + break; + case PA_CONTEXT_FAILED: + BOOST_LOG(debug) << "Pulseadio context failed"sv; + events->raise(failed); + break; + case PA_CONTEXT_CONNECTING: + BOOST_LOG(debug) << "Connecting to pulseaudio"sv; + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + } + }); + + pa_context_set_state_callback(ctx.get(), ctx_state_cb, events_cb.get()); + + auto status = pa_context_connect(ctx.get(), nullptr, PA_CONTEXT_NOFLAGS, nullptr); + if(status) { + BOOST_LOG(error) << "Couldn't connect to pulseaudio: "sv << pa_strerror(status); + return -1; + } + + worker = std::thread { + [](loop_t::pointer loop) { + int retval; + auto status = pa_mainloop_run(loop, &retval); + + if(status < 0) { + BOOST_LOG(fatal) << "Couldn't run pulseaudio main loop"sv; + + log_flush(); + std::abort(); + } + }, + loop.get() + }; + + auto event = events->pop(); + if(event == failed) { + return -1; + } + + return 0; + } + + std::vector card_info() override { + auto alarm = safe::make_alarm(); + + std::vector cards; + cb_t f = [&cards, alarm](ctx_t::pointer ctx, const pa_card_info *card_info, int eol) { + if(!card_info) { + if(!eol) { + BOOST_LOG(error) << "Couldn't get pulseaudio card info: "sv << pa_strerror(pa_context_errno(ctx)); + } + + alarm->ring(true); + return; + } + + cards.emplace_back(make(card_info)); + }; + + op_t op { pa_context_get_card_info_list(ctx.get(), cb, &f) }; + + if(!op) { + BOOST_LOG(error) << "Couldn't create card info operation: "sv << pa_strerror(pa_context_errno(ctx.get())); + + return {}; + } + + alarm->wait(); + + return cards; + } + + std::unique_ptr create_mic(std::uint32_t sample_rate, std::uint32_t frame_size) override { + return microphone(sample_rate, frame_size); + } + + int set_output(const card_t &card, const profile_t &profile) override { + auto alarm = safe::make_alarm(); + + op_t op { pa_context_set_card_profile_by_name( + ctx.get(), card.name.c_str(), profile.name.c_str(), success_cb, alarm.get()) + }; + + if(!op) { + BOOST_LOG(error) << "Couldn't create set profile operation: "sv << pa_strerror(pa_context_errno(ctx.get())); + return -1; + } + + alarm->wait(); + if(*alarm->status()) { + BOOST_LOG(error) << "Couldn't set profile ["sv << profile.name << "]: "sv << pa_strerror(pa_context_errno(ctx.get())); + + return -1; + } + + return 0; + } + + ~server_t() override { + if(worker.joinable()) { + pa_context_disconnect(ctx.get()); + + KITTY_WHILE_LOOP(auto event = events->pop(), event != terminated && event != failed, { + event = events->pop(); + }) + + pa_mainloop_quit(loop.get(), 0); + worker.join(); + } + } +}; +} // namespace pa + +std::unique_ptr audio_control() { + auto audio = std::make_unique(); + + if(audio->init()) { + return nullptr; + } + + return audio; +} + +std::unique_ptr init() { + pa::server_t server; + if(server.init()) { + return std::make_unique(); + } + + auto cards = server.card_info(); + + for(auto &card : cards) { + BOOST_LOG(info) << "---- CARD ----"sv; + BOOST_LOG(info) << "Name: ["sv << card.name << ']'; + BOOST_LOG(info) << "Description: ["sv << card.description << ']'; + + if(card.active_profile) { + BOOST_LOG(info) << "Active profile:"sv; + BOOST_LOG(info) << " Name: [" << card.active_profile->name << ']'; + BOOST_LOG(info) << " Description: ["sv << card.active_profile->description << ']'; + BOOST_LOG(info) << " Available: ["sv << card.active_profile->available << ']'; + BOOST_LOG(info); + } + + + BOOST_LOG(info) << " -- stereo --"sv; + for(auto &profile : card.stereo) { + BOOST_LOG(info) << " "sv << profile.name << ": "sv << profile.description << " ("sv << profile.available << ')'; + } + + BOOST_LOG(info) << " -- surround 5.1 --"sv; + for(auto &profile : card.surround51) { + BOOST_LOG(info) << " "sv << profile.name << ": "sv << profile.description << " ("sv << profile.available << ')'; + } + + BOOST_LOG(info) << " -- surround 7.1 --"sv; + for(auto &profile : card.surround71) { + BOOST_LOG(info) << " "sv << profile.name << ": "sv << profile.description << " ("sv << profile.available << ')'; + } + } + + return std::make_unique(); +} +} // namespace platf \ No newline at end of file diff --git a/sunshine/platform/linux/display.cpp b/sunshine/platform/linux/display.cpp index 0c45c749..b405ba12 100644 --- a/sunshine/platform/linux/display.cpp +++ b/sunshine/platform/linux/display.cpp @@ -4,7 +4,6 @@ #include "sunshine/platform/common.h" -#include #include #include @@ -20,9 +19,6 @@ #include #include -#include -#include - #include "sunshine/config.h" #include "sunshine/main.h" #include "sunshine/task_pool.h" @@ -359,28 +355,6 @@ struct shm_attr_t : public x11_attr_t { } }; -struct mic_attr_t : public mic_t { - pa_sample_spec ss; - util::safe_ptr mic; - - explicit mic_attr_t(pa_sample_format format, std::uint32_t sample_rate, - std::uint8_t channels) : ss { format, sample_rate, channels }, mic {} {} - - capture_e sample(std::vector &sample_buf) override { - auto sample_size = sample_buf.size(); - - auto buf = sample_buf.data(); - int status; - if(pa_simple_read(mic.get(), buf, sample_size * 2, &status)) { - BOOST_LOG(error) << "pa_simple_read() failed: "sv << pa_strerror(status); - - return capture_e::error; - } - - return capture_e::ok; - } -}; - std::shared_ptr display(platf::dev_type_e hwdevice_type) { if(hwdevice_type != platf::dev_type_e::none) { BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; @@ -409,32 +383,6 @@ std::shared_ptr display(platf::dev_type_e hwdevice_type) { return x11_disp; } -std::unique_ptr microphone(std::uint32_t sample_rate, std::uint32_t) { - auto mic = std::make_unique(PA_SAMPLE_S16LE, sample_rate, 2); - - int status; - - const char *audio_sink = "@DEFAULT_MONITOR@"; - if(!config::audio.sink.empty()) { - audio_sink = config::audio.sink.c_str(); - } - - mic->mic.reset( - pa_simple_new(nullptr, "sunshine", - pa_stream_direction_t::PA_STREAM_RECORD, audio_sink, - "sunshine-record", &mic->ss, nullptr, nullptr, &status)); - - if(!mic->mic) { - auto err_str = pa_strerror(status); - BOOST_LOG(error) << "pa_simple_new() failed: "sv << err_str; - - log_flush(); - std::abort(); - } - - return mic; -} - ifaddr_t get_ifaddrs() { ifaddrs *p { nullptr }; diff --git a/sunshine/platform/linux/input.cpp b/sunshine/platform/linux/input.cpp index ed3b5985..ad3397d5 100644 --- a/sunshine/platform/linux/input.cpp +++ b/sunshine/platform/linux/input.cpp @@ -584,6 +584,4 @@ void freeInput(void *p) { auto *input = (input_raw_t *)p; delete input; } - -std::unique_ptr init() { return std::make_unique(); } } // namespace platf diff --git a/sunshine/thread_safe.h b/sunshine/thread_safe.h index 21db9974..8918a676 100644 --- a/sunshine/thread_safe.h +++ b/sunshine/thread_safe.h @@ -16,9 +16,9 @@ namespace safe { template class event_t { +public: using status_t = util::optional_t; -public: template void raise(Args &&...args) { std::lock_guard lg { _lock }; @@ -131,10 +131,97 @@ private: }; template -class queue_t { +class alarm_raw_t { +public: using status_t = util::optional_t; + alarm_raw_t() : _status { util::false_v } {} + + void ring(const status_t &status) { + std::lock_guard lg(_lock); + + _status = status; + _cv.notify_one(); + } + + void ring(status_t &&status) { + std::lock_guard lg(_lock); + + _status = std::move(status); + _cv.notify_one(); + } + + template + auto wait_for(const std::chrono::duration &rel_time) { + std::unique_lock ul(_lock); + + return _cv.wait_for(ul, rel_time, [this]() { return (bool)status(); }); + } + + template + auto wait_for(const std::chrono::duration &rel_time, Pred &&pred) { + std::unique_lock ul(_lock); + + return _cv.wait_for(ul, rel_time, [this, &pred]() { return (bool)status() || pred(); }); + } + + template + auto wait_until(const std::chrono::duration &rel_time) { + std::unique_lock ul(_lock); + + return _cv.wait_until(ul, rel_time, [this]() { return (bool)status(); }); + } + + template + auto wait_until(const std::chrono::duration &rel_time, Pred &&pred) { + std::unique_lock ul(_lock); + + return _cv.wait_until(ul, rel_time, [this, &pred]() { return (bool)status() || pred(); }); + } + + auto wait() { + std::unique_lock ul(_lock); + _cv.wait(ul, [this]() { return (bool)status(); }); + } + + template + auto wait(Pred &&pred) { + std::unique_lock ul(_lock); + _cv.wait(ul, [this, &pred]() { return (bool)status() || pred(); }); + } + + const status_t &status() const { + return _status; + } + + status_t &status() { + return _status; + } + + void reset() { + _status = status_t {}; + } + +private: + std::mutex _lock; + std::condition_variable _cv; + + status_t _status; +}; + +template +using alarm_t = std::shared_ptr>; + +template +alarm_t make_alarm() { + return std::make_shared>(); +} + +template +class queue_t { public: + using status_t = util::optional_t; + queue_t(std::uint32_t max_elements) : _max_elements { max_elements } {} template @@ -202,7 +289,6 @@ public: } std::vector &unsafe() { - std::lock_guard { _lock }; return _queue; } diff --git a/sunshine/utility.h b/sunshine/utility.h index 66606a38..200f7c0f 100644 --- a/sunshine/utility.h +++ b/sunshine/utility.h @@ -53,6 +53,10 @@ auto &b = std::get<1>(a##_##b##_##c); \ auto &c = std::get<2>(a##_##b##_##c) +#define TUPLE_EL(a, b, expr) \ + decltype(expr) a##_ = expr; \ + auto &a = std::get(a##_) + namespace util { template class X, class... Y> @@ -830,6 +834,5 @@ inline auto little(T x) { return endian_helper::little(x); } template inline auto big(T x) { return endian_helper::big(x); } } // namespace endian - } // namespace util #endif