mirror of
https://github.com/thinkonmay/sunshine-sdk.git
synced 2025-12-26 14:41:14 +00:00
322 lines
8.7 KiB
C++
322 lines
8.7 KiB
C++
/**
|
|
* @file src/audio.cpp
|
|
* @brief Definitions for audio capture and encoding.
|
|
*/
|
|
// standard includes
|
|
#include <thread>
|
|
|
|
// lib includes
|
|
#include <opus/opus_multistream.h>
|
|
|
|
// local includes
|
|
#include "audio.h"
|
|
#include "config.h"
|
|
#include "globals.h"
|
|
#include "logging.h"
|
|
#include "platform/common.h"
|
|
#include "thread_safe.h"
|
|
#include "utility.h"
|
|
|
|
namespace audio {
|
|
using namespace std::literals;
|
|
using opus_t = util::safe_ptr<OpusMSEncoder, opus_multistream_encoder_destroy>;
|
|
using sample_queue_t = std::shared_ptr<safe::queue_t<std::vector<float>>>;
|
|
|
|
static int start_audio_control(audio_ctx_t &ctx);
|
|
static void stop_audio_control(audio_ctx_t &);
|
|
static void apply_surround_params(opus_stream_config_t &stream, const stream_params_t ¶ms);
|
|
|
|
int map_stream(int channels, bool quality);
|
|
|
|
constexpr auto SAMPLE_RATE = 48000;
|
|
|
|
// NOTE: If you adjust the bitrates listed here, make sure to update the
|
|
// corresponding bitrate adjustment logic in rtsp_stream::cmd_announce()
|
|
opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] {
|
|
{
|
|
SAMPLE_RATE,
|
|
2,
|
|
1,
|
|
1,
|
|
platf::speaker::map_stereo,
|
|
96000,
|
|
},
|
|
{
|
|
SAMPLE_RATE,
|
|
2,
|
|
1,
|
|
1,
|
|
platf::speaker::map_stereo,
|
|
512000,
|
|
},
|
|
{
|
|
SAMPLE_RATE,
|
|
6,
|
|
4,
|
|
2,
|
|
platf::speaker::map_surround51,
|
|
256000,
|
|
},
|
|
{
|
|
SAMPLE_RATE,
|
|
6,
|
|
6,
|
|
0,
|
|
platf::speaker::map_surround51,
|
|
1536000,
|
|
},
|
|
{
|
|
SAMPLE_RATE,
|
|
8,
|
|
5,
|
|
3,
|
|
platf::speaker::map_surround71,
|
|
450000,
|
|
},
|
|
{
|
|
SAMPLE_RATE,
|
|
8,
|
|
8,
|
|
0,
|
|
platf::speaker::map_surround71,
|
|
2048000,
|
|
},
|
|
};
|
|
|
|
void encodeThread(sample_queue_t samples, config_t config, void *channel_data) {
|
|
auto packets = mail::man->queue<packet_t>(mail::audio_packets);
|
|
auto stream = stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
|
|
if (config.flags[config_t::CUSTOM_SURROUND_PARAMS]) {
|
|
apply_surround_params(stream, config.customStreamParams);
|
|
}
|
|
|
|
// Encoding takes place on this thread
|
|
platf::adjust_thread_priority(platf::thread_priority_e::high);
|
|
|
|
opus_t opus {opus_multistream_encoder_create(
|
|
stream.sampleRate,
|
|
stream.channelCount,
|
|
stream.streams,
|
|
stream.coupledStreams,
|
|
stream.mapping,
|
|
OPUS_APPLICATION_RESTRICTED_LOWDELAY,
|
|
nullptr
|
|
)};
|
|
|
|
opus_multistream_encoder_ctl(opus.get(), OPUS_SET_BITRATE(stream.bitrate));
|
|
opus_multistream_encoder_ctl(opus.get(), OPUS_SET_VBR(0));
|
|
|
|
BOOST_LOG(info) << "Opus initialized: "sv << stream.sampleRate / 1000 << " kHz, "sv
|
|
<< stream.channelCount << " channels, "sv
|
|
<< stream.bitrate / 1000 << " kbps (total), LOWDELAY"sv;
|
|
|
|
auto frame_size = config.packetDuration * stream.sampleRate / 1000;
|
|
while (auto sample = samples->pop()) {
|
|
buffer_t packet {1400};
|
|
|
|
int bytes = opus_multistream_encode_float(opus.get(), sample->data(), frame_size, std::begin(packet), packet.size());
|
|
if (bytes < 0) {
|
|
BOOST_LOG(error) << "Couldn't encode audio: "sv << opus_strerror(bytes);
|
|
packets->stop();
|
|
|
|
return;
|
|
}
|
|
|
|
packet.fake_resize(bytes);
|
|
packets->raise(channel_data, std::move(packet));
|
|
}
|
|
}
|
|
|
|
void capture(safe::mail_t mail, config_t config, void *channel_data) {
|
|
auto shutdown_event = mail->event<bool>(mail::shutdown);
|
|
auto stream = stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
|
|
if (config.flags[config_t::CUSTOM_SURROUND_PARAMS]) {
|
|
apply_surround_params(stream, config.customStreamParams);
|
|
}
|
|
|
|
auto ref = get_audio_ctx_ref();
|
|
if (!ref) {
|
|
return;
|
|
}
|
|
|
|
auto init_failure_fg = util::fail_guard([&shutdown_event]() {
|
|
BOOST_LOG(error) << "Unable to initialize audio capture. The stream will not have audio."sv;
|
|
|
|
// Wait for shutdown to be signalled if we fail init.
|
|
// This allows streaming to continue without audio.
|
|
shutdown_event->view();
|
|
});
|
|
|
|
auto &control = ref->control;
|
|
if (!control) {
|
|
return;
|
|
}
|
|
|
|
// Order of priority:
|
|
// 1. Virtual sink
|
|
// 2. Audio sink
|
|
// 3. Host
|
|
std::string *sink = &ref->sink.host;
|
|
if (!config::audio.sink.empty()) {
|
|
sink = &config::audio.sink;
|
|
}
|
|
|
|
// Prefer the virtual sink if host playback is disabled or there's no other sink
|
|
if (ref->sink.null && (!config.flags[config_t::HOST_AUDIO] || sink->empty())) {
|
|
auto &null = *ref->sink.null;
|
|
switch (stream.channelCount) {
|
|
case 2:
|
|
sink = &null.stereo;
|
|
break;
|
|
case 6:
|
|
sink = &null.surround51;
|
|
break;
|
|
case 8:
|
|
sink = &null.surround71;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Only the first to start a session may change the default sink
|
|
if (!ref->sink_flag->exchange(true, std::memory_order_acquire)) {
|
|
// If the selected sink is different than the current one, change sinks.
|
|
ref->restore_sink = ref->sink.host != *sink;
|
|
if (ref->restore_sink) {
|
|
if (control->set_sink(*sink)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
auto frame_size = config.packetDuration * stream.sampleRate / 1000;
|
|
auto mic = control->microphone(stream.mapping, stream.channelCount, stream.sampleRate, frame_size);
|
|
if (!mic) {
|
|
return;
|
|
}
|
|
|
|
// Audio is initialized, so we don't want to print the failure message
|
|
init_failure_fg.disable();
|
|
|
|
// Capture takes place on this thread
|
|
platf::adjust_thread_priority(platf::thread_priority_e::critical);
|
|
|
|
auto samples = std::make_shared<sample_queue_t::element_type>(30);
|
|
std::thread thread {encodeThread, samples, config, channel_data};
|
|
|
|
auto fg = util::fail_guard([&]() {
|
|
samples->stop();
|
|
thread.join();
|
|
|
|
shutdown_event->view();
|
|
});
|
|
|
|
int samples_per_frame = frame_size * stream.channelCount;
|
|
|
|
while (!shutdown_event->peek()) {
|
|
std::vector<float> sample_buffer;
|
|
sample_buffer.resize(samples_per_frame);
|
|
|
|
auto status = mic->sample(sample_buffer);
|
|
switch (status) {
|
|
case platf::capture_e::ok:
|
|
break;
|
|
case platf::capture_e::timeout:
|
|
continue;
|
|
case platf::capture_e::reinit:
|
|
BOOST_LOG(info) << "Reinitializing audio capture"sv;
|
|
mic.reset();
|
|
do {
|
|
mic = control->microphone(stream.mapping, stream.channelCount, stream.sampleRate, frame_size);
|
|
if (!mic) {
|
|
BOOST_LOG(warning) << "Couldn't re-initialize audio input"sv;
|
|
}
|
|
} while (!mic && !shutdown_event->view(5s));
|
|
continue;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
samples->raise(std::move(sample_buffer));
|
|
}
|
|
}
|
|
|
|
audio_ctx_ref_t get_audio_ctx_ref() {
|
|
static auto control_shared {safe::make_shared<audio_ctx_t>(start_audio_control, stop_audio_control)};
|
|
return control_shared.ref();
|
|
}
|
|
|
|
bool is_audio_ctx_sink_available(const audio_ctx_t &ctx) {
|
|
if (!ctx.control) {
|
|
return false;
|
|
}
|
|
|
|
const std::string &sink = ctx.sink.host.empty() ? config::audio.sink : ctx.sink.host;
|
|
if (sink.empty()) {
|
|
return false;
|
|
}
|
|
|
|
return ctx.control->is_sink_available(sink);
|
|
}
|
|
|
|
int map_stream(int channels, bool quality) {
|
|
int shift = quality ? 1 : 0;
|
|
switch (channels) {
|
|
case 2:
|
|
return STEREO + shift;
|
|
case 6:
|
|
return SURROUND51 + shift;
|
|
case 8:
|
|
return SURROUND71 + shift;
|
|
}
|
|
return STEREO;
|
|
}
|
|
|
|
int start_audio_control(audio_ctx_t &ctx) {
|
|
auto fg = util::fail_guard([]() {
|
|
BOOST_LOG(warning) << "There will be no audio"sv;
|
|
});
|
|
|
|
ctx.sink_flag = std::make_unique<std::atomic_bool>(false);
|
|
|
|
// The default sink has not been replaced yet.
|
|
ctx.restore_sink = false;
|
|
|
|
if (!(ctx.control = platf::audio_control())) {
|
|
return 0;
|
|
}
|
|
|
|
auto sink = ctx.control->sink_info();
|
|
if (!sink) {
|
|
// Let the calling code know it failed
|
|
ctx.control.reset();
|
|
return 0;
|
|
}
|
|
|
|
ctx.sink = std::move(*sink);
|
|
|
|
fg.disable();
|
|
return 0;
|
|
}
|
|
|
|
void stop_audio_control(audio_ctx_t &ctx) {
|
|
// restore audio-sink if applicable
|
|
if (!ctx.restore_sink) {
|
|
return;
|
|
}
|
|
|
|
// Change back to the host sink, unless there was none
|
|
const std::string &sink = ctx.sink.host.empty() ? config::audio.sink : ctx.sink.host;
|
|
if (!sink.empty()) {
|
|
// Best effort, it's allowed to fail
|
|
ctx.control->set_sink(sink);
|
|
}
|
|
}
|
|
|
|
void apply_surround_params(opus_stream_config_t &stream, const stream_params_t ¶ms) {
|
|
stream.channelCount = params.channelCount;
|
|
stream.streams = params.streams;
|
|
stream.coupledStreams = params.coupledStreams;
|
|
stream.mapping = params.mapping;
|
|
}
|
|
} // namespace audio
|