bsnes/ruby/audio/waveout.cpp

138 lines
4.4 KiB
C++

#include <mmsystem.h>
auto CALLBACK waveOutCallback(HWAVEOUT handle, UINT message, DWORD_PTR userData, DWORD_PTR, DWORD_PTR) -> void;
struct AudioWaveOut : AudioDriver {
AudioWaveOut& self = *this;
AudioWaveOut(Audio& super) : AudioDriver(super) {}
~AudioWaveOut() { terminate(); }
auto create() -> bool override {
super.setChannels(2);
super.setFrequency(44100);
super.setLatency(512);
return initialize();
}
auto driver() -> string override { return "waveOut"; }
auto ready() -> bool override { return true; }
auto hasDevices() -> vector<string> override {
vector<string> devices{"Default"};
for(uint index : range(waveOutGetNumDevs())) {
WAVEOUTCAPS caps{};
if(waveOutGetDevCaps(index, &caps, sizeof(WAVEOUTCAPS)) == MMSYSERR_NOERROR) {
devices.append((const char*)utf8_t(caps.szPname));
}
}
return devices;
}
auto hasBlocking() -> bool override { return true; }
auto hasDynamic() -> bool override { return true; }
auto hasFrequencies() -> vector<uint> override { return {44100}; }
auto hasLatencies() -> vector<uint> override { return {512, 384, 320, 256, 192, 160, 128, 96, 80, 64, 48, 40, 32}; }
auto setBlocking(bool blocking) -> bool override { return true; }
auto setDynamic(bool dynamic) -> bool override { return initialize(); }
auto setLatency(uint latency) -> bool override { return initialize(); }
auto clear() -> void override {
for(auto& header : headers) {
memory::fill(header.lpData, frameCount * 4);
}
}
auto level() -> double override {
return (double)((blockQueue * frameCount) + frameIndex) / (blockCount * frameCount);
}
auto output(const double samples[]) -> void override {
uint16_t lsample = sclamp<16>(samples[0] * 32767.0);
uint16_t rsample = sclamp<16>(samples[1] * 32767.0);
auto block = (uint32_t*)headers[blockIndex].lpData;
block[frameIndex] = lsample << 0 | rsample << 16;
if(++frameIndex >= frameCount) {
frameIndex = 0;
if(self.dynamic) {
while(waveOutWrite(handle, &headers[blockIndex], sizeof(WAVEHDR)) == WAVERR_STILLPLAYING);
InterlockedIncrement(&blockQueue);
} else while(true) {
auto result = waveOutWrite(handle, &headers[blockIndex], sizeof(WAVEHDR));
if(!self.blocking || result != WAVERR_STILLPLAYING) break;
InterlockedIncrement(&blockQueue);
}
if(++blockIndex >= blockCount) {
blockIndex = 0;
}
}
}
private:
auto initialize() -> bool {
terminate();
auto deviceIndex = hasDevices().find(self.device);
if(!deviceIndex) deviceIndex = 0;
WAVEFORMATEX format{};
format.wFormatTag = WAVE_FORMAT_PCM;
format.nChannels = 2;
format.nSamplesPerSec = 44100;
format.nBlockAlign = 4;
format.wBitsPerSample = 16;
format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign;
format.cbSize = 0; //not sizeof(WAVEFORMAT); size of extra information after WAVEFORMATEX
//-1 = default; 0+ = specific device; subtract -1 as hasDevices() includes "Default" entry
waveOutOpen(&handle, (int)*deviceIndex - 1, &format, (DWORD_PTR)waveOutCallback, (DWORD_PTR)this, CALLBACK_FUNCTION);
frameCount = self.latency;
blockCount = 32;
frameIndex = 0;
blockIndex = 0;
blockQueue = 0;
headers.resize(blockCount);
for(auto& header : headers) {
memory::fill(&header, sizeof(WAVEHDR));
header.lpData = (LPSTR)LocalAlloc(LMEM_FIXED, frameCount * 4);
header.dwBufferLength = frameCount * 4;
waveOutPrepareHeader(handle, &header, sizeof(WAVEHDR));
}
waveOutSetVolume(handle, 0xffff'ffff); //100% volume (65535 left, 65535 right)
waveOutRestart(handle);
return true;
}
auto terminate() -> void {
if(!handle) return;
waveOutPause(handle);
waveOutReset(handle);
for(auto& header : headers) {
waveOutUnprepareHeader(handle, &header, sizeof(WAVEHDR));
LocalFree(header.lpData);
}
waveOutClose(handle);
handle = nullptr;
headers.reset();
}
HWAVEOUT handle = nullptr;
vector<WAVEHDR> headers;
uint frameCount = 0;
uint blockCount = 0;
uint frameIndex = 0;
uint blockIndex = 0;
public:
LONG blockQueue = 0;
};
auto CALLBACK waveOutCallback(HWAVEOUT handle, UINT message, DWORD_PTR userData, DWORD_PTR, DWORD_PTR) -> void {
auto instance = (AudioWaveOut*)userData;
if(instance->blockQueue > 0) InterlockedDecrement(&instance->blockQueue);
}