From 10486656d58a4d64b1280e0ab0f2527eda93a88c Mon Sep 17 00:00:00 2001 From: Annika Date: Fri, 24 Jul 2026 01:12:37 -0700 Subject: [PATCH] init --- Makefile | 17 ++++ main.cpp | 32 +++++++ pipemini/pipemini.cpp | 216 ++++++++++++++++++++++++++++++++++++++++++ pipemini/pipemini.h | 41 ++++++++ pipemini_demo | Bin 0 -> 45656 bytes 5 files changed, 306 insertions(+) create mode 100644 Makefile create mode 100644 main.cpp create mode 100644 pipemini/pipemini.cpp create mode 100644 pipemini/pipemini.h create mode 100755 pipemini_demo diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d8952e2 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +CXX ?= g++ +CXXFLAGS ?= -O2 -Wall -std=c++17 + +PIPEWIRE_CFLAGS := $(shell pkg-config --cflags libpipewire-0.3) +PIPEWIRE_LIBS := $(shell pkg-config --libs libpipewire-0.3) + +TARGET := pipemini_demo + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): main.cpp pipemini/pipemini.cpp pipemini/pipemini.h + $(CXX) $(CXXFLAGS) $(PIPEWIRE_CFLAGS) main.cpp pipemini/pipemini.cpp -o $(TARGET) $(PIPEWIRE_LIBS) + +clean: + rm -f $(TARGET) diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..81303a8 --- /dev/null +++ b/main.cpp @@ -0,0 +1,32 @@ +#include "pipemini/pipemini.h" +#include +#include +#include + +int main() { + const int sample_rate = 48000; + const float freq = 440.0f; + float phase = 0.0f; + + PipeMini audio; + + bool ok = audio.start([&](int16_t* left, int16_t* right) { + float s = sinf(phase) * 0.25f; // quiet sine + int16_t v = (int16_t)(s * 32767.0f); + *left = v; + *right = v; + + phase += 2.0f * 3.14159265f * freq / sample_rate; + if (phase > 2.0f * 3.14159265f) phase -= 2.0f * 3.14159265f; + }, sample_rate); + + if (!ok) { + fprintf(stderr, "failed to start audio\n"); + return 1; + } + + printf("playing 440 Hz sine for 5 seconds...\n"); + sleep(5); + + return 0; +} diff --git a/pipemini/pipemini.cpp b/pipemini/pipemini.cpp new file mode 100644 index 0000000..6d9b80f --- /dev/null +++ b/pipemini/pipemini.cpp @@ -0,0 +1,216 @@ +#include "pipemini.h" +#include +#include +#include +#include + +#define DEFAULT_CHANNELS 2 +#define DEFAULT_FORMAT SPA_AUDIO_FORMAT_S16 + +struct PipeMiniData { + PipeMini* self; + std::thread loop_thread; + std::atomic should_quit{false}; + std::mutex mtx; + std::condition_variable cv; + bool teardown_done = false; +}; + +PipeMini::PipeMini() = default; + +PipeMini::~PipeMini() { + stop(); +} + +void PipeMini::teardown() { + if (stream_) { + pw_stream_disconnect(stream_); + pw_stream_destroy(stream_); + stream_ = nullptr; + } + + spa_hook_remove(&stream_listener_); + + if (core_) { + pw_core_disconnect(core_); + core_ = nullptr; + } + + if (context_) { + pw_context_destroy(context_); + context_ = nullptr; + } + + if (loop_) { + pw_main_loop_quit(loop_); + } +} + +static int on_teardown_event(spa_loop* loop, bool async, uint32_t id, const void* data, size_t size, void* userdata) { + (void)loop; + (void)async; + (void)id; + (void)data; + (void)size; + + auto* pmd = static_cast(userdata); + PipeMini* self = pmd->self; + + self->teardown(); + + { + std::lock_guard lock(pmd->mtx); + pmd->teardown_done = true; + } + pmd->cv.notify_one(); + + return 0; +} + +static const pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .process = PipeMini::on_process, +}; + +bool PipeMini::start(const Callback& cb, int sample_rate) { + if (running_) return false; + + callback_ = cb; + sample_rate_ = sample_rate; + + pw_init(nullptr, nullptr); + + loop_ = pw_main_loop_new(nullptr); + if (!loop_) return false; + + context_ = pw_context_new(pw_main_loop_get_loop(loop_), nullptr, 0); + if (!context_) { + pw_main_loop_destroy(loop_); + loop_ = nullptr; + return false; + } + + core_ = pw_context_connect(context_, nullptr, 0); + if (!core_) { + pw_context_destroy(context_); + context_ = nullptr; + pw_main_loop_destroy(loop_); + loop_ = nullptr; + return false; + } + + stream_ = pw_stream_new( + core_, + "pipemini", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Music", + nullptr)); + if (!stream_) { + pw_main_loop_destroy(loop_); + loop_ = nullptr; + return false; + } + + pw_stream_add_listener(stream_, &stream_listener_, &stream_events, this); + + uint8_t buffer[1024]; + spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + spa_audio_info_raw info = {}; + info.format = DEFAULT_FORMAT; + info.channels = DEFAULT_CHANNELS; + info.rate = (uint32_t)sample_rate_; + info.position[0] = SPA_AUDIO_CHANNEL_FL; + info.position[1] = SPA_AUDIO_CHANNEL_FR; + + const spa_pod* params = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info); + + if (pw_stream_connect(stream_, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + (pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS), + ¶ms, 1) < 0) { + pw_stream_destroy(stream_); + stream_ = nullptr; + pw_core_disconnect(core_); + core_ = nullptr; + pw_context_destroy(context_); + context_ = nullptr; + pw_main_loop_destroy(loop_); + loop_ = nullptr; + return false; + } + + running_ = true; + + // Run the PipeWire loop in a background thread. + data_ = new PipeMiniData{this, {}, {}}; + data_->loop_thread = std::thread([this]() { + pw_main_loop_run(loop_); + }); + + return true; +} + +void PipeMini::stop() { + if (!running_) return; + + if (data_ && loop_) { + pw_loop* loop = pw_main_loop_get_loop(loop_); + pw_loop_invoke(loop, on_teardown_event, 0, nullptr, 0, true, data_); + + std::unique_lock lock(data_->mtx); + data_->cv.wait(lock, [this]() { return data_->teardown_done; }); + + if (data_->loop_thread.joinable()) { + data_->loop_thread.join(); + } + + delete data_; + data_ = nullptr; + } + + pw_main_loop_destroy(loop_); + loop_ = nullptr; + + callback_ = nullptr; + running_ = false; +} + +void PipeMini::on_process(void* userdata) { + auto* self = static_cast(userdata); + if (!self || !self->callback_) return; + + pw_buffer* b = pw_stream_dequeue_buffer(self->stream_); + if (!b) return; + + spa_buffer* buf = b->buffer; + int16_t* dst = static_cast(buf->datas[0].data); + if (!dst) { + pw_stream_queue_buffer(self->stream_, b); + return; + } + + uint32_t n_frames = buf->datas[0].maxsize / (DEFAULT_CHANNELS * sizeof(int16_t)); + if (b->requested) { + n_frames = SPA_MIN(b->requested, n_frames); + } + + for (uint32_t i = 0; i < n_frames; ++i) { + int16_t left = 0; + int16_t right = 0; + self->callback_(&left, &right); + dst[i * 2 + 0] = left; + dst[i * 2 + 1] = right; + } + + buf->datas[0].chunk->offset = 0; + buf->datas[0].chunk->stride = DEFAULT_CHANNELS * sizeof(int16_t); + buf->datas[0].chunk->size = n_frames * DEFAULT_CHANNELS * sizeof(int16_t); + + pw_stream_queue_buffer(self->stream_, b); +} diff --git a/pipemini/pipemini.h b/pipemini/pipemini.h new file mode 100644 index 0000000..8db2f42 --- /dev/null +++ b/pipemini/pipemini.h @@ -0,0 +1,41 @@ +#ifndef PIPEMINI_H +#define PIPEMINI_H + +#include +#include +#include +#include + +// Simple PipeWire audio output interface. +// Calls a user-provided callback once per sample at the configured sample rate. +// The callback writes one 16-bit stereo sample (left, right). +class PipeMini { +public: + using Callback = std::function; + + PipeMini(); + ~PipeMini(); + + // Start the stream with the given callback. + // sample_rate is typically 48000. + bool start(const Callback& cb, int sample_rate = 48000); + void stop(); + +public: + static void on_process(void* userdata); + + void teardown(); + +private: + pw_main_loop* loop_ = nullptr; + pw_stream* stream_ = nullptr; + pw_core* core_ = nullptr; + pw_context* context_ = nullptr; + spa_hook stream_listener_ = {}; + Callback callback_; + int sample_rate_ = 48000; + bool running_ = false; + struct PipeMiniData* data_ = nullptr; +}; + +#endif // PIPEMINI_H diff --git a/pipemini_demo b/pipemini_demo new file mode 100755 index 0000000000000000000000000000000000000000..15c8568aaab2220d513caa5082d2ee7d09b76f98 GIT binary patch literal 45656 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 B#76)C literal 0 HcmV?d00001