#include "lcs_audio_output.hpp" #include "lcs_audio_mixer.hpp" #include #include #include #include #include #include #if defined(_WIN32) #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #ifndef NOMINMAX #define NOMINMAX #endif #include #include namespace lcs { namespace { struct WaveOutDevice { struct Block { WAVEHDR header{}; std::vector samples; }; HWAVEOUT device{nullptr}; std::vector blocks; std::size_t next_block{}; bool opened{}; bool failed{}; [[nodiscard]] const char *backend_name() const noexcept { return "waveOut"; } bool ensure_open(AudioMixer &mixer) { if (opened) return true; if (failed) return false; WAVEFORMATEX format{}; format.wFormatTag = WAVE_FORMAT_PCM; format.nChannels = static_cast(AudioMixer::kOutputChannels); format.nSamplesPerSec = AudioMixer::kSampleRate; format.wBitsPerSample = 16u; format.nBlockAlign = static_cast(AudioMixer::kOutputChannels * sizeof(std::int16_t)); format.nAvgBytesPerSec = AudioMixer::kSampleRate * format.nBlockAlign; const MMRESULT open_result = waveOutOpen(&device, WAVE_MAPPER, &format, 0, 0, CALLBACK_NULL); if (open_result != MMSYSERR_NOERROR) { if (AudioMixer::diagnostics_enabled()) std::cerr << "[audio-host] waveOutOpen failed code=" << open_result << "\n"; failed = true; device = nullptr; return false; } waveOutPause(device); blocks.resize(AudioMixer::kBlockCount); next_block = 0u; opened = true; mixer.note_device_opened(); if (AudioMixer::diagnostics_enabled()) std::cerr << "[audio-host] waveOut 44100Hz stereo block_frames=" << AudioMixer::kBlockFrames << " blocks=" << AudioMixer::kBlockCount << " prebuffer_blocks=" << mixer.prebuffer_blocks() << " prebuffer_ms=" << (mixer.prebuffer_blocks() * AudioMixer::kBlockFrames * 1000u / AudioMixer::kSampleRate) << "\n"; return true; } [[nodiscard]] std::size_t outstanding_blocks() const { return static_cast(std::count_if( blocks.begin(), blocks.end(), [](const Block &block) { return (block.header.dwFlags & WHDR_PREPARED) != 0u && (block.header.dwFlags & WHDR_DONE) == 0u; })); } bool begin_block(std::int16_t *&samples) { Block &block = blocks[next_block]; if ((block.header.dwFlags & WHDR_PREPARED) != 0u) { if ((block.header.dwFlags & WHDR_DONE) == 0u) return false; waveOutUnprepareHeader(device, &block.header, sizeof(WAVEHDR)); } block.samples.resize(AudioMixer::kBlockSamples); samples = block.samples.data(); return true; } bool commit_block() { Block &block = blocks[next_block]; block.header = WAVEHDR{}; block.header.lpData = reinterpret_cast(block.samples.data()); block.header.dwBufferLength = static_cast(block.samples.size() * sizeof(std::int16_t)); const MMRESULT prepare_result = waveOutPrepareHeader(device, &block.header, sizeof(WAVEHDR)); if (prepare_result != MMSYSERR_NOERROR) { if (AudioMixer::diagnostics_enabled()) std::cerr << "[audio-host] waveOutPrepareHeader failed code=" << prepare_result << "\n"; return false; } const MMRESULT write_result = waveOutWrite(device, &block.header, sizeof(WAVEHDR)); if (write_result != MMSYSERR_NOERROR) { if (AudioMixer::diagnostics_enabled()) std::cerr << "[audio-host] waveOutWrite failed code=" << write_result << "\n"; waveOutUnprepareHeader(device, &block.header, sizeof(WAVEHDR)); return false; } next_block = (next_block + 1u) % blocks.size(); return true; } void pause_playback() { waveOutPause(device); } void resume_playback() { waveOutRestart(device); } void close_device(bool playback_started) { if (!opened || device == nullptr) return; if (!playback_started) waveOutRestart(device); waveOutReset(device); for (Block &block : blocks) { if ((block.header.dwFlags & WHDR_PREPARED) != 0u) waveOutUnprepareHeader(device, &block.header, sizeof(WAVEHDR)); } waveOutClose(device); device = nullptr; opened = false; blocks.clear(); } }; using AudioDevice = WaveOutDevice; #else #include namespace lcs { namespace { struct SdlAudioDevice { SDL_AudioDeviceID device{}; std::vector block; bool opened{}; bool failed{}; [[nodiscard]] const char *backend_name() const noexcept { return "SDL"; } bool ensure_open(AudioMixer &mixer) { if (opened) return true; if (failed) return false; if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) { std::cerr << "[audio-host] SDL_InitSubSystem failed: " << SDL_GetError() << "\n"; failed = true; return false; } SDL_AudioSpec want{}; want.freq = static_cast(AudioMixer::kSampleRate); want.format = AUDIO_S16SYS; want.channels = static_cast(AudioMixer::kOutputChannels); want.samples = static_cast(AudioMixer::kBlockFrames); SDL_AudioSpec got{}; device = SDL_OpenAudioDevice(nullptr, 0, &want, &got, 0); if (device == 0) { std::cerr << "[audio-host] SDL_OpenAudioDevice failed: " << SDL_GetError() << "\n"; failed = true; return false; } SDL_PauseAudioDevice(device, 1); block.resize(AudioMixer::kBlockSamples); opened = true; mixer.note_device_opened(); std::cerr << "[audio-host] SDL " << got.freq << "Hz channels=" << static_cast(got.channels) << " prebuffer_ms=" << (mixer.prebuffer_blocks() * AudioMixer::kBlockFrames * 1000u / AudioMixer::kSampleRate) << "\n"; return true; } [[nodiscard]] std::size_t outstanding_blocks() const { if (device == 0) return 0u; return SDL_GetQueuedAudioSize(device) / (AudioMixer::kBlockSamples * sizeof(std::int16_t)); } bool begin_block(std::int16_t *&samples) { if (outstanding_blocks() >= AudioMixer::kBlockCount) return false; if (block.size() != AudioMixer::kBlockSamples) block.resize(AudioMixer::kBlockSamples); samples = block.data(); return true; } bool commit_block() { return SDL_QueueAudio(device, block.data(), static_cast(AudioMixer::kBlockSamples * sizeof(std::int16_t))) == 0; } void pause_playback() { SDL_PauseAudioDevice(device, 1); } void resume_playback() { SDL_PauseAudioDevice(device, 0); } void close_device(bool) { if (!opened || device == 0) return; SDL_PauseAudioDevice(device, 1); SDL_ClearQueuedAudio(device); SDL_CloseAudioDevice(device); device = 0; opened = false; block.clear(); } }; using AudioDevice = SdlAudioDevice; #endif struct AudioHost { std::mutex mutex; AudioMixer mixer; AudioDevice device; }; AudioHost &audio_host() { static AudioHost host; return host; } } bool audio_output_enabled() { static const bool enabled = [] { if (const char *text = std::getenv("PSPRECOMP_AUDIO")) return *text != '\0' && std::string(text) != "0"; return true; }(); return enabled; } void audio_output_submit(std::span pcm, std::uint32_t frames, bool stereo, std::uint32_t left, std::uint32_t right, std::uint32_t source_rate, std::uint32_t channel, std::uint64_t start_time_us, std::uint64_t now_us) { if (!audio_output_enabled()) return; AudioHost &host = audio_host(); std::lock_guard guard(host.mutex); host.mixer.submit(pcm, frames, stereo, left, right, source_rate, channel, start_time_us, now_us, host.device); } void audio_output_advance(std::uint64_t guest_time_us) { if (!audio_output_enabled()) return; AudioHost &host = audio_host(); std::lock_guard guard(host.mutex); host.mixer.advance(guest_time_us, host.device); host.mixer.write_summary(guest_time_us, host.device); } void audio_output_reset_channel(std::uint32_t channel) { AudioHost &host = audio_host(); std::lock_guard guard(host.mutex); host.mixer.reset_channel(channel); } void audio_output_shutdown() { AudioHost &host = audio_host(); std::lock_guard guard(host.mutex); host.mixer.shutdown(host.device); } }