Fix nodes

This commit is contained in:
Joey Yakimowich-Payne 2026-02-12 16:52:00 -07:00
commit 3c1c86f952
6 changed files with 297 additions and 57 deletions

View file

@ -107,10 +107,12 @@ bool MatchesRule(const NodeInfo& node, const RuleMatch& match) {
struct StreamData {
pw_stream* stream = nullptr;
pw_impl_module* module = nullptr;
pw_proxy* proxy = nullptr;
spa_hook listener{};
pw_thread_loop* loop = nullptr;
bool is_source = false;
bool loopback = false;
bool linger = false;
std::string target_node;
std::string name;
bool ready = false;
@ -175,6 +177,42 @@ static const pw_proxy_events kLinkProxyEvents = {
.error = LinkProxyError,
};
void NodeProxyBound(void* data, uint32_t global_id) {
auto* sd = static_cast<StreamData*>(data);
if (!sd) return;
sd->node_id = global_id;
sd->ready = true;
if (sd->loop) {
pw_thread_loop_signal(sd->loop, false);
}
}
void NodeProxyRemoved(void* data) {
auto* sd = static_cast<StreamData*>(data);
if (!sd) return;
sd->ready = true;
if (sd->loop) {
pw_thread_loop_signal(sd->loop, false);
}
}
void NodeProxyError(void* data, int, int res, const char* message) {
auto* sd = static_cast<StreamData*>(data);
if (!sd) return;
sd->failed = true;
sd->error = message ? message : spa_strerror(res);
if (sd->loop) {
pw_thread_loop_signal(sd->loop, false);
}
}
static const pw_proxy_events kNodeProxyEvents = {
.version = PW_VERSION_PROXY_EVENTS,
.bound = NodeProxyBound,
.removed = NodeProxyRemoved,
.error = NodeProxyError,
};
void StreamProcess(void* data) {
auto* stream_data = static_cast<StreamData*>(data);
if (!stream_data || !stream_data->stream) {
@ -435,7 +473,29 @@ struct Client::Impl {
void Client::Impl::NodeInfoChanged(void* data, const struct pw_node_info* info) {
auto* np = static_cast<NodeProxyData*>(data);
if (!np || !info || np->params_subscribed) return;
if (!np || !info) return;
auto* impl = static_cast<Client::Impl*>(np->impl_ptr);
bool notify = false;
if (impl && info->props) {
const char* virt_str = spa_dict_lookup(info->props, PW_KEY_NODE_VIRTUAL);
if (virt_str) {
std::lock_guard<std::mutex> lock(impl->cache_mutex);
auto it = impl->nodes.find(np->node_id);
if (it != impl->nodes.end()) {
bool new_virtual = spa_streq(virt_str, "true");
if (it->second.is_virtual != new_virtual) {
it->second.is_virtual = new_virtual;
notify = true;
}
}
}
}
if (notify) {
impl->NotifyChange();
}
if (np->params_subscribed) return;
for (uint32_t i = 0; i < info->n_params; ++i) {
if (info->params[i].id == SPA_PARAM_Props &&
@ -508,15 +568,18 @@ void Client::Impl::NodeParamChanged(void* data, int, uint32_t id,
}
}
bool is_virtual = false;
bool uses_monitor_volume = false;
{
std::lock_guard<std::mutex> lock(impl->cache_mutex);
is_virtual = impl->virtual_streams.count(np->node_id) > 0;
auto node_it = impl->nodes.find(np->node_id);
if (node_it != impl->nodes.end()) {
uses_monitor_volume = node_it->second.is_virtual;
}
}
float effective_vol = is_virtual && mon_volume >= 0.0f ? mon_volume : volume;
bool effective_mute = is_virtual && found_mon_mute ? mon_mute : mute;
bool effective_found_mute = is_virtual ? found_mon_mute : found_mute;
float effective_vol = uses_monitor_volume && mon_volume >= 0.0f ? mon_volume : volume;
bool effective_mute = uses_monitor_volume && found_mon_mute ? mon_mute : mute;
bool effective_found_mute = uses_monitor_volume ? found_mon_mute : found_mute;
bool changed = false;
{
@ -855,16 +918,27 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
{
std::lock_guard<std::mutex> lock(cache_mutex);
for (const auto& entry : nodes) {
if (entry.second.name == stream_name) {
return {Status::Error(StatusCode::kInvalidArgument, "duplicate node name"), 0};
}
}
for (const auto& entry : virtual_streams) {
if (entry.second && entry.second->name == stream_name) {
return {Status::Error(StatusCode::kInvalidArgument, "duplicate node name"), 0};
}
}
for (const auto& entry : nodes) {
if (entry.second.name == stream_name) {
uint32_t existing_id = entry.first;
auto adopted = std::make_unique<StreamData>();
adopted->linger = true;
adopted->loop = thread_loop;
adopted->is_source = is_source;
adopted->name = stream_name;
adopted->node_id = existing_id;
adopted->ready = true;
if (options.format.rate != 0) adopted->rate = options.format.rate;
if (options.format.channels != 0) adopted->channels = options.format.channels;
virtual_streams.emplace(existing_id, std::move(adopted));
return {Status::Ok(), existing_id};
}
}
if (options.behavior == VirtualBehavior::kLoopback && options.target_node) {
bool found_target = false;
for (const auto& entry : nodes) {
@ -976,29 +1050,53 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
return {Status::Ok(), node_id};
}
pw_properties* props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, media_category,
PW_KEY_MEDIA_ROLE, "Music",
PW_KEY_MEDIA_CLASS, media_class_value.c_str(),
PW_KEY_NODE_NAME, stream_name.c_str(),
PW_KEY_MEDIA_NAME, display_name.c_str(),
PW_KEY_NODE_DESCRIPTION, display_name.c_str(),
PW_KEY_NODE_VIRTUAL, "true",
nullptr);
// Build audio position string for channels (e.g. "FL,FR" for stereo).
std::string audio_position;
{
static const char* const kChannelNames[] = {
"FL", "FR", "FC", "LFE", "RL", "RR", "SL", "SR"
};
uint32_t ch = options.format.channels;
for (uint32_t i = 0; i < ch && i < 8; ++i) {
if (i > 0) audio_position += ',';
audio_position += kChannelNames[i];
}
if (audio_position.empty()) audio_position = "FL,FR";
}
pw_properties* props = pw_properties_new(
"factory.name", "support.null-audio-sink",
PW_KEY_NODE_NAME, stream_name.c_str(),
PW_KEY_NODE_DESCRIPTION, display_name.c_str(),
PW_KEY_MEDIA_CLASS, media_class_value.c_str(),
PW_KEY_NODE_VIRTUAL, "true",
"audio.position", audio_position.c_str(),
nullptr);
#ifndef WARPPIPE_TESTING
pw_properties_set(props, PW_KEY_OBJECT_LINGER, "true");
#endif
if (!props) {
return {Status::Error(StatusCode::kInternal, "failed to allocate stream properties"), 0};
return {Status::Error(StatusCode::kInternal, "failed to allocate node properties"), 0};
}
if (node_group) {
pw_properties_set(props, PW_KEY_NODE_GROUP, node_group);
}
pw_stream* stream = pw_stream_new(core, stream_name.c_str(), props);
if (!stream) {
return {Status::Error(StatusCode::kUnavailable, "failed to create pipewire stream"), 0};
pw_proxy* proxy = reinterpret_cast<pw_proxy*>(
pw_core_create_object(core, "adapter",
PW_TYPE_INTERFACE_Node,
PW_VERSION_NODE,
&props->dict, 0));
pw_properties_free(props);
if (!proxy) {
return {Status::Error(StatusCode::kUnavailable, "failed to create virtual node"), 0};
}
auto stream_data = std::make_unique<StreamData>();
stream_data->stream = stream;
stream_data->proxy = proxy;
#ifndef WARPPIPE_TESTING
stream_data->linger = true;
#endif
stream_data->loop = thread_loop;
stream_data->is_source = is_source;
stream_data->loopback = false;
@ -1013,28 +1111,11 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
stream_data->channels = options.format.channels;
}
pw_stream_add_listener(stream, &stream_data->listener, &kStreamEvents, stream_data.get());
pw_proxy_add_listener(proxy, &stream_data->listener, &kNodeProxyEvents, stream_data.get());
const struct spa_pod* params[1];
uint8_t buffer[1024];
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
spa_audio_info_raw audio_info{};
audio_info.format = SPA_AUDIO_FORMAT_F32;
audio_info.rate = stream_data->rate;
audio_info.channels = stream_data->channels;
params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &audio_info);
enum pw_direction direction = is_source ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT;
enum pw_stream_flags flags = PW_STREAM_FLAG_MAP_BUFFERS;
int res = pw_stream_connect(stream, direction, PW_ID_ANY, flags, params, 1);
if (res < 0) {
pw_stream_destroy(stream);
return {Status::Error(StatusCode::kUnavailable, "failed to connect pipewire stream"), 0};
}
uint32_t node_id = pw_stream_get_node_id(stream);
uint32_t node_id = SPA_ID_INVALID;
int wait_attempts = 0;
while (node_id == SPA_ID_INVALID && !stream_data->failed && wait_attempts < 3) {
while (node_id == SPA_ID_INVALID && !stream_data->failed && !stream_data->ready && wait_attempts < 3) {
int wait_res = pw_thread_loop_timed_wait(thread_loop, kSyncWaitSeconds);
if (wait_res == -ETIMEDOUT) {
break;
@ -1044,17 +1125,17 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
}
if (stream_data->failed) {
std::string error = stream_data->error.empty() ? "stream entered error state" : stream_data->error;
pw_stream_destroy(stream);
std::string error = stream_data->error.empty() ? "node creation failed" : stream_data->error;
pw_proxy_destroy(proxy);
return {Status::Error(StatusCode::kUnavailable, std::move(error)), 0};
}
node_id = stream_data->node_id;
if (node_id == SPA_ID_INVALID) {
pw_stream_destroy(stream);
return {Status::Error(StatusCode::kTimeout, "timed out waiting for stream node id"), 0};
pw_proxy_destroy(proxy);
return {Status::Error(StatusCode::kTimeout, "timed out waiting for virtual node id"), 0};
}
stream_data->node_id = node_id;
stream_data->ready = true;
{
std::lock_guard<std::mutex> lock(cache_mutex);
@ -1140,9 +1221,15 @@ void Client::Impl::DisconnectLocked() {
for (auto& entry : streams) {
StreamData* stream_data = entry.second.get();
if (!stream_data) continue;
if (stream_data->module) {
if (stream_data->linger && stream_data->proxy) {
spa_hook_remove(&stream_data->listener);
stream_data->proxy = nullptr;
} else if (stream_data->module) {
pw_impl_module_destroy(stream_data->module);
stream_data->module = nullptr;
} else if (stream_data->proxy) {
pw_proxy_destroy(stream_data->proxy);
stream_data->proxy = nullptr;
} else if (stream_data->stream) {
pw_stream_disconnect(stream_data->stream);
pw_stream_destroy(stream_data->stream);
@ -2113,6 +2200,9 @@ Status Client::RemoveNode(NodeId node) {
}
pw_impl_module_destroy(owned_stream->module);
owned_stream->module = nullptr;
} else if (owned_stream->proxy) {
pw_proxy_destroy(owned_stream->proxy);
owned_stream->proxy = nullptr;
} else if (owned_stream->stream) {
pw_stream_disconnect(owned_stream->stream);
pw_stream_destroy(owned_stream->stream);
@ -2137,12 +2227,15 @@ Status Client::SetNodeVolume(NodeId node, float volume, bool mute) {
pw_thread_loop_lock(impl_->thread_loop);
bool is_virtual = false;
{
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
if (impl_->nodes.find(node.value) == impl_->nodes.end()) {
auto node_it = impl_->nodes.find(node.value);
if (node_it == impl_->nodes.end()) {
pw_thread_loop_unlock(impl_->thread_loop);
return Status::Error(StatusCode::kNotFound, "node not found");
}
is_virtual = node_it->second.is_virtual;
}
uint32_t n_channels;
@ -2154,7 +2247,8 @@ Status Client::SetNodeVolume(NodeId node, float volume, bool mute) {
if (streamIt != impl_->virtual_streams.end() && streamIt->second->stream) {
own_stream = streamIt->second->stream;
n_channels = streamIt->second->channels;
} else {
}
if (!own_stream) {
auto proxyIt = impl_->node_proxies.find(node.value);
if (proxyIt == impl_->node_proxies.end() || !proxyIt->second->proxy) {
pw_thread_loop_unlock(impl_->thread_loop);
@ -2170,8 +2264,8 @@ Status Client::SetNodeVolume(NodeId node, float volume, bool mute) {
uint8_t buffer[512];
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
uint32_t vol_prop = own_stream ? SPA_PROP_monitorVolumes : SPA_PROP_channelVolumes;
uint32_t mute_prop = own_stream ? SPA_PROP_monitorMute : SPA_PROP_mute;
uint32_t vol_prop = is_virtual ? SPA_PROP_monitorVolumes : SPA_PROP_channelVolumes;
uint32_t mute_prop = is_virtual ? SPA_PROP_monitorMute : SPA_PROP_mute;
spa_pod_frame obj_frame;
spa_pod_builder_push_object(&builder, &obj_frame,