/** * @file src/platform/macos/microphone.mm * @brief Definitions for microphone capture on macOS. */ // local includes #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/platform/macos/av_audio.h" namespace platf { using namespace std::literals; struct av_mic_t: public mic_t { AVAudio *av_audio_capture {}; ~av_mic_t() override { [av_audio_capture release]; } capture_e sample(std::vector &sample_in) override { const uint32_t neededBytes = static_cast(sample_in.size() * sizeof(float)); uint8_t *dst = reinterpret_cast(sample_in.data()); uint32_t remaining = neededBytes; while (remaining > 0) { uint32_t avail = 0; void *tail = TPCircularBufferTail(&av_audio_capture->audioSampleBuffer, &avail); if (avail == 0) { // Using 5 second timeout to prevent indefinite hanging dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5LL * NSEC_PER_SEC); if (dispatch_semaphore_wait(av_audio_capture->audioSemaphore, timeout) != 0) { BOOST_LOG(warning) << "Audio sample timeout - no audio data received within 5 seconds"sv; // Fill with silence and return to prevent hanging std::fill(sample_in.begin(), sample_in.end(), 0.0f); return capture_e::timeout; } continue; } const uint32_t toCopy = (avail < remaining) ? avail : remaining; std::memcpy(dst, tail, toCopy); TPCircularBufferConsume(&av_audio_capture->audioSampleBuffer, toCopy); dst += toCopy; remaining -= toCopy; } return capture_e::ok; } }; struct macos_audio_control_t: public audio_control_t { AVCaptureDevice *audio_capture_device {}; public: int set_sink(const std::string &sink) override { BOOST_LOG(warning) << "audio_control_t::set_sink() unimplemented: "sv << sink; return 0; } std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, bool continuous_audio, bool host_audio_enabled) override { auto mic = std::make_unique(); mic->av_audio_capture = [[AVAudio alloc] init]; // Set the host audio enabled flag from the stream configuration mic->av_audio_capture.hostAudioEnabled = host_audio_enabled ? YES : NO; BOOST_LOG(debug) << "Set hostAudioEnabled to: "sv << (host_audio_enabled ? "YES" : "NO"); if (config::audio.sink.empty()) { // Use macOS system-wide audio tap BOOST_LOG(info) << "Using macOS system audio tap for capture."sv; BOOST_LOG(info) << "Sample rate: "sv << sample_rate << ", Frame size: "sv << frame_size << ", Channels: "sv << channels; if ([mic->av_audio_capture setupSystemTap:sample_rate frameSize:frame_size channels:channels]) { BOOST_LOG(error) << "Failed to setup system audio tap."sv; return nullptr; } } else { // Use specified macOS audio sink const char *audio_sink = config::audio.sink.c_str(); BOOST_LOG(info) << "Using configured audio sink "sv << audio_sink << " for capture."sv; if ((audio_capture_device = [AVAudio findMicrophone:[NSString stringWithUTF8String:audio_sink]]) == nullptr) { BOOST_LOG(error) << "opening microphone '"sv << audio_sink << "' failed. Please set a valid input source in the Sunshine config."sv; BOOST_LOG(error) << "Available inputs:"sv; for (NSString *name in [AVAudio microphoneNames]) { BOOST_LOG(error) << "\t"sv << [name UTF8String]; } return nullptr; } if ([mic->av_audio_capture setupMicrophone:audio_capture_device sampleRate:sample_rate frameSize:frame_size channels:channels]) { BOOST_LOG(error) << "Failed to setup microphone."sv; return nullptr; } } return mic; } bool is_sink_available(const std::string &sink) override { BOOST_LOG(warning) << "audio_control_t::is_sink_available() unimplemented: "sv << sink; return true; } std::optional sink_info() override { sink_t sink; return sink; } }; std::unique_ptr audio_control() { return std::make_unique(); } } // namespace platf