/** * @file src/audio.cpp * @brief Definitions for audio capture and encoding. */ // standard includes #include // lib includes #include // 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; using sample_queue_t = std::shared_ptr>>; 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(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(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(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 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(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(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