mplay

Controllable music player
git clone https://git.sinitax.com/sinitax/mplay
Log | Files | Refs | sfeed.txt

commit f64d66eb59704cf729638be46cb471da96a91cf5
parent aab4376554c0930e82a02c02f6bc778c15501cb2
Author: Louis Burda <quent.burda@gmail.com>
Date:   Sat, 17 Dec 2022 21:22:40 +0100

Implemented threaded mainloop and basic interaction commands

Diffstat:
Msrc/mplay.c | 569++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
1 file changed, 404 insertions(+), 165 deletions(-)

diff --git a/src/mplay.c b/src/mplay.c @@ -1,4 +1,7 @@ #include <pulse/introspect.h> +#include <pulse/operation.h> +#include <pulse/stream.h> +#include <pulse/thread-mainloop.h> #include <pulse/volume.h> #define MINIMP3_IMPLEMENTATION #include "minimp3.h" @@ -21,7 +24,8 @@ enum { KEY_NONE = 0, KEY_BRK = 3, - KEY_LEFT = 0x100, + KEY_QUIT = 0x100, + KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, @@ -29,37 +33,32 @@ enum { #define ERR(...) err(1, __VA_ARGS__) #define ERRX(...) errx(1, __VA_ARGS__) -#define PA_CTX_ERR(ctx, fmt, ...) do { \ - warnx(fmt ": %s", __VA_ARGS__ __VA_OPT__(,) \ - pa_strerror(pa_context_errno(ctx))); \ - pa_mainloop_quit(pa_mloop, 1); \ - } while (0) #define PA_ERRX(...) do { \ warnx(__VA_ARGS__); \ - pa_mainloop_quit(pa_mloop, 1); \ + exit(1); \ } while (0) -static const pa_sample_spec pa_spec = { +static pa_sample_spec pa_spec = { .format = PA_SAMPLE_S16LE, .rate = 44100, .channels = 2 }; - -static const pa_buffer_attr pa_buf = { +static pa_buffer_attr pa_buf = { + .fragsize = UINT32_MAX, .maxlength = UINT32_MAX, .tlength = UINT32_MAX, - .prebuf = UINT32_MAX, + .prebuf = 0, .minreq = UINT32_MAX, }; static pa_stream_flags_t pa_stream_flags = PA_STREAM_START_CORKED | PA_STREAM_INTERPOLATE_TIMING | - PA_STREAM_NOT_MONOTONIC | PA_STREAM_AUTO_TIMING_UPDATE | - PA_STREAM_ADJUST_LATENCY; + PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY; -static pa_mainloop *pa_mloop; +static pa_threaded_mainloop *pa_mloop; static pa_context *pa_ctx; static pa_stream *pa_strm; static pa_sink_input_info pa_strm_info; +static bool pa_strm_info_update; static struct { uint8_t *data; @@ -70,11 +69,36 @@ static struct { mp3dec_t dec; uint8_t *pos; ssize_t left; + + mp3d_sample_t *samples; + size_t sample_cnt; + size_t sample_cap; + + size_t sample_next; + + bool seek; + pa_usec_t seek_stream_time; + pa_usec_t seek_track_time; + + int rate; + int channels; + + bool init; + bool pause; + int err; } mp3d; +void decoder_init(void); +void decoder_seek(size_t sample_pos); +int decode_next_frame(mp3d_sample_t *samples); + static pthread_t input_worker_thread; static pthread_t pa_worker_thread; +static struct termios term_old; +static struct termios term_new; +static bool term_set = false; + static int exitcode; uint8_t * @@ -104,227 +128,442 @@ map_file(const char *path, size_t *len) } void -pa_stream_write_callback(pa_stream *stream, size_t requested, void *data) +decoder_init(void) { - mp3dec_frame_info_t info; mp3d_sample_t samples[MINIMP3_MAX_SAMPLES_PER_FRAME]; - ssize_t remaining; + + mp3dec_init(&mp3d.dec); + mp3d.pos = audiofile.data; + mp3d.left = audiofile.len; + + mp3d.sample_cnt = 0; + mp3d.sample_next = 0; + mp3d.sample_cap = MINIMP3_MAX_SAMPLES_PER_FRAME; + mp3d.samples = malloc(mp3d.sample_cap * sizeof(mp3d_sample_t)); + if (!mp3d.samples) ERR("malloc"); + + /* decode channel specs */ + if (!decode_next_frame(samples)) + exit(1); + + mp3d.err = 0; + mp3d.seek = false; + mp3d.pause = false; + mp3d.init = true; +} + +int +decode_next_frame(mp3d_sample_t *samples) +{ + mp3dec_frame_info_t info; int cnt; - remaining = requested; - while (remaining > 0 && mp3d.left > 0) { + if (mp3d.sample_next < mp3d.sample_cnt) { + cnt = MIN(MINIMP3_MAX_SAMPLES_PER_FRAME, + mp3d.sample_cnt - mp3d.sample_next); + memcpy(samples, &mp3d.samples[mp3d.sample_next], + cnt * sizeof(mp3d_sample_t)); + mp3d.sample_next += cnt; + } else { cnt = mp3dec_decode_frame(&mp3d.dec, mp3d.pos, mp3d.left, samples, &info); - if (mp3d.pos == audiofile.data && !cnt) - ERRX("Invalid mp3 input file"); - if (!cnt && !info.frame_bytes) break; + cnt *= info.channels; + + if (mp3d.pos == audiofile.data && !cnt) { + warnx("MP3 decode error"); + mp3d.err = 1; + return 0; + } + + if (!mp3d.init) { + mp3d.rate = info.hz; + mp3d.channels = info.channels; + } + + if (mp3d.sample_cnt + cnt > mp3d.sample_cap) { + mp3d.sample_cap = MAX(mp3d.sample_cap * 2, + mp3d.sample_cnt + cnt); + mp3d.samples = realloc(mp3d.samples, + mp3d.sample_cap * sizeof(mp3d_sample_t)); + if (!mp3d.samples) ERR("realloc"); + } + memcpy(&mp3d.samples[mp3d.sample_cnt], samples, + cnt * sizeof(mp3d_sample_t)); + mp3d.sample_cnt += cnt; mp3d.pos += info.frame_bytes; mp3d.left -= info.frame_bytes; - if (!cnt) continue; - pa_stream_write(stream, samples, - cnt * info.channels * sizeof(mp3d_sample_t), - NULL, 0, PA_SEEK_RELATIVE); - remaining -= info.frame_bytes; + mp3d.sample_next += cnt; } - if (!mp3d.left) - pa_mainloop_quit(pa_mloop, 0); + return cnt; } void -pa_stream_state_callback(pa_stream *stream, void *data) +decoder_seek(size_t sample_pos) { - switch (pa_stream_get_state(stream)) { - case PA_STREAM_FAILED: - pa_mainloop_quit(pa_mloop, 1); - return; - case PA_STREAM_READY: - pa_stream_cork(stream, 0, NULL, NULL); - break; - case PA_STREAM_TERMINATED: - pa_mainloop_quit(pa_mloop, 0); - break; - case PA_STREAM_UNCONNECTED: - case PA_STREAM_CREATING: - default: - break; + mp3d_sample_t samples[MINIMP3_MAX_SAMPLES_PER_FRAME]; + int cnt; + + while (mp3d.sample_next < sample_pos) { + cnt = decode_next_frame(samples); + if (!cnt) exit(0); } + mp3d.sample_next = sample_pos; } void -pa_state_callback(pa_context *pa_ctx, void *data) +context_poll_until(pa_context_state_t target_state) { - pa_channel_map pa_chmap; pa_context_state_t state; - int ret; - switch (pa_context_get_state(pa_ctx)) { - case PA_CONTEXT_FAILED: - pa_mainloop_quit(pa_mloop, 1); - return; - case PA_CONTEXT_READY: - pa_channel_map_init_stereo(&pa_chmap); - - mp3dec_init(&mp3d.dec); - mp3d.pos = audiofile.data; - mp3d.left = audiofile.len; - - pa_strm = pa_stream_new(pa_ctx, "mplay", &pa_spec, &pa_chmap); - if (!pa_strm) PA_CTX_ERR(pa_ctx, "pa_stream_new"); - - pa_stream_set_state_callback(pa_strm, - pa_stream_state_callback, NULL); - pa_stream_set_write_callback(pa_strm, - pa_stream_write_callback, NULL); - - ret = pa_stream_connect_playback(pa_strm, - NULL, &pa_buf, pa_stream_flags, NULL, NULL); - if (ret) PA_ERRX("pa_stream_connect_playback failed"); - - break; - case PA_CONTEXT_TERMINATED: - pa_mainloop_quit(pa_mloop, 0); - break; - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_UNCONNECTED: - default: - break; + while (true) { + pa_threaded_mainloop_lock(pa_mloop); + state = pa_context_get_state(pa_ctx); + pa_threaded_mainloop_unlock(pa_mloop); + if (state == target_state) + break; + pa_threaded_mainloop_wait(pa_mloop); } } void -pa_sink_input_info_callback(struct pa_context *ctx, +stream_poll_until(pa_stream_state_t target_state) +{ + pa_stream_state_t state; + + while (true) { + pa_threaded_mainloop_lock(pa_mloop); + state = pa_stream_get_state(pa_strm); + pa_threaded_mainloop_unlock(pa_mloop); + if (state == target_state) + break; + pa_threaded_mainloop_wait(pa_mloop); + } +} + +void +update_sink_input_info_callback(struct pa_context *ctx, const pa_sink_input_info *info, int eol, void *data) { if (eol) return; memcpy(&pa_strm_info, info, sizeof(pa_sink_input_info)); + pa_strm_info_update = true; + pa_threaded_mainloop_signal(pa_mloop, 1); } -void * -pa_worker(void *arg) +void +update_sink_input_info(void) { - if (pa_mainloop_run(pa_mloop, NULL)) - exitcode = 1; - printf("KILL INPUT\n"); - pthread_kill(input_worker_thread, SIGINT); - return NULL; + pa_operation *op; + + pa_strm_info_update = false; + op = pa_context_get_sink_input_info(pa_ctx, + pa_stream_get_index(pa_strm), + update_sink_input_info_callback, NULL); + if (!op) PA_ERRX("pa_context_get_sink_input_info failed"); + + while (!pa_strm_info_update) { + pa_threaded_mainloop_wait(pa_mloop); + } + + pa_operation_unref(op); + + pa_threaded_mainloop_accept(pa_mloop); } -int -getkey_esc(void) +void +pa_stream_write_callback(pa_stream *stream, size_t requested, void *data) { - char c; + mp3d_sample_t samples[MINIMP3_MAX_SAMPLES_PER_FRAME]; + const pa_timing_info *info; + ssize_t remaining; + int cnt, channels; - c = getchar(); - switch (c) { - case 'A': - return KEY_UP; - case 'B': - return KEY_DOWN; - case 'C': - return KEY_DOWN; - case 'D': - return KEY_DOWN; - default: - return KEY_NONE; + if (!mp3d.left) { + info = pa_stream_get_timing_info(pa_strm); + if (info->read_index >= info->write_index) + exit(0); + } + + remaining = requested; + while (remaining > 0 && mp3d.left > 0) { + cnt = decode_next_frame(samples); + if (!cnt) exit(mp3d.err); + + pa_stream_write(stream, samples, + cnt * sizeof(mp3d_sample_t), NULL, 0, + mp3d.seek ? PA_SEEK_RELATIVE_ON_READ: PA_SEEK_RELATIVE); + + mp3d.seek = false; + remaining -= cnt * sizeof(mp3d_sample_t); } } +double +get_track_time(void) +{ + pa_usec_t time; + + pa_stream_get_time(pa_strm, &time); + return (time - mp3d.seek_stream_time + mp3d.seek_track_time) / 1000000.F; +} + +ssize_t +stream_samples_buffered(void) +{ + const pa_timing_info *info; + + info = pa_stream_get_timing_info(pa_strm); + if (info->write_index_corrupt || info->read_index_corrupt) + return 0; + + return (info->write_index - info->read_index) / sizeof(mp3d_sample_t); +} + void -cmd_inc_volume(void) +cmd_volume(float delta) { - pa_context_get_sink_input_info(pa_ctx, - pa_stream_get_index(pa_strm), - pa_sink_input_info_callback, NULL); - pa_cvolume_inc(&pa_strm_info.volume, 65536 / 20); + pa_operation *op; + + pa_threaded_mainloop_lock(pa_mloop); + + update_sink_input_info(); + + if (delta > 0) { + pa_cvolume_inc(&pa_strm_info.volume, + delta * PA_VOLUME_NORM / 100); + } else { + pa_cvolume_dec(&pa_strm_info.volume, + -delta * PA_VOLUME_NORM / 100); + } if (pa_cvolume_avg(&pa_strm_info.volume) > 2 * PA_VOLUME_NORM) pa_cvolume_set(&pa_strm_info.volume, 2, 2 * PA_VOLUME_NORM); - pa_context_set_sink_input_volume(pa_ctx, + + op = pa_context_set_sink_input_volume(pa_ctx, pa_stream_get_index(pa_strm), &pa_strm_info.volume, NULL, NULL); - printf("VOLUME: %02.f\n", - pa_cvolume_avg(&pa_strm_info.volume) + pa_operation_unref(op); + + printf("VOLUME: %02.2f\n", pa_cvolume_avg(&pa_strm_info.volume) * 100.f / PA_VOLUME_NORM); + + pa_threaded_mainloop_unlock(pa_mloop); } void -cmd_dec_volume(void) +cmd_seek(float delta) { - pa_context_get_sink_input_info(pa_ctx, - pa_stream_get_index(pa_strm), - pa_sink_input_info_callback, NULL); - pa_cvolume_dec(&pa_strm_info.volume, 65536 / 20); - pa_context_set_sink_input_volume(pa_ctx, - pa_stream_get_index(pa_strm), - &pa_strm_info.volume, NULL, NULL); - printf("VOLUME: %02.f\n", - pa_cvolume_avg(&pa_strm_info.volume) - * 100.f / PA_VOLUME_NORM); + ssize_t sample_pos; + pa_operation *op; + int64_t index; + + pa_threaded_mainloop_lock(pa_mloop); + + sample_pos = MAX(0, (ssize_t) mp3d.sample_next + + delta * mp3d.channels * mp3d.rate - stream_samples_buffered()); + decoder_seek(sample_pos); + op = pa_stream_flush(pa_strm, NULL, NULL); + pa_operation_unref(op); + + mp3d.seek = true; + pa_stream_get_time(pa_strm, &mp3d.seek_stream_time); + mp3d.seek_track_time = mp3d.sample_next * 1000000.F + / (mp3d.channels * mp3d.rate); + + printf("SEEK: %02.2f\n", mp3d.seek_track_time / 1000000.F); + + pa_threaded_mainloop_unlock(pa_mloop); +} + +void +cmd_pause(void) +{ + ssize_t sample_pos; + pa_operation *op; + + pa_threaded_mainloop_lock(pa_mloop); + mp3d.pause ^= 1; + op = pa_stream_cork(pa_strm, mp3d.pause, NULL, NULL); + pa_operation_unref(op); + printf("PAUSE: %i\n", mp3d.pause); + pa_threaded_mainloop_unlock(pa_mloop); +} + +void +cmd_status(void) +{ + pa_threaded_mainloop_lock(pa_mloop); + update_sink_input_info(); + printf("STATUS: vol:%02.2f pos:%02.2f pause:%i\n", + pa_cvolume_avg(&pa_strm_info.volume) * 100.0 / PA_VOLUME_NORM, + get_track_time(), mp3d.pause); + pa_threaded_mainloop_unlock(pa_mloop); +} + +int +getkey_esc(void) +{ + int c; + + c = getchar(); + switch (c) { + case 'A': + return KEY_UP; + case 'B': + return KEY_DOWN; + case 'C': + return KEY_RIGHT; + case 'D': + return KEY_LEFT; + case EOF: + return KEY_QUIT; + default: + return KEY_NONE; + } } int getkey(void) { - char c; + int c; c = getchar(); - if (c == 0x1b) { + switch (c) { + case 0x1b: c = getchar(); - if (c == '[') { + switch (c) { + case '[': return getkey_esc(); - } else { + default: return KEY_NONE; } - } else { - return c; + default: + return c & 0xff; } } void * input_worker(void *arg) { - struct termios old, new; int key; - if (tcgetattr(0, &old)) + if (tcgetattr(0, &term_old)) ERR("tcgetattr"); + term_set = true; - new = old; - new.c_iflag |= BRKINT; - new.c_iflag &= ~IGNBRK; - new.c_lflag &= ~ICANON; - new.c_lflag &= ~ECHO; + term_new = term_old; + term_new.c_iflag |= BRKINT; + term_new.c_iflag &= ~IGNBRK; + term_new.c_lflag &= ~ICANON; + term_new.c_lflag &= ~ECHO; - if (tcsetattr(0, TCSANOW, &new)) + if (tcsetattr(0, TCSANOW, &term_new)) ERR("tcsetattr"); while (true) { while (!(key = getkey())); - if (key == '+' || key == KEY_UP) { - cmd_inc_volume(); - } else if (key == '-' || key == KEY_DOWN) { - cmd_dec_volume(); - } else if (key == KEY_LEFT) { - - } else if (key == KEY_RIGHT) { - - } else { + switch (key) { + case KEY_UP: + case '+': + cmd_volume(+5); + break; + case KEY_DOWN: + case '-': + cmd_volume(-5); + break; + case KEY_LEFT: + cmd_seek(-5); + break; + case KEY_RIGHT: + cmd_seek(+5); + break; + case 'c': + cmd_pause(); + break; + case 's': + cmd_status(); + break; + case KEY_QUIT: + case 'q': + return NULL; + default: printf("Unbound key: %i\n", key); + break; } } - tcsetattr(0, TCSANOW, &old); - return NULL; } +void +context_init(void) +{ + pa_mainloop_api *pa_mloop_api; + int ret; + + pa_threaded_mainloop_lock(pa_mloop); + + pa_mloop_api = pa_threaded_mainloop_get_api(pa_mloop); + if (!pa_mloop_api) ERRX("pa_threaded_mainloop_get_api"); + + pa_ctx = pa_context_new(pa_mloop_api, "mplay"); + if (!pa_ctx) ERRX("pa_context_new"); + + ret = pa_context_connect(pa_ctx, NULL, 0, NULL); + if (ret) ERRX("pa_context_connect: %s", + pa_strerror(pa_context_errno(pa_ctx))); + + pa_threaded_mainloop_unlock(pa_mloop); + + context_poll_until(PA_CONTEXT_READY); +} + +void +stream_init(void) +{ + pa_channel_map pa_chmap; + pa_context_state_t state; + int ret; + + pa_channel_map_init_stereo(&pa_chmap); + + pa_threaded_mainloop_lock(pa_mloop); + + pa_strm = pa_stream_new(pa_ctx, "mplay", &pa_spec, &pa_chmap); + if (!pa_strm) ERRX("pa_stream_new: %s", + pa_strerror(pa_context_errno(pa_ctx))); + + pa_stream_set_write_callback(pa_strm, pa_stream_write_callback, NULL); + + ret = pa_stream_connect_playback(pa_strm, NULL, + &pa_buf, pa_stream_flags, NULL, NULL); + if (ret) ERRX("pa_stream_connect_playback failed"); + + pa_threaded_mainloop_unlock(pa_mloop); + + stream_poll_until(PA_STREAM_READY); +} + +void +sigint_handler(int sig) +{ + exit(0); +} + +void +term_reset(void) +{ + if (term_set) + tcsetattr(0, TCSANOW, &term_old); +} + int main(int argc, const char **argv) { - pa_mainloop_api *pa_mloop_api; pa_context_state_t pa_ctx_state; + struct sigaction act; int ret; if (argc != 2) { @@ -333,40 +572,40 @@ main(int argc, const char **argv) } audiofile.data = map_file(argv[1], &audiofile.len); + decoder_init(); - pa_mloop = pa_mainloop_new(); - if (!pa_mloop) ERRX("pa_mainloop_new"); + pa_spec.channels = mp3d.channels; + pa_spec.rate = mp3d.rate; - pa_mloop_api = pa_mainloop_get_api(pa_mloop); - if (!pa_mloop_api) ERRX("pa_mainloop_get_api"); + pa_mloop = pa_threaded_mainloop_new(); + if (!pa_mloop) ERRX("pa_threaded_mainloop_new"); - pa_ctx = pa_context_new(pa_mloop_api, "mplay"); - if (!pa_ctx) ERRX("pa_context_new"); + pa_threaded_mainloop_start(pa_mloop); - ret = pa_context_connect(pa_ctx, NULL, 0, NULL); - if (ret) ERRX("pa_context_connect: %s", - pa_strerror(pa_context_errno(pa_ctx))); + context_init(); + stream_init(); - pa_context_set_state_callback(pa_ctx, - pa_state_callback, NULL); + pa_threaded_mainloop_lock(pa_mloop); + pa_stream_cork(pa_strm, 0, NULL, NULL); + pa_threaded_mainloop_unlock(pa_mloop); exitcode = 0; - - if (pthread_create(&pa_worker_thread, NULL, pa_worker, NULL)) - ERR("pthread_create"); + atexit(term_reset); + signal(SIGINT, sigint_handler); if (pthread_create(&input_worker_thread, NULL, input_worker, NULL)) ERR("pthread_create"); + pthread_join(input_worker_thread, NULL); - pthread_setschedprio(pa_worker_thread, 1); - pthread_setschedprio(input_worker_thread, 2); + pa_threaded_mainloop_stop(pa_mloop); - pthread_join(input_worker_thread, NULL); - pthread_join(pa_worker_thread, NULL); + pa_stream_disconnect(pa_strm); + pa_stream_unref(pa_strm); pa_context_disconnect(pa_ctx); pa_context_unref(pa_ctx); - pa_mainloop_free(pa_mloop); + + pa_threaded_mainloop_free(pa_mloop); munmap(audiofile.data, audiofile.len);