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
11 changes: 11 additions & 0 deletions src/lib/core/presenter/DocumentManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,17 @@ DocumentManager::~DocumentManager()
// The documents have to be deleted before the application context plug-ins.
// This is because the Local device has to be deleted last in
// ApplicationPlugin.
for(auto document : m_documents)
{
// Reverse creation order, to match ~DocumentModel (e.g. the execution
// plugin must be torn down before the device explorer plugin).
auto& plugs = document->model().pluginModels();
for(auto it = plugs.rbegin(); it != plugs.rend(); ++it)
{
(*it)->on_documentClosing();
}
}

for(auto document : m_documents)
{
document->deleteLater();
Expand Down
2 changes: 2 additions & 0 deletions src/lib/score/gfx/Vulkan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ QVulkanInstance* staticVulkanInstance(bool create)
if(!instance.create())
{
g_staticVulkanInstanceInvalid = true;
delete g_staticVulkanInstance;
g_staticVulkanInstance = nullptr;
}
});

Expand Down
24 changes: 24 additions & 0 deletions src/lib/score/model/path/ObjectPath.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,30 @@ QString ObjectPath::toString() const noexcept
return s;
}

ObjectPath ObjectPath::fromString(const QString& str) noexcept
{
std::vector<ObjectIdentifier> v;
for(const auto& seg : QStringView{str}.split(QLatin1Char('/'), Qt::SkipEmptyParts))
{
const auto dot = seg.lastIndexOf(QLatin1Char('.'));
if(dot <= 0)
return {};
bool ok{};
const int32_t id = seg.mid(dot + 1).toInt(&ok);
if(!ok)
return {};
v.emplace_back(seg.left(dot).toString(), id);
}
return ObjectPath{std::move(v)};
}

QObject* ObjectPath::findObject(const score::DocumentContext& ctx) const noexcept
{
if(m_objectIdentifiers.empty())
return nullptr;
return find_impl_unsafe(ctx);
}

QObject* ObjectPath::find_impl(const score::DocumentContext& ctx) const
{
using namespace score;
Expand Down
2 changes: 2 additions & 0 deletions src/lib/score/model/path/ObjectPath.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ class SCORE_LIB_BASE_EXPORT ObjectPath
ObjectPath() noexcept { }
~ObjectPath() noexcept = default;
QString toString() const noexcept;
static ObjectPath fromString(const QString& str) noexcept;
QObject* findObject(const score::DocumentContext& ctx) const noexcept;

explicit ObjectPath(std::vector<ObjectIdentifier> vec) noexcept
: m_objectIdentifiers{std::move(vec)}
Expand Down
13 changes: 9 additions & 4 deletions src/plugins/score-plugin-avnd/Crousti/ProcessModel.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
namespace oscr
{

// The condition under which a process needs the port-callback storage (a
// model-side Info instance). The storage member and its init guard below must
// both use it — keep them in sync.
template <typename Info>
concept needs_ports_callback_storage = has_dynamic_ports<Info>;

template <typename Info>
struct MessageBusWrapperToUi
{
Expand Down Expand Up @@ -92,7 +98,8 @@ class ProcessModel final
oscr::dynamic_ports_storage<Info> dynamic_ports;

[[no_unique_address]]
ossia::type_if<Info, oscr::has_dynamic_ports<Info>> object_storage_for_ports_callbacks;
ossia::type_if<Info, oscr::needs_ports_callback_storage<Info>>
object_storage_for_ports_callbacks;

ProcessModel(
const TimeVal& duration, const Id<Process::ProcessModel>& id,
Expand Down Expand Up @@ -202,9 +209,7 @@ class ProcessModel final

void init_controller_ports()
{
if constexpr(
avnd::dynamic_ports_input_introspection<Info>::size > 0
|| avnd::dynamic_ports_output_introspection<Info>::size > 0)
if constexpr(oscr::needs_ports_callback_storage<Info>)
{
avnd::control_input_introspection<Info>::for_all_n2(
avnd::get_inputs<Info>((Info&)this->object_storage_for_ports_callbacks),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ DeviceDocumentPlugin::DeviceDocumentPlugin(
m_explorer = new DeviceExplorerModel{*this, this};
}

void DeviceDocumentPlugin::on_documentClosing()
{
for(auto dev : this->m_list.devices())
{
dev->disconnect();
}
}

DeviceDocumentPlugin::~DeviceDocumentPlugin()
{
for(auto dev : this->m_list.devices())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class SCORE_PLUGIN_DEVICEEXPLORER_EXPORT DeviceDocumentPlugin final

void init();

void on_documentClosing() override;

Device::Node& rootNode() { return m_rootNode; }
const Device::Node& rootNode() const { return m_rootNode; }

Expand Down
174 changes: 148 additions & 26 deletions src/plugins/score-plugin-gfx/Gfx/GStreamer/GStreamerDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ extern "C" {
#include <Gfx/GStreamer/GStreamerLoader.hpp>
#include <Video/GStreamerCompatibility.hpp>

#include <ossia/detail/parse_strict.hpp>

#include <climits>
#include <regex>
#include <ctre.hpp>
#include <thread>

namespace Gfx::GStreamer
Expand Down Expand Up @@ -72,6 +74,11 @@ struct gstreamer_pipeline

// Ring buffer per channel, written by GStreamer thread, read by audio engine
static constexpr std::size_t ring_size = 65536;

// Max block the audio thread may resize the output storage to; the
// parameter reserves this up front so the per-tick resize never reallocates
// (a realloc would free a buffer the audio thread is reading through).
static constexpr std::size_t max_block = 1 << 15;
std::vector<std::vector<float>> ring; // [channel][ring_size]
std::atomic<std::size_t> write_pos{0};
std::atomic<std::size_t> read_pos{0};
Expand Down Expand Up @@ -100,11 +107,38 @@ struct gstreamer_pipeline
write_pos.store(wp + num_samples, std::memory_order_release);
}

// Points at the parameter's audio spans so read_into_output can re-point
// them after a resize. A raw pointer (not a std::function) so that clearing
// or using it during teardown can never throw on the audio thread.
ossia::small_vector<std::span<float>, 8>* output_spans{};

// Called by audio engine (indirectly): copy from ring into output spans
void read_into_output(int block_size)
{
if(!output_data)
return;

// The engine tick size can differ from the configured buffer size
// (e.g. PipeWire dynamic quantum); the storage follows it, but never
// beyond the capacity reserved at construction (so no reallocation).
if(block_size > (int)max_block)
block_size = max_block;
bool resized = false;
for(auto& v : *output_data)
{
if(std::ssize(v) != block_size)
{
v.resize(block_size);
resized = true;
}
}
if(resized && output_spans && output_data)
{
const std::size_t n = std::min(output_spans->size(), output_data->size());
for(std::size_t i = 0; i < n; i++)
(*output_spans)[i] = (*output_data)[i];
}

auto rp = read_pos.load(std::memory_order_relaxed);
auto wp = write_pos.load(std::memory_order_acquire);

Expand Down Expand Up @@ -237,12 +271,15 @@ struct gstreamer_pipeline
}
else
{
// Caps not yet negotiated; default to video
info.is_video = true;
info.width = 640;
info.height = 480;
info.pixfmt = AV_PIX_FMT_RGBA;
video_count++;
// Caps not yet negotiated (e.g. live sources: v4l2src, webrtcsrc,
// audiotestsrc is-live=true don't preroll in PAUSED).
// Fall back to classifying from the pipeline description: the
// nearest audio/video token before this appsink wins.
classify_from_pipeline_string(pipeline_string, sink_name, info);
if(info.is_video)
video_count++;
else
audio_count++;
}
gst.object_unref(pad);
}
Expand Down Expand Up @@ -347,17 +384,16 @@ struct gstreamer_pipeline
}

private:
static constexpr auto appsink_name_rexp
= ctll::fixed_string{R"(appsink\b[^!]*?\bname\s*=\s*([A-Za-z0-9_]+))"};
static constexpr auto elem_name_rexp
= ctll::fixed_string{R"(\bname\s*=\s*([A-Za-z0-9_]+))"};

static std::vector<std::string> find_appsink_names(const std::string& pipeline)
{
std::vector<std::string> names;
// Match "appsink" optionally followed by properties including name=<identifier>
std::regex re(R"(appsink\b[^!]*?\bname\s*=\s*(\w+))");
auto begin = std::sregex_iterator(pipeline.begin(), pipeline.end(), re);
auto end = std::sregex_iterator();
for(auto it = begin; it != end; ++it)
{
names.push_back((*it)[1].str());
}
for(auto m : ctre::search_all<appsink_name_rexp>(pipeline))
names.push_back(m.get<1>().to_string());
return names;
}

Expand All @@ -366,14 +402,87 @@ struct gstreamer_pipeline
static std::vector<std::string> find_all_named_elements(const std::string& pipeline)
{
std::vector<std::string> names;
std::regex re(R"(\bname\s*=\s*(\w+))");
auto begin = std::sregex_iterator(pipeline.begin(), pipeline.end(), re);
auto end = std::sregex_iterator();
for(auto it = begin; it != end; ++it)
for(auto m : ctre::search_all<elem_name_rexp>(pipeline))
names.push_back(m.get<1>().to_string());
return names;
}

static constexpr auto channels_rexp
= ctll::fixed_string{R"(channels=(?:\(int\))?\s*([0-9]+))"};
static constexpr auto rate_rexp
= ctll::fixed_string{R"(rate=(?:\(int\))?\s*([0-9]+))"};
static constexpr auto width_rexp
= ctll::fixed_string{R"(width=(?:\(int\))?\s*([0-9]+))"};
static constexpr auto height_rexp
= ctll::fixed_string{R"(height=(?:\(int\))?\s*([0-9]+))"};
static constexpr auto format_rexp
= ctll::fixed_string{R"(format=(?:\(string\))?\s*([A-Za-z0-9]+))"};

template <ctll::fixed_string Rexp>
static int last_int_of(std::string_view str, int fallback)
{
int ret = fallback;
for(auto m : ctre::search_all<Rexp>(str))
if(auto v = ossia::parse_strict<int>(m.template get<1>().to_view()))
ret = *v;
return ret;
}

// Guess an appsink's media type from the pipeline string when caps are
// not negotiated yet: look for the last audio/video-ish token occurring
// before "appsink ... name=<sink_name>".
static void classify_from_pipeline_string(
const std::string& pipeline, const std::string& sink_name, AppsinkInfo& info)
{
std::size_t sink_pos = std::string::npos;
for(auto m : ctre::search_all<appsink_name_rexp>(pipeline))
{
names.push_back((*it)[1].str());
if(m.get<1>().to_view() == sink_name)
{
sink_pos = std::distance(pipeline.begin(), m.get<0>().begin());
break;
}
}
const std::string_view before
= std::string_view{pipeline}.substr(0, sink_pos);

static constexpr std::string_view audio_tokens[]
= {"audio/x-raw", "audioconvert", "audioresample", "audiotestsrc"};
static constexpr std::string_view video_tokens[]
= {"video/x-raw", "videoconvert", "videoscale", "videotestsrc", "videorate"};

std::size_t last_audio = std::string::npos, last_video = std::string::npos;
for(auto tok : audio_tokens)
if(auto p = before.rfind(tok); p != std::string::npos)
last_audio = (last_audio == std::string::npos) ? p : std::max(last_audio, p);
for(auto tok : video_tokens)
if(auto p = before.rfind(tok); p != std::string::npos)
last_video = (last_video == std::string::npos) ? p : std::max(last_video, p);

const bool is_audio = last_audio != std::string::npos
&& (last_video == std::string::npos || last_audio > last_video);
if(is_audio)
{
info.is_video = false;
info.channels = last_int_of<channels_rexp>(before, 2);
info.rate = last_int_of<rate_rexp>(before, 48000);
}
else
{
info.is_video = true;
info.width = last_int_of<width_rexp>(before, 640);
info.height = last_int_of<height_rexp>(before, 480);
info.pixfmt = AV_PIX_FMT_RGBA;
std::string format;
for(auto m : ctre::search_all<format_rexp>(before))
format = m.get<1>().to_string();
if(!format.empty())
{
auto& map = ::Video::gstreamerToLibav();
if(auto it = map.find(format); it != map.end())
info.pixfmt = it->second;
}
}
return names;
}

static void parse_video_caps(
Expand Down Expand Up @@ -601,18 +710,29 @@ class gstreamer_audio_parameter final : public ossia::audio_parameter
, m_audio_data(num_channels)
, m_block_size{bs}
{
// Set up owned buffers and point audio spans to them permanently
// Set up owned buffers and point audio spans to them permanently.
// Reserve a generous capacity up front so the per-tick resize() in
// read_into_output (block size follows the engine quantum) never
// reallocates — a realloc here would free a buffer the audio thread may
// still be reading through the spans, causing a double free.
audio.resize(num_channels);
for(int i = 0; i < num_channels; i++)
{
m_audio_data[i].reserve(gstreamer_pipeline::AudioBuffer::max_block);
m_audio_data[i].resize(bs, 0.f);
audio[i] = m_audio_data[i];
}
// Give the AudioBuffer a pointer so read_into_output can fill our buffers
// Give the AudioBuffer pointers so read_into_output can fill our buffers
// and re-point our spans after a resize.
m_buffer.output_data = &m_audio_data;
m_buffer.output_spans = &audio;
}

virtual ~gstreamer_audio_parameter() { m_buffer.output_data = nullptr; }
virtual ~gstreamer_audio_parameter()
{
m_buffer.output_data = nullptr;
m_buffer.output_spans = nullptr;
}

// clone_value() (not virtual) reads from audio spans.
// We use push_value(audio_port) as a hook — it's called by the audio engine
Expand All @@ -633,8 +753,10 @@ class gstreamer_audio_parameter final : public ossia::audio_parameter

void refresh_from_ring()
{
m_buffer.read_into_output(m_block_size);
// Re-point spans (in case vectors reallocated, though they shouldn't)
// Follow whatever block size the engine last requested in pre_tick
const int bs
= m_audio_data.empty() ? m_block_size : (int)m_audio_data[0].size();
m_buffer.read_into_output(bs);
for(std::size_t i = 0; i < m_audio_data.size(); i++)
audio[i] = m_audio_data[i];
}
Expand Down
Loading
Loading