From 3b13d633f1a08a2736d7c4372d40b46be3b40642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Sat, 4 Jul 2026 21:49:59 -0400 Subject: [PATCH] backends: fix dynamic frame-port channels, gst texture caps, TD analyzer crash Three independent binding-correctness fixes, previously bundled in the channel-safety PR: - wrappers/per_frame_port: derive a dynamic frame port's channel count from the buffer the host handed us when the host never ran the channel manager (Python/GStreamer/Pd one-block drivers left it 0, which copied nothing and left the output uninitialised). - gstreamer/texture_filter: stop fixate_caps collapsing the src caps to 1x1; copy the input width/height to the output. - touchdesigner/chop/audio_processor: fix the crash on audio->control analyzers (objects with zero audio-output busses). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JH7HS58wPSA22HztHZsakd --- .../avnd/binding/gstreamer/texture_filter.hpp | 33 +++++++++++++++++-- .../touchdesigner/chop/audio_processor.hpp | 24 +++++++++++--- .../avnd/wrappers/process/per_frame_port.hpp | 6 ++++ 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/include/avnd/binding/gstreamer/texture_filter.hpp b/include/avnd/binding/gstreamer/texture_filter.hpp index 41fd1559..03f780f0 100644 --- a/include/avnd/binding/gstreamer/texture_filter.hpp +++ b/include/avnd/binding/gstreamer/texture_filter.hpp @@ -347,9 +347,36 @@ struct element : property_handler { GST_DEBUG_OBJECT(this, "fixate_caps direction=%d", direction); - // For dynamic resolution effects, we cannot predetermine output size - // Start with input size as baseline, actual size will be handled in transform - GstCaps* result = gst_caps_fixate(gst_caps_copy(othercaps)); + // The src template advertises [1,MAX], so a plain fixate() picks 1x1. + // Default the output to the input resolution; size-changing effects + // renegotiate in transform(). + othercaps = gst_caps_make_writable(othercaps); + + if(gst_caps_get_size(caps) > 0 && gst_caps_get_size(othercaps) > 0) + { + GstStructure* ins = gst_caps_get_structure(caps, 0); + gint width = 0, height = 0, fps_n = 0, fps_d = 1; + const gboolean have_w = gst_structure_get_int(ins, "width", &width); + const gboolean have_h = gst_structure_get_int(ins, "height", &height); + const gboolean have_fps + = gst_structure_get_fraction(ins, "framerate", &fps_n, &fps_d); + + const guint n = gst_caps_get_size(othercaps); + for(guint i = 0; i < n; i++) + { + GstStructure* outs = gst_caps_get_structure(othercaps, i); + if(have_w) + gst_structure_fixate_field_nearest_int(outs, "width", width); + if(have_h) + gst_structure_fixate_field_nearest_int(outs, "height", height); + if(have_fps) + gst_structure_fixate_field_nearest_fraction( + outs, "framerate", fps_n, fps_d); + } + } + + // Fixate any remaining fields (format etc.) + GstCaps* result = gst_caps_fixate(othercaps); return result; } diff --git a/include/avnd/binding/touchdesigner/chop/audio_processor.hpp b/include/avnd/binding/touchdesigner/chop/audio_processor.hpp index 080cda83..6d637550 100644 --- a/include/avnd/binding/touchdesigner/chop/audio_processor.hpp +++ b/include/avnd/binding/touchdesigner/chop/audio_processor.hpp @@ -96,10 +96,24 @@ struct audio_processor : public TD::CHOP_CPlusPlusBase { if constexpr(avnd::bus_introspection::output_busses == 0) { + // Analyzer (audio in -> control out): no audio output bus, but still + // prepare() so the scratch buffers exist -- skipping it crashed TD on cook. + auto in0 = inputs->getNumInputs() > 0 ? inputs->getInputCHOP(0) : nullptr; + const int in_ch = in0 ? updateAudioChannels(inputs) : 0; + info->numChannels = 0; - info->numSamples = 0; + info->numSamples = in0 ? in0->numSamples : 0; info->startIndex = 0; - info->sampleRate = 0; + info->sampleRate = in0 ? in0->sampleRate : default_sample_rate; + + avnd::process_setup setup_info{ + .input_channels = in_ch, + .output_channels = 0, + .frames_per_buffer = default_buffer_size, + .rate = in0 ? (float)in0->sampleRate : (float)default_sample_rate}; + processor.allocate_buffers(setup_info, float{}); + implementation.init_channels(in0 ? in0->numChannels : 0, 0); + avnd::prepare(implementation, setup_info); return true; } else if constexpr(avnd::bus_introspection::output_busses >= 1) @@ -240,10 +254,12 @@ struct audio_processor : public TD::CHOP_CPlusPlusBase const TD::OP_CHOPInput* input = inputs->getInputCHOP(0); assert(input); - // Prepare input/output spans for processing + // Size by output->numChannels (what TD allocated), not + // get_output_channels(impl, 0): bus 0 doesn't exist for analyzers and its + // bogus count runs out_ptrs off the end of output->channels. const int num_samples = output->numSamples; const int num_in_channels = total_input_channels; - const int num_out_channels = channels.get_output_channels(implementation, 0); + const int num_out_channels = output->numChannels; auto in_ptrs = (float**) alloca(sizeof(float*) * num_in_channels + 4); auto out_ptrs = (float**) alloca(sizeof(float*) * num_out_channels + 4); diff --git a/include/avnd/wrappers/process/per_frame_port.hpp b/include/avnd/wrappers/process/per_frame_port.hpp index fdfe42e0..2f4fe80b 100644 --- a/include/avnd/wrappers/process/per_frame_port.hpp +++ b/include/avnd/wrappers/process/per_frame_port.hpp @@ -127,6 +127,10 @@ struct process_adapter avnd::generic_audio_frame_port && avnd::dynamic_poly_audio_port) { + // Hosts that skip the channel manager leave channels==0; derive it + // from the buffer so we don't copy nothing into an uninit output. + if(field.channels <= 0) + field.channels = std::max(0, (int)in.size() - offset); const int ch = avnd::get_channels(field); if(offset + ch <= (int)this->m_input_frame_storage.size()) field.frame = this->m_input_frame_storage.data() + offset; @@ -143,6 +147,8 @@ struct process_adapter avnd::generic_audio_frame_port && avnd::dynamic_poly_audio_port) { + if(field.channels <= 0) + field.channels = std::max(0, (int)out.size() - offset); const int ch = avnd::get_channels(field); if(offset + ch <= (int)this->m_output_frame_storage.size()) field.frame = this->m_output_frame_storage.data() + offset;