Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ if(SENDSPIN_CLI_BUILD_TESTS)
tests/pcm_volume_test.cpp
tests/pipewire_sink_test.cpp
tests/player_listener_test.cpp
tests/portaudio_device_test.cpp
tests/sink_recovery_test.cpp
tests/state_store_test.cpp
tests/supported_formats_test.cpp
Expand Down
8 changes: 7 additions & 1 deletion docs/wiki/Advanced-Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ pair split on the first colon (`coreaudio:2`, `portaudio:2`, `pulse:<sink>`,
`pipewire:<node>`), or an ALSA PCM name such as `hw:1,0`, `plughw:1,0`, or `default`. `plughw:` lets ALSA convert
rate and format for a device that refuses the stream as it arrives.

A PortAudio name listed under several host APIs, as every device is on Windows, is refused
on its own; put `-l`'s host API column in front of it, e.g.
`portaudio:directsound:Speakers (USB DAC)`.

`default` follows the host's normal audio configuration. Under a system service, name
a hardware device such as `hw:1,0` instead; the service does not have a logged-in
desktop audio session.
Expand Down Expand Up @@ -88,7 +92,9 @@ argument the way `-o` does:
- On macOS, `coreaudio` for the system default input, which is followed if it moves, or
`coreaudio:<index|name>` for one device from `-l`'s list.
- `portaudio` for PortAudio's default input as of startup, or `portaudio:<index|name>`
for one device from `-l`'s list, in a build with the PortAudio backend.
for one device from `-l`'s list, in a build with the PortAudio backend. A name listed
under several host APIs takes the one from `-l`'s host API column in front of it:
`portaudio:directsound:<name>`.
- `loopback` or `loopback:<sink>` for what this host plays; see
[Stream what this host plays](#stream-what-this-host-plays).
- `tone` and `null`, which need no sound card: `tone` streams a 440 Hz test tone, which
Expand Down
6 changes: 4 additions & 2 deletions docs/wiki/Getting-Started-on-Windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,10 @@ Get-ChildItem .\sendspin-cli-0.5.0-windows-x86_64 | Unblock-File
.\sendspin-cli-0.5.0-windows-x86_64\sendspin-cli.exe
```

`-l` lists what it can play through; `-o portaudio:<index>` picks one. Ctrl+C and closing
the window stop it cleanly, and it handles logging off and shutting down the same way.
`-l` lists what it can play through. Windows lists each device once per host API, so pick one
by that column and its name, e.g. `-o "portaudio:directsound:Speakers (USB DAC)"`; an index
works too, but shifts as devices come and go. Ctrl+C and closing the window stop it cleanly,
and it handles logging off and shutting down the same way.

## Next

Expand Down
10 changes: 7 additions & 3 deletions src/audio_sink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -488,14 +488,18 @@ void print_audio_devices(std::FILE* out) {
#endif

#ifdef SENDSPIN_CLI_HAVE_PORTAUDIO
std::fprintf(out, "\nPortAudio output devices on this host (-o portaudio:<index|name>):\n");
std::fprintf(out, "\nPortAudio output devices on this host "
"(-o portaudio:<index|name|host API:name>):\n");
PortAudioSink::list_devices(out);
std::fprintf(out,
"\nThe name is the form worth writing down. PortAudio numbers devices as it\n"
"walks each host API, so an index shifts as devices come and go; a name is\n"
"matched in full and case-insensitively, and one that matches more than one\n"
"device is refused rather than guessed at. -o portaudio with no device at all\n"
"follows this host's default output, resolved afresh at every stream.\n"
"device is refused rather than guessed at. Where a device is listed under\n"
"several host APIs, as every one is on Windows, put the host API column in\n"
"front of the name, e.g. -o portaudio:directsound:<name>. -o portaudio with no\n"
"device at all follows this host's default output, resolved afresh at every\n"
"stream.\n"
"\nInput-only devices are left out, since -o cannot play through them. The\n"
"rate on the device's own line is its default; the rates below it are what it\n"
"will actually take, asked for the same way a stream would ask. Only the four\n"
Expand Down
3 changes: 2 additions & 1 deletion src/audio_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,8 @@ void print_capture_devices(std::FILE* out) {
"the first time --input opens a device.\n");
#endif
#ifdef SENDSPIN_CLI_HAVE_PORTAUDIO
std::fprintf(out, "\nPortAudio input devices on this host (--input portaudio:<index|name>):\n");
std::fprintf(out, "\nPortAudio input devices on this host "
"(--input portaudio:<index|name|host API:name>):\n");
PortAudioSource::list_devices(out);
std::fprintf(out,
"\nA capture stream runs at one format for the whole run: 48000 Hz where the\n"
Expand Down
130 changes: 107 additions & 23 deletions src/portaudio_device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,107 @@ bool pa_has_channels(const PaDeviceInfo* info, PaDirection direction) {
0;
}

const char* pa_host_api_prefix(PaHostApiTypeId type) {
switch (type) {
case paDirectSound:
return "directsound";
case paMME:
return "mme";
case paASIO:
return "asio";
case paCoreAudio:
return "coreaudio";
case paOSS:
return "oss";
case paALSA:
return "alsa";
case paWDMKS:
return "wdmks";
case paJACK:
return "jack";
case paWASAPI:
return "wasapi";
default:
return nullptr;
}
}

const char* pa_host_api_label(PaHostApiIndex host_api) {
const PaHostApiInfo* host = Pa_GetHostApiInfo(host_api);
if (host == nullptr) {
return "(unknown host API)";
}
const char* prefix = pa_host_api_prefix(host->type);
return (prefix != nullptr) ? prefix : host->name;
}

bool match_pa_device_name(const std::string& device, const std::vector<PaDeviceEntry>& entries,
PaDirection direction, PaDeviceIndex& out, std::string& error) {
const std::string flag = flag_of(direction);
const std::string spec = flag + " portaudio:" + device;
std::vector<const PaDeviceEntry*> matches;
for (const PaDeviceEntry& entry : entries) {
if (iequals(device, entry.name.c_str())) {
matches.push_back(&entry);
}
}

// Read as <host api>:<name> only when no device has the whole name, so none that resolved
// before resolves differently now.
const size_t colon = device.find(':');
if (matches.empty() && colon != std::string::npos) {
const std::string host_api = device.substr(0, colon);
const std::string name = device.substr(colon + 1);
for (const PaDeviceEntry& entry : entries) {
if (iequals(host_api, entry.host_api) && iequals(name, entry.name.c_str())) {
matches.push_back(&entry);
}
}
}

const std::string noun = noun_of(direction);
if (matches.empty()) {
error = spec + ": no " + noun + " device by that name -- run with -l to list them";
return false;
}
if (matches.size() > 1) {
// A prefix only tells them apart if each sits under a different, nameable host API.
bool by_host_api = true;
std::string listed;
for (size_t i = 0; i < matches.size(); ++i) {
const char* host_api = matches[i]->host_api;
for (size_t j = 0; j < i; ++j) {
const char* earlier = matches[j]->host_api;
if (host_api != nullptr && earlier != nullptr &&
std::strcmp(host_api, earlier) == 0) {
by_host_api = false;
}
}
if (host_api == nullptr) {
by_host_api = false;
}
Comment on lines +193 to +195
listed += (listed.empty() ? "" : ", ") + std::to_string(matches[i]->index);
if (host_api != nullptr) {
listed += std::string(" ") + host_api;
}
}
error = spec + ": " + std::to_string(matches.size()) + " " + noun +
" devices share that name (" + listed + ") -- ";
if (by_host_api) {
// Quoted, since device names carry spaces and parentheses.
error += "put its host API in front to pick one, e.g. " + flag +
" \"portaudio:" + matches.front()->host_api + ":" + matches.front()->name +
"\"";
} else {
error += "name the one you mean by index instead";
}
return false;
}

out = matches.front()->index;
return true;
}

bool resolve_pa_device(const std::string& device, PaDirection direction, PaDeviceIndex& out,
std::string& error) {
const PaDeviceIndex count = Pa_GetDeviceCount();
Expand Down Expand Up @@ -162,33 +263,16 @@ bool resolve_pa_device(const std::string& device, PaDirection direction, PaDevic
return true;
}

std::vector<PaDeviceIndex> matches;
std::vector<PaDeviceEntry> entries;
for (PaDeviceIndex i = 0; i < count; ++i) {
const PaDeviceInfo* info = Pa_GetDeviceInfo(i);
if (pa_has_channels(info, direction) && iequals(device, info->name)) {
matches.push_back(i);
}
}
if (matches.empty()) {
error = spec + ": no " + noun + " device by that name -- run with -l to list them";
return false;
}
if (matches.size() > 1) {
std::string indices;
for (const PaDeviceIndex index : matches) {
if (!indices.empty()) {
indices += ", ";
}
indices += std::to_string(index);
if (pa_has_channels(info, direction) && info->name != nullptr) {
const PaHostApiInfo* host = Pa_GetHostApiInfo(info->hostApi);
entries.push_back(
{i, info->name, (host != nullptr) ? pa_host_api_prefix(host->type) : nullptr});
}
error = spec + ": " + std::to_string(matches.size()) + " " + noun +
" devices share that name (indices " + indices +
") -- name the one you mean by index instead";
return false;
}

out = matches.front();
return true;
return match_pa_device_name(device, entries, direction, out, error);
}

} // namespace sendspin_cli
23 changes: 21 additions & 2 deletions src/portaudio_device.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <cstdint>
#include <mutex>
#include <string>
#include <vector>

namespace sendspin_cli {

Expand Down Expand Up @@ -69,8 +70,26 @@ bool pa_format_for(uint8_t bits_per_sample, PaSampleFormat& format);
/// True if the device has channels on that side.
bool pa_has_channels(const PaDeviceInfo* info, PaDirection direction);

/// Resolves a PortAudio device spec: empty is the default, digits an index, else a unique name.
/// PortAudio must be initialized.
/// The prefix a device spec names a host API by, e.g. "wasapi"; nullptr for one it cannot name.
const char* pa_host_api_prefix(PaHostApiTypeId type);

/// The host API column -l prints: its prefix, else PortAudio's own name for it.
const char* pa_host_api_label(PaHostApiIndex host_api);

/// One device as the name lookup sees it.
struct PaDeviceEntry {
PaDeviceIndex index;
std::string name;
const char* host_api; ///< pa_host_api_prefix() of its host API, or nullptr
};

/// Picks the one device `device` names, whole or as <host api>:<name>, out of the devices on
/// that side.
bool match_pa_device_name(const std::string& device, const std::vector<PaDeviceEntry>& entries,
PaDirection direction, PaDeviceIndex& out, std::string& error);

/// Resolves a PortAudio device spec: empty is the default, digits an index, else a unique name,
/// optionally behind its host API prefix. PortAudio must be initialized.
bool resolve_pa_device(const std::string& device, PaDirection direction, PaDeviceIndex& out,
std::string& error);

Expand Down
6 changes: 2 additions & 4 deletions src/portaudio_sink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,9 @@ void PortAudioSink::list_devices(std::FILE* out) {
std::fprintf(out, " idx name host API "
"out ch default rate\n");
}
const PaHostApiInfo* host = Pa_GetHostApiInfo(info->hostApi);
std::fprintf(out, " %3d %-38s %-12s %2d ch %6.0f Hz%s\n", static_cast<int>(i),
info->name, (host != nullptr) ? host->name : "(unknown host API)",
info->maxOutputChannels, info->defaultSampleRate,
(i == fallback) ? " (system default)" : "");
info->name, pa_host_api_label(info->hostApi), info->maxOutputChannels,
info->defaultSampleRate, (i == fallback) ? " (system default)" : "");
print_device_capabilities(out, i);
++listed;
}
Expand Down
6 changes: 2 additions & 4 deletions src/portaudio_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,9 @@ void PortAudioSource::list_devices(std::FILE* out) {
std::fprintf(out, " idx name host API "
" in ch default rate\n");
}
const PaHostApiInfo* host = Pa_GetHostApiInfo(info->hostApi);
std::fprintf(out, " %3d %-38s %-12s %2d ch %6.0f Hz%s\n", static_cast<int>(i),
info->name, (host != nullptr) ? host->name : "(unknown host API)",
info->maxInputChannels, info->defaultSampleRate,
(i == fallback) ? " (system default)" : "");
info->name, pa_host_api_label(info->hostApi), info->maxInputChannels,
info->defaultSampleRate, (i == fallback) ? " (system default)" : "");
++listed;
}

Expand Down
Loading
Loading