Skip to content

Instantly share code, notes, and snippets.

@rlapz
Created March 30, 2025 16:50
Show Gist options
  • Save rlapz/dab3bb52603a4cf60eab20cf941def89 to your computer and use it in GitHub Desktop.
Save rlapz/dab3bb52603a4cf60eab20cf941def89 to your computer and use it in GitHub Desktop.
A simple audio player (pipewire & ffmpeg) by DeepSeek. (ncurses version)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>
#include <pthread.h>
#include <ncurses.h>
#include <pipewire/pipewire.h>
#include <spa/utils/result.h>
#include <spa/param/audio/format-utils.h>
#include <spa/pod/builder.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
#include <libavutil/opt.h>
#include <libavutil/time.h>
#define PROGRESS_BAR_WIDTH 50
typedef struct {
bool running;
bool paused;
bool seek_requested;
int64_t seek_target;
double duration_sec;
double current_sec;
pthread_mutex_t lock;
} PlayerState;
struct player_data {
struct pw_stream *stream;
struct pw_main_loop *loop;
AVFormatContext *format_ctx;
AVCodecContext *codec_ctx;
SwrContext *swr_ctx;
int audio_stream_idx;
PlayerState state;
pthread_t input_thread;
};
static void update_display(struct player_data *data) {
clear();
printw("Music Player - PipeWire + FFmpeg\n\n");
// Progress bar
double progress = data->state.duration_sec > 0 ?
(data->state.current_sec / data->state.duration_sec) : 0;
int pos = PROGRESS_BAR_WIDTH * progress;
printw("[");
for (int i = 0; i < PROGRESS_BAR_WIDTH; i++) {
printw(i < pos ? "=" : (i == pos ? ">" : " "));
}
printw("] %.1f/%.1f sec\n\n", data->state.current_sec, data->state.duration_sec);
// Controls
printw("Controls:\n");
printw(" [SPACE] Play/Pause\n");
printw(" [s] Stop\n");
printw(" [q] Quit\n");
printw("\nStatus: %s\n", data->state.paused ? "PAUSED" : "PLAYING");
refresh();
}
static void* input_thread_func(void *userdata) {
struct player_data *data = userdata;
initscr();
cbreak();
noecho();
keypad(stdscr, TRUE);
nodelay(stdscr, TRUE);
curs_set(0);
while (data->state.running) {
int ch = getch();
if (ch != ERR) {
pthread_mutex_lock(&data->state.lock);
switch (ch) {
case ' ':
data->state.paused = !data->state.paused;
break;
case 's':
// Stop (return to beginning)
data->state.current_sec = 0;
data->state.seek_requested = true;
data->state.seek_target = 0;
data->state.paused = true;
break;
case 'q':
data->state.running = false;
pw_main_loop_quit(data->loop);
break;
}
pthread_mutex_unlock(&data->state.lock);
}
update_display(data);
usleep(100000); // 100ms refresh rate
}
endwin();
return NULL;
}
static void on_process(void *userdata) {
struct player_data *data = userdata;
struct pw_buffer *b;
float *dst;
int ret;
AVPacket *pkt = av_packet_alloc();
pthread_mutex_lock(&data->state.lock);
bool paused = data->state.paused;
pthread_mutex_unlock(&data->state.lock);
if (paused) {
if ((b = pw_stream_dequeue_buffer(data->stream))) {
// Output silence when paused
memset(b->buffer->datas[0].data, 0, b->buffer->datas[0].maxsize);
b->buffer->datas[0].chunk->size = b->buffer->datas[0].maxsize;
pw_stream_queue_buffer(data->stream, b);
}
return;
}
if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) {
pw_log_warn("out of buffers: %m");
av_packet_free(&pkt);
return;
}
dst = (float *)b->buffer->datas[0].data;
AVFrame *frame = av_frame_alloc();
if (!frame) {
pw_log_error("Failed to allocate frame");
av_packet_free(&pkt);
return;
}
while (1) {
ret = av_read_frame(data->format_ctx, pkt);
if (ret < 0) {
if (ret == AVERROR_EOF) {
// Loop at end of file
av_seek_frame(data->format_ctx, data->audio_stream_idx, 0, AVSEEK_FLAG_BACKWARD);
continue;
} else {
pw_log_error("read frame error: %s", av_err2str(ret));
pw_main_loop_quit(data->loop);
break;
}
}
if (pkt->stream_index != data->audio_stream_idx) {
av_packet_unref(pkt);
continue;
}
ret = avcodec_send_packet(data->codec_ctx, pkt);
if (ret < 0) {
pw_log_error("error sending packet: %s", av_err2str(ret));
av_packet_unref(pkt);
continue;
}
ret = avcodec_receive_frame(data->codec_ctx, frame);
if (ret == AVERROR(EAGAIN)) {
av_packet_unref(pkt);
continue;
} else if (ret < 0) {
pw_log_error("error receiving frame: %s", av_err2str(ret));
av_packet_unref(pkt);
break;
}
// Update current position
pthread_mutex_lock(&data->state.lock);
data->state.current_sec = frame->best_effort_timestamp * av_q2d(data->format_ctx->streams[data->audio_stream_idx]->time_base);
pthread_mutex_unlock(&data->state.lock);
// Convert to float planar
swr_convert(data->swr_ctx, (uint8_t **)&dst, frame->nb_samples,
(const uint8_t **)frame->extended_data, frame->nb_samples);
b->buffer->datas[0].chunk->offset = 0;
b->buffer->datas[0].chunk->stride = sizeof(float) * data->codec_ctx->ch_layout.nb_channels;
b->buffer->datas[0].chunk->size = frame->nb_samples * b->buffer->datas[0].chunk->stride;
av_frame_unref(frame);
av_packet_unref(pkt);
break;
}
av_frame_free(&frame);
av_packet_free(&pkt);
pw_stream_queue_buffer(data->stream, b);
}
static const struct pw_stream_events stream_events = {
PW_VERSION_STREAM_EVENTS,
.process = on_process,
};
void cleanup(struct player_data *data) {
data->state.running = false;
if (data->input_thread) pthread_join(data->input_thread, NULL);
if (data->stream) pw_stream_destroy(data->stream);
if (data->loop) pw_main_loop_destroy(data->loop);
if (data->swr_ctx) swr_free(&data->swr_ctx);
if (data->codec_ctx) avcodec_free_context(&data->codec_ctx);
if (data->format_ctx) avformat_close_input(&data->format_ctx);
pthread_mutex_destroy(&data->state.lock);
avformat_network_deinit();
}
int main(int argc, char *argv[]) {
struct player_data data = {0};
const struct spa_pod *params[1];
uint8_t buffer[1024];
struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
const struct spa_pod *param;
int ret;
if (argc < 2) {
fprintf(stderr, "Usage: %s <audio-file>\n", argv[0]);
return 1;
}
// Initialize player state
pthread_mutex_init(&data.state.lock, NULL);
data.state.running = true;
data.state.paused = false;
data.state.seek_requested = false;
data.state.current_sec = 0;
// Initialize FFmpeg
avformat_network_init();
// Open input file
if ((ret = avformat_open_input(&data.format_ctx, argv[1], NULL, NULL)) < 0) {
fprintf(stderr, "Could not open file '%s': %s\n", argv[1], av_err2str(ret));
return 1;
}
if ((ret = avformat_find_stream_info(data.format_ctx, NULL)) < 0) {
fprintf(stderr, "Could not find stream information: %s\n", av_err2str(ret));
cleanup(&data);
return 1;
}
// Get duration
data.state.duration_sec = data.format_ctx->duration / (double)AV_TIME_BASE;
// Find audio stream
data.audio_stream_idx = av_find_best_stream(data.format_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
if (data.audio_stream_idx < 0) {
fprintf(stderr, "Could not find audio stream in file\n");
cleanup(&data);
return 1;
}
AVStream *stream = data.format_ctx->streams[data.audio_stream_idx];
const AVCodec *codec = avcodec_find_decoder(stream->codecpar->codec_id);
if (!codec) {
fprintf(stderr, "Unsupported codec\n");
cleanup(&data);
return 1;
}
data.codec_ctx = avcodec_alloc_context3(codec);
if (!data.codec_ctx) {
fprintf(stderr, "Could not allocate codec context\n");
cleanup(&data);
return 1;
}
if ((ret = avcodec_parameters_to_context(data.codec_ctx, stream->codecpar)) < 0) {
fprintf(stderr, "Could not copy codec parameters: %s\n", av_err2str(ret));
cleanup(&data);
return 1;
}
if ((ret = avcodec_open2(data.codec_ctx, codec, NULL)) < 0) {
fprintf(stderr, "Could not open codec: %s\n", av_err2str(ret));
cleanup(&data);
return 1;
}
// Initialize PipeWire
pw_init(&argc, &argv);
data.loop = pw_main_loop_new(NULL);
if (!data.loop) {
fprintf(stderr, "Failed to create PipeWire main loop\n");
cleanup(&data);
return 1;
}
// Create audio stream
data.stream = pw_stream_new_simple(
pw_main_loop_get_loop(data.loop),
"audio-player",
pw_properties_new(
PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Playback",
PW_KEY_MEDIA_ROLE, "Music",
NULL),
&stream_events,
&data);
if (!data.stream) {
fprintf(stderr, "Failed to create PipeWire stream\n");
cleanup(&data);
return 1;
}
// Configure audio format
enum spa_audio_format spa_format = SPA_AUDIO_FORMAT_F32;
uint32_t rate = data.codec_ctx->sample_rate;
uint32_t channels = data.codec_ctx->ch_layout.nb_channels;
// Set up resampler
data.swr_ctx = swr_alloc();
if (!data.swr_ctx) {
fprintf(stderr, "Failed to allocate resampler\n");
cleanup(&data);
return 1;
}
av_opt_set_int(data.swr_ctx, "in_sample_rate", data.codec_ctx->sample_rate, 0);
av_opt_set_int(data.swr_ctx, "out_sample_rate", rate, 0);
av_opt_set_sample_fmt(data.swr_ctx, "in_sample_fmt", data.codec_ctx->sample_fmt, 0);
av_opt_set_sample_fmt(data.swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
av_opt_set_chlayout(data.swr_ctx, "in_chlayout", &data.codec_ctx->ch_layout, 0);
av_opt_set_chlayout(data.swr_ctx, "out_chlayout", &data.codec_ctx->ch_layout, 0);
if ((ret = swr_init(data.swr_ctx)) < 0) {
fprintf(stderr, "Failed to initialize resampler: %s\n", av_err2str(ret));
cleanup(&data);
return 1;
}
// Prepare format
param = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat,
&SPA_AUDIO_INFO_RAW_INIT(
.format = spa_format,
.rate = rate,
.channels = channels));
if (!param) {
fprintf(stderr, "Failed to build audio format\n");
cleanup(&data);
return 1;
}
params[0] = param;
if ((ret = pw_stream_connect(data.stream,
PW_DIRECTION_OUTPUT,
PW_ID_ANY,
PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_MAP_BUFFERS |
PW_STREAM_FLAG_RT_PROCESS,
params, 1)) < 0) {
fprintf(stderr, "Failed to connect stream: %s\n", spa_strerror(ret));
cleanup(&data);
return 1;
}
// Start input thread
pthread_create(&data.input_thread, NULL, input_thread_func, &data);
// Start playback
pw_main_loop_run(data.loop);
// Cleanup
cleanup(&data);
return 0;
}
#!/bin/sh
gcc -o audio2 audio2.c $(pkg-config --cflags --libs libpipewire-0.3 libspa-0.2 libavformat libavcodec libswresample libavutil) -lm -lncurses -lpthread
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment