commit 10486656d58a4d64b1280e0ab0f2527eda93a88c Author: Annika Date: Fri Jul 24 01:12:37 2026 -0700 init 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 0000000..15c8568 Binary files /dev/null and b/pipemini_demo differ