Created
March 30, 2025 16:50
-
-
Save rlapz/dab3bb52603a4cf60eab20cf941def89 to your computer and use it in GitHub Desktop.
A simple audio player (pipewire & ffmpeg) by DeepSeek. (ncurses version)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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