// Sound for a recording, with WASAPI: what the computer plays (the default output device, in loopback) // and the microphone (the default input device). Each is asked for in shared mode as 48 kHz stereo // float, which Windows converts any device's own format to (AUTOCONVERTPCM), so nothing here // resamples. Where a device will not convert, its own format is taken if it is 48 kHz float, and its // channels are made stereo here. // // Each packet comes with the QueryPerformanceCounter time its first sample was heard, which places it // on the recording's timeline (module.jai's audio thread mixes them). Loopback sends nothing while // nothing plays: those gaps are silence. A device unplugged mid-recording (a headset) is opened // again, from the new default, once a second. AUDIO_RATE :: 48000; AUDIO_CHANNELS :: 2; Audio_Source_Kind :: enum u8 { SYSTEM; MICROPHONE; } Audio_Source :: struct { kind: Audio_Source_Kind; name: string; // "Speakers (Realtek(R) Audio)"; heap client: *Audio_Client; capture: *Audio_Capture_Client; channels: s32; // as the device gives them: 2, unless it would not convert open: bool; lost_at: s64; // when it was lost (ticks), to open it again a second on; 0 while open error: string; // why it could not be opened; heap } Audio_Packet :: struct { samples: [] float; // interleaved stereo, AUDIO_CHANNELS a frame; empty for silence frames: s64; ticks: s64; // when its first frame was heard, in QueryPerformanceCounter ticks silent: bool; } // The default device of its kind, started. False with why: none, or it will not open. audio_open :: (s: *Audio_Source, kind: Audio_Source_Kind) -> ok: bool, error: string { s.kind = kind; enumerator: *Device_Enumerator; hr := audio_CoCreateInstance(*CLSID_MM_DEVICE_ENUMERATOR, null, CLSCTX_ALL, *IID_MM_DEVICE_ENUMERATOR, xx *enumerator); if FAILED(hr) return false, hresult_text("the audio device list", hr); defer com_release(*enumerator); device: *Audio_Device; flow: s32 = ifx kind == .SYSTEM then E_RENDER else E_CAPTURE; hr = enumerator.vtable.GetDefaultAudioEndpoint(enumerator, flow, E_CONSOLE, *device); if FAILED(hr) return false, ifx kind == .SYSTEM then "no sound output device" else "no microphone"; defer com_release(*device); free(s.name); s.name = copy_string(audio_device_name(device)); hr = device.vtable.Activate(device, *IID_AUDIO_CLIENT, CLSCTX_ALL, null, xx *s.client); if FAILED(hr) return false, hresult_text(tprint("% (%)", s.name, "IAudioClient"), hr); flags: u32 = AUTOCONVERTPCM | SRC_DEFAULT_QUALITY; if kind == .SYSTEM flags |= STREAMFLAGS_LOOPBACK; wanted := WAVEFORMATEX.{wFormatTag = WAVE_FORMAT_IEEE_FLOAT, nChannels = AUDIO_CHANNELS, nSamplesPerSec = AUDIO_RATE, nAvgBytesPerSec = AUDIO_RATE * AUDIO_CHANNELS * 4, nBlockAlign = AUDIO_CHANNELS * 4, wBitsPerSample = 32}; BUFFER :: 2_000_000; // 200 ms, in 100 ns: room for the audio thread being late hr = s.client.vtable.Initialize(s.client, SHAREMODE_SHARED, flags, BUFFER, 0, *wanted, null); s.channels = AUDIO_CHANNELS; if FAILED(hr) { // No conversion here: the device's own format, if it is 48 kHz float, its channels made stereo after. mix: *WAVEFORMATEX; if FAILED(s.client.vtable.GetMixFormat(s.client, *mix)) return false, hresult_text(tprint("% will not give 48 kHz stereo", s.name), hr); defer audio_CoTaskMemFree(mix); is_float := mix.wFormatTag == WAVE_FORMAT_IEEE_FLOAT || (mix.wFormatTag == WAVE_FORMAT_EXTENSIBLE && mix.wBitsPerSample == 32); if !is_float || mix.nSamplesPerSec != AUDIO_RATE return false, tprint("% gives % Hz %, not 48 kHz float, and will not convert", s.name, mix.nSamplesPerSec, ifx is_float then "float" else "PCM"); com_release(*s.client); hr = device.vtable.Activate(device, *IID_AUDIO_CLIENT, CLSCTX_ALL, null, xx *s.client); if FAILED(hr) return false, hresult_text(s.name, hr); hr = s.client.vtable.Initialize(s.client, SHAREMODE_SHARED, flags & ~(AUTOCONVERTPCM | SRC_DEFAULT_QUALITY), BUFFER, 0, mix, null); if FAILED(hr) return false, hresult_text(tprint("% (Initialize)", s.name), hr); s.channels = mix.nChannels; } hr = s.client.vtable.GetService(s.client, *IID_AUDIO_CAPTURE_CLIENT, xx *s.capture); if FAILED(hr) return false, hresult_text(tprint("% (IAudioCaptureClient)", s.name), hr); hr = s.client.vtable.Start(s.client); if FAILED(hr) return false, hresult_text(tprint("% (Start)", s.name), hr); s.open = true; s.lost_at = 0; return true, ""; } // The packets waiting, each as stereo float (temp), oldest first. lost: the device went away // (unplugged, or the default changed): audio_close, and open it again later. audio_take :: (s: *Audio_Source, frequency: s64) -> [] Audio_Packet, lost: bool { packets: [..] Audio_Packet; packets.allocator = temp; while true { next: u32; hr := s.capture.vtable.GetNextPacketSize(s.capture, *next); if hresult_is(hr, AUDCLNT_E_DEVICE_INVALIDATED) return packets, true; if FAILED(hr) || !next break; data: *u8; frames, flags: u32; device_position, qpc_position: u64; // qpc_position: QueryPerformanceCounter time, in 100 ns hr = s.capture.vtable.GetBuffer(s.capture, *data, *frames, *flags, *device_position, *qpc_position); if hresult_is(hr, AUDCLNT_E_DEVICE_INVALIDATED) return packets, true; if FAILED(hr) || hresult_is(hr, AUDCLNT_S_BUFFER_EMPTY) break; packet: Audio_Packet; packet.frames = frames; packet.ticks = cast(s64) (cast(float64) qpc_position * frequency / 10_000_000.0); packet.silent = (flags & BUFFERFLAGS_SILENT) != 0; if !packet.silent && frames { packet.samples = NewArray(cast(s64) frames * AUDIO_CHANNELS, float, initialized = false,, temp); input := cast(*float) data; if s.channels == AUDIO_CHANNELS { memcpy(packet.samples.data, input, cast(s64) frames * AUDIO_CHANNELS * size_of(float)); } else { // Mono to both sides; more channels than two: the first two (front left and right). channels := cast(s64) s.channels; for f: 0..cast(s64) frames - 1 { left := input[f * channels]; right := ifx channels > 1 then input[f * channels + 1] else left; packet.samples[f * 2] = left; packet.samples[f * 2 + 1] = right; } } } s.capture.vtable.ReleaseBuffer(s.capture, frames); array_add(*packets, packet); } return packets, false; } audio_close :: (s: *Audio_Source) { if s.client && s.open s.client.vtable.Stop(s.client); com_release(*s.capture); com_release(*s.client); s.open = false; } audio_free :: (s: *Audio_Source) { audio_close(s); free(s.name); free(s.error); s.name, s.error = "", ""; } // COM on this thread, in the multithreaded apartment, as WASAPI's objects are used from it only. audio_thread_start :: () { audio_CoInitializeEx(null, COINIT_MULTITHREADED); } audio_thread_end :: () { audio_CoUninitialize(); } #scope_file audio_device_name :: (device: *Audio_Device) -> string { store: *Property_Store; if FAILED(device.vtable.OpenPropertyStore(device, STGM_READ, *store)) return ""; defer com_release(*store); value: PROPVARIANT; if FAILED(store.vtable.GetValue(store, *PKEY_DEVICE_FRIENDLY_NAME, *value)) return ""; defer audio_PropVariantClear(*value); VT_LPWSTR :: 31; if value.vt != VT_LPWSTR || !value.pointer return ""; name, ok := wide_to_utf8(cast(*u16) value.pointer,, temp); return ifx ok then name else ""; } // ---------------------------------------------------------------- bindings ole32_dll :: #library,system "ole32"; audio_CoInitializeEx :: (reserved: *void, flags: u32) -> HRESULT #foreign ole32_dll "CoInitializeEx"; audio_CoUninitialize :: () #foreign ole32_dll "CoUninitialize"; audio_CoCreateInstance :: (clsid: *GUID, outer: *void, class_context: u32, iid: *GUID, out: **void) -> HRESULT #foreign ole32_dll "CoCreateInstance"; audio_CoTaskMemFree :: (memory: *void) #foreign ole32_dll "CoTaskMemFree"; audio_PropVariantClear :: (value: *PROPVARIANT) -> HRESULT #foreign ole32_dll "PropVariantClear"; COINIT_MULTITHREADED : u32 : 0; CLSCTX_ALL : u32 : 0x17; E_RENDER : s32 : 0; E_CAPTURE : s32 : 1; E_CONSOLE : s32 : 0; STGM_READ : u32 : 0; SHAREMODE_SHARED : s32 : 0; STREAMFLAGS_LOOPBACK : u32 : 0x00020000; AUTOCONVERTPCM : u32 : 0x80000000; SRC_DEFAULT_QUALITY : u32 : 0x08000000; BUFFERFLAGS_SILENT : u32 : 0x2; WAVE_FORMAT_IEEE_FLOAT : u16 : 3; WAVE_FORMAT_EXTENSIBLE : u16 : 0xFFFE; AUDCLNT_S_BUFFER_EMPTY :: 0x08890001; AUDCLNT_E_DEVICE_INVALIDATED :: 0x88890004; CLSID_MM_DEVICE_ENUMERATOR := #run guid("BCDE0395-E52F-467C-8E3D-C4579291692E"); IID_MM_DEVICE_ENUMERATOR := #run guid("A95664D2-9614-4F35-A746-DE8DB63617E6"); IID_AUDIO_CLIENT := #run guid("1CB9AD4C-DBFA-4C32-B178-C2F568A703B2"); IID_AUDIO_CAPTURE_CLIENT := #run guid("C8ADBD64-E71E-48A0-A4DE-185C395CD317"); PROPERTYKEY :: struct { fmtid: GUID; pid: u32; } PKEY_DEVICE_FRIENDLY_NAME := PROPERTYKEY.{#run guid("A45C254E-DF1C-4EFD-8020-67D146A850E0"), 14}; // A PROPVARIANT, as far as a string in it: its type, then (after 6 reserved bytes) the pointer. PROPVARIANT :: struct { vt: u16; reserved: [3] u16; pointer: *void; padding: *void; } WAVEFORMATEX :: struct { wFormatTag: u16; nChannels: u16; nSamplesPerSec: u32; nAvgBytesPerSec: u32; nBlockAlign: u16; wBitsPerSample: u16; cbSize: u16; } Unknown_Methods :: struct { QueryInterface: (this: *void, iid: *GUID, out: **void) -> HRESULT #c_call; AddRef: (this: *void) -> u32 #c_call; Release: (this: *void) -> u32 #c_call; } Device_Enumerator :: struct { vtable: *struct { using base: Unknown_Methods; EnumAudioEndpoints: *void; GetDefaultAudioEndpoint: (this: *Device_Enumerator, flow: s32, role: s32, out: **Audio_Device) -> HRESULT #c_call; }; } Audio_Device :: struct { vtable: *struct { using base: Unknown_Methods; Activate: (this: *Audio_Device, iid: *GUID, class_context: u32, parameters: *void, out: **void) -> HRESULT #c_call; OpenPropertyStore: (this: *Audio_Device, access: u32, out: **Property_Store) -> HRESULT #c_call; }; } Property_Store :: struct { vtable: *struct { using base: Unknown_Methods; GetCount: *void; GetAt: *void; GetValue: (this: *Property_Store, key: *PROPERTYKEY, out: *PROPVARIANT) -> HRESULT #c_call; }; } Audio_Client :: struct { vtable: *struct { using base: Unknown_Methods; Initialize: (this: *Audio_Client, share_mode: s32, flags: u32, buffer_duration: s64, periodicity: s64, format: *WAVEFORMATEX, session: *GUID) -> HRESULT #c_call; GetBufferSize: *void; GetStreamLatency: *void; GetCurrentPadding: *void; IsFormatSupported: *void; GetMixFormat: (this: *Audio_Client, out: **WAVEFORMATEX) -> HRESULT #c_call; GetDevicePeriod: *void; Start: (this: *Audio_Client) -> HRESULT #c_call; Stop: (this: *Audio_Client) -> HRESULT #c_call; Reset: *void; SetEventHandle: *void; GetService: (this: *Audio_Client, iid: *GUID, out: **void) -> HRESULT #c_call; }; } Audio_Capture_Client :: struct { vtable: *struct { using base: Unknown_Methods; GetBuffer: (this: *Audio_Capture_Client, data: **u8, frames: *u32, flags: *u32, device_position: *u64, qpc_position: *u64) -> HRESULT #c_call; ReleaseBuffer: (this: *Audio_Capture_Client, frames: u32) -> HRESULT #c_call; GetNextPacketSize: (this: *Audio_Capture_Client, out: *u32) -> HRESULT #c_call; }; }