2025-11-28 00:35:46 +09:00

321 lines
11 KiB
C++

//*********************************************************
//
// Copyright (c) Microsoft. All rights reserved.
// This code is licensed under the MIT License (MIT).
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************
//
// WASAPIRendering.cpp : Scaffolding associated with the WASAPI Rendering sample application.
//
#include "pch.h"
#include <functiondiscoverykeys.h> // PKEY_Device_FriendlyName
#include "WASAPIRenderer.h"
#include "CmdLine.h"
#include "ToneGen.h"
#include <wil/result.h>
#include <wil/com.h>
int TargetFrequency = 440;
int TargetLatency = 30;
int TargetDurationInSec = 10;
bool ShowHelp;
bool UseConsoleDevice;
bool UseCommunicationsDevice;
bool UseMultimediaDevice;
bool DisableMMCSS;
bool EnableAudioViewManagerService;
wchar_t* OutputEndpoint;
CommandLineSwitch CmdLineArgs[] =
{
{ L"?", L"Print this help", CommandLineSwitch::SwitchTypeNone, &ShowHelp},
{ L"h", L"Print this help", CommandLineSwitch::SwitchTypeNone, &ShowHelp},
{ L"f", L"Sine wave frequency (Hz)", CommandLineSwitch::SwitchTypeInteger, &TargetFrequency, false},
{ L"l", L"Audio Render Latency (ms)", CommandLineSwitch::SwitchTypeInteger, &TargetLatency, false},
{ L"d", L"Sine Wave Duration (s)", CommandLineSwitch::SwitchTypeInteger, &TargetDurationInSec, false},
{ L"w", L"Enable call to AudioViewManagerService", CommandLineSwitch::SwitchTypeNone, &EnableAudioViewManagerService},
{ L"console", L"Use the default console device", CommandLineSwitch::SwitchTypeNone, &UseConsoleDevice},
{ L"communications", L"Use the default communications device", CommandLineSwitch::SwitchTypeNone, &UseCommunicationsDevice},
{ L"multimedia", L"Use the default multimedia device", CommandLineSwitch::SwitchTypeNone, &UseMultimediaDevice},
{ L"endpoint", L"Use the specified endpoint ID", CommandLineSwitch::SwitchTypeString, &OutputEndpoint, true},
};
size_t CmdLineArgLength = ARRAYSIZE(CmdLineArgs);
//
// Print help for the sample
//
void Help(LPCWSTR ProgramName)
{
printf("Usage: %ls [-/][Switch][:][Value]\n\n", ProgramName);
printf("Where Switch is one of the following: \n");
for (size_t i = 0; i < CmdLineArgLength; i += 1)
{
printf(" -%ls: %ls\n", CmdLineArgs[i].SwitchName, CmdLineArgs[i].SwitchHelp);
}
}
//
// Retrieves the device friendly name for a particular device in a device collection.
//
HRESULT GetDeviceName(IMMDeviceCollection* DeviceCollection, UINT DeviceIndex, LPWSTR* _deviceName)
{
wil::com_ptr_nothrow<IMMDevice> device;
wil::unique_cotaskmem_string deviceId;
RETURN_IF_FAILED(DeviceCollection->Item(DeviceIndex, &device));
RETURN_IF_FAILED(device->GetId(&deviceId));
wil::com_ptr_nothrow<IPropertyStore> propertyStore;
RETURN_IF_FAILED(device->OpenPropertyStore(STGM_READ, &propertyStore));
wil::unique_prop_variant friendlyName;
RETURN_IF_FAILED(propertyStore->GetValue(PKEY_Device_FriendlyName, &friendlyName));
wil::unique_cotaskmem_string deviceName;
RETURN_IF_FAILED(wil::str_printf_nothrow(deviceName, L"%ls (%ls)", friendlyName.vt != VT_LPWSTR ? L"Unknown" : friendlyName.pwszVal, deviceId.get()));
*_deviceName = deviceName.release();
return S_OK;
}
//
// Based on the input switches, pick the specified device to use.
//
HRESULT PickDevice(IMMDevice** DeviceToUse, bool* IsDefaultDevice, ERole* DefaultDeviceRole)
{
wil::com_ptr_nothrow<IMMDeviceEnumerator> deviceEnumerator;
wil::com_ptr_nothrow<IMMDeviceCollection> deviceCollection;
wil::com_ptr_nothrow<IMMDevice> device;
*IsDefaultDevice = false; // Assume we're not using the default device.
RETURN_IF_FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&deviceEnumerator)));
//
// First off, if none of the console switches was specified, use the console device.
//
if (!UseConsoleDevice && !UseCommunicationsDevice && !UseMultimediaDevice && OutputEndpoint == nullptr)
{
//
// The user didn't specify an output device, prompt the user for a device and use that.
//
RETURN_IF_FAILED(deviceEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &deviceCollection));
printf("Select an output device:\n");
printf(" 0: Default Console Device\n");
printf(" 1: Default Communications Device\n");
printf(" 2: Default Multimedia Device\n");
UINT deviceCount;
RETURN_IF_FAILED(deviceCollection->GetCount(&deviceCount));
for (UINT i = 0; i < deviceCount; i += 1)
{
wil::unique_cotaskmem_string deviceName;
RETURN_IF_FAILED(GetDeviceName(deviceCollection.get(), i, &deviceName));
printf(" %d: %ls\n", i + 3, deviceName.get());
}
wchar_t choice[10];
_getws_s(choice); // Note: Using the safe CRT version of _getws.
long deviceIndex;
wchar_t* endPointer;
deviceIndex = wcstoul(choice, &endPointer, 0);
if (deviceIndex == 0 && endPointer == choice)
{
printf("unrecognized device index: %ls\n", choice);
return E_UNEXPECTED;
}
switch (deviceIndex)
{
case 0:
UseConsoleDevice = 1;
break;
case 1:
UseCommunicationsDevice = 1;
break;
case 2:
UseMultimediaDevice = 1;
break;
default:
RETURN_IF_FAILED(deviceCollection->Item(deviceIndex - 3, &device));
break;
}
}
else if (OutputEndpoint != nullptr)
{
RETURN_IF_FAILED(deviceEnumerator->GetDevice(OutputEndpoint, &device));
}
if (device == nullptr)
{
ERole deviceRole = eConsole; // Assume we're using the console role.
if (UseConsoleDevice)
{
deviceRole = eConsole;
}
else if (UseCommunicationsDevice)
{
deviceRole = eCommunications;
}
else if (UseMultimediaDevice)
{
deviceRole = eMultimedia;
}
RETURN_IF_FAILED(deviceEnumerator->GetDefaultAudioEndpoint(eRender, deviceRole, &device));
*IsDefaultDevice = true;
*DefaultDeviceRole = deviceRole;
}
*DeviceToUse = device.detach();
return S_OK;
}
int wmain(int argc, wchar_t* argv[])
{
printf("WASAPI Render Shared Event Driven Sample\n");
printf("Copyright (c) Microsoft. All Rights Reserved\n");
printf("\n");
if (!ParseCommandLine(argc, argv, CmdLineArgs, CmdLineArgLength))
{
Help(argv[0]);
return -1;
}
//
// Now that we've parsed our command line, do some semantic checks.
//
//
// First off, show the help for the app if the user asked for it.
//
if (ShowHelp)
{
Help(argv[0]);
return 0;
}
//
// The user can only specify one of -console, -communications or -multimedia or a specific endpoint.
//
if (((UseConsoleDevice != 0) + (UseCommunicationsDevice != 0) + (UseMultimediaDevice != 0) + (OutputEndpoint != nullptr)) > 1)
{
printf("Can only specify one of -Console, -Communications, -Multimedia, or a specific endpoint.\n");
return -1;
}
//
// A GUI application should use COINIT_APARTMENTTHREADED instead of COINIT_MULTITHREADED.
//
if (FAILED(CoInitializeEx(NULL, COINIT_MULTITHREADED)))
{
printf("Unable to initialize COM\n");
return -1;
}
wil::unique_couninitialize_call comUninitialize;
//
// Now that we've parsed our command line, pick the device to render.
//
wil::com_ptr_nothrow<IMMDevice> device;
bool isDefaultDevice;
ERole role;
if (PickDevice(&device, &isDefaultDevice, &role) != S_OK)
{
return -1;
}
printf("Render a %d hz Sine wave for %d seconds\n", TargetFrequency, TargetDurationInSec);
//
// Instantiate a renderer and play a sound for TargetDuration seconds
//
// Configure the renderer to enable stream switching on the specified role if the user specified one of the default devices.
//
CWASAPIRenderer renderer;
renderer.SetUp(device.get(), isDefaultDevice, role, EnableAudioViewManagerService);
if (renderer.Initialize(TargetLatency) == S_OK)
{
//
// We've initialized the renderer. Once we've done that, we know some information about the
// mix format and we can allocate the buffer that we're going to render.
//
// The buffer is going to contain "TargetDuration" seconds worth of PCM data. That means
// we're going to have TargetDuration*samples/second frames multiplied by the frame size.
//
UINT32 renderBufferSizeInBytes = (renderer.BufferSizePerPeriod() * renderer.FrameSize());
size_t renderDataLength = (renderer.SamplesPerSecond() * TargetDurationInSec * renderer.FrameSize()) + (renderBufferSizeInBytes - 1);
size_t renderBufferCount = renderDataLength / (renderBufferSizeInBytes);
//
// Build the render buffer queue.
//
std::forward_list<RenderBuffer> renderQueue;
// Keep an iterator to the tail of the list so we can append elements.
auto renderQueueTail = renderQueue.before_begin();
double theta = 0;
for (size_t i = 0; i < renderBufferCount; i += 1)
{
try
{
// Append another buffer to the queue.
renderQueueTail = renderQueue.emplace_after(renderQueueTail, renderBufferSizeInBytes);
}
catch (std::bad_alloc const&)
{
printf("Unable to allocate render buffer\n");
return -1;
}
RenderBuffer& renderBuffer = *renderQueueTail;
//
// Generate tone data in the buffer.
//
switch (renderer.SampleType())
{
case CWASAPIRenderer::RenderSampleType::Float:
GenerateSineSamples<float>(renderBuffer._buffer.get(), renderBuffer._bufferLength, TargetFrequency,
renderer.ChannelCount(), renderer.SamplesPerSecond(), &theta);
break;
case CWASAPIRenderer::RenderSampleType::Pcm16Bit:
GenerateSineSamples<short>(renderBuffer._buffer.get(), renderBuffer._bufferLength, TargetFrequency,
renderer.ChannelCount(), renderer.SamplesPerSecond(), &theta);
break;
}
}
if (SUCCEEDED(renderer.Start(std::move(renderQueue))))
{
do
{
printf(".");
Sleep(1000);
} while (--TargetDurationInSec);
printf("\n");
renderer.Stop();
}
}
renderer.Shutdown();
return 0;
}