mplay.flac

Controllable music player (flac)
git clone https://git.sinitax.com/sinitax/mplay.flac
Log | Files | Refs | Submodules | LICENSE | sfeed.txt

commit 9900659b323b51cf9aa53257147967a303e429a4
Author: Louis Burda <quent.burda@gmail.com>
Date:   Sun, 10 Mar 2024 14:20:29 +0100

Initial version

Diffstat:
A.gitignore | 3+++
A.gitmodules | 3+++
ALICENSE | 21+++++++++++++++++++++
AMakefile | 28++++++++++++++++++++++++++++
Alib/mplay | 1+
Amplay.flac.c | 715+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 771 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,3 @@ +.cache +compile_commands.json +mplay.flac diff --git a/.gitmodules b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/mplay"] + path = lib/mplay + url = git@sinitax.com:sinitax/mplay diff --git a/LICENSE b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Louis Burda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile @@ -0,0 +1,28 @@ +PREFIX ?= /usr/local +BINDIR ?= /bin + +CFLAGS = -I lib/mplay +CFLAGS += -Wunused-variable -Wunused-function -Wconversion +LDFLAGS = -lpulse -lpulse-simple -lFLAC + +ifeq ($(DEBUG),1) +CFLAGS += -Og -g +else +CFLAGS += -O2 +endif + +all: mplay.flac + +clean: + rm -f mplay.flac + +mplay.flac: mplay.flac.c lib/mplay/mplay.h + $(CC) -o $@ $< $(CFLAGS) $(LDFLAGS) + +install: + install -m755 mplay.flac -t "$(DESTDIR)$(PREFIX)$(BINDIR)" + +uninstall: + rm -f "$(DESTDIR)$(PREFIX)$(BINDIR)/mplay.flac" + +.PHONY: all clean install uninstall diff --git a/lib/mplay b/lib/mplay @@ -0,0 +1 @@ +Subproject commit c1d226be9672047fd66c20fde4bb2b3d197aa260 diff --git a/mplay.flac.c b/mplay.flac.c @@ -0,0 +1,715 @@ +#include "mplay.h" + +#include <FLAC/format.h> +#include <FLAC/stream_decoder.h> +#include <pulse/sample.h> +#include <pulse/introspect.h> +#include <pulse/operation.h> +#include <pulse/stream.h> +#include <pulse/thread-mainloop.h> +#include <pulse/volume.h> +#include <pulse/pulseaudio.h> +#include <pulse/def.h> + +#include <sys/mman.h> +#include <sys/stat.h> +#include <pthread.h> +#include <fcntl.h> +#include <termios.h> +#include <unistd.h> + +#include <stdarg.h> +#include <stdint.h> +#include <stdbool.h> +#include <string.h> +#include <stdio.h> +#include <stdlib.h> + +struct decoder { + FLAC__StreamDecoder *flac; + uint8_t *samples; + size_t sample_cnt; + size_t sample_cap; + size_t sample_size; + size_t sample_max; + + size_t sample_next; + + bool seek; + + uint32_t rate; + uint8_t channels; + pa_sample_format_t format; + + bool init; + bool pause; + bool done; + bool end; +}; + +static pa_buffer_attr pa_buf = { + .fragsize = UINT32_MAX, + .maxlength = UINT32_MAX, + .tlength = UINT32_MAX, + .prebuf = UINT32_MAX, + .minreq = UINT32_MAX, +}; + +static pa_stream_flags_t pa_stream_flags = PA_STREAM_START_CORKED + | PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY; + +static pa_threaded_mainloop *pa_mloop; +static pa_context *pa_ctx; +static pa_stream *pa_strm; +static pa_sink_input_info pa_strm_sink; +static bool pa_strm_sink_update; + +static struct decoder decoder; + +static pthread_t input_worker_thread; +static bool input_worker_alive = false; + +static struct termios term_old; +static struct termios term_new; +static bool term_set = false; + +static bool headless = false; +static bool verbose = false; +static bool istty = false; + +static void +__attribute__((noreturn)) +__attribute__((format(printf, 1, 2))) +die(const char *fmt, ...) +{ + va_list ap; + + fputs("mplay.flac: ", stderr); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + if (*fmt && fmt[strlen(fmt)-1] == ':') { + perror(NULL); + } else { + fputc('\n', stderr); + } + + exit(1); +} + +static void +term_set_canonical(void) +{ + term_new.c_lflag |= ICANON; + term_new.c_lflag |= ECHO; + if (tcsetattr(0, TCSANOW, &term_new)) + die("tcsetattr:"); +} + +static void +term_set_raw(void) +{ + term_new.c_lflag &= ~(0U | ICANON); + term_new.c_lflag &= ~(0U | ECHO); + if (tcsetattr(0, TCSANOW, &term_new)) + die("tcsetattr:"); +} + +static void +get_line(char *buf, int size) +{ + char *c; + + if (istty) putc('>', stderr); + if (istty) term_set_canonical(); + fgets(buf, size, stdin); + if ((c = strchr(buf, '\n'))) *c = '\0'; + if (istty) term_set_raw(); +} + +static FLAC__StreamDecoderWriteStatus +decoder_write_callback(const FLAC__StreamDecoder *flac, + const FLAC__Frame *frame, const FLAC__int32 *const buffer[], void *user) +{ + uint32_t si, ch; + + assert(decoder.sample_next == decoder.sample_cnt); + + if (decoder.sample_cnt + frame->header.blocksize > decoder.sample_cap) { + decoder.sample_cap = MAX(decoder.sample_cap * 2, + decoder.sample_cnt + frame->header.blocksize); + decoder.samples = realloc(decoder.samples, decoder.sample_cap + * decoder.channels * decoder.sample_size); + if (!decoder.samples) die("realloc:"); + } + + if (decoder.format == PA_SAMPLE_S16LE) { + uint16_t *sample = (uint16_t *)decoder.samples + + decoder.sample_cnt * decoder.channels; + for (si = 0; si < frame->header.blocksize; si++) { + for (ch = 0; ch < frame->header.channels; ch++) { + *sample++ = (uint16_t)htole32((uint32_t)buffer[ch][si]); + } + } + } else if (decoder.format == PA_SAMPLE_S32LE) { + uint32_t *sample = (uint32_t *)decoder.samples + + decoder.sample_cnt * decoder.channels; + for (si = 0; si < frame->header.blocksize; si++) { + for (ch = 0; ch < frame->header.channels; ch++) { + *sample++ = htole32((uint32_t)buffer[ch][si]); + } + } + } + + decoder.sample_cnt += frame->header.blocksize; + decoder.sample_max = MAX(decoder.sample_max, decoder.sample_cnt); + + return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; +} + +static void +decoder_metadata_callback(const FLAC__StreamDecoder *flac, + const FLAC__StreamMetadata *metadata, void *user) +{ + if (!decoder.init && metadata->type == FLAC__METADATA_TYPE_STREAMINFO) { + decoder.sample_cnt = 0; + decoder.rate = metadata->data.stream_info.sample_rate; + decoder.channels = (uint8_t) metadata->data.stream_info.channels; + decoder.sample_size = metadata->data.stream_info.bits_per_sample / 8; + decoder.sample_max = metadata->data.stream_info.total_samples; + if (decoder.sample_size == 2) { + decoder.format = PA_SAMPLE_S16LE; + } else if (decoder.sample_size == 4) { + decoder.format = PA_SAMPLE_S32LE; + } else { + die("unsupported sample size"); + } + decoder.init = true; + } +} + +static void +decoder_error_callback(const FLAC__StreamDecoder *flac, + FLAC__StreamDecoderErrorStatus status, void *user) +{ + die("flac decoding error: %s", FLAC__StreamDecoderErrorStatusString[status]); +} + +static void +decoder_flac_decode_next(void) +{ + bool ok; + + ok = FLAC__stream_decoder_process_single(decoder.flac); + FLAC__StreamDecoderState state = + FLAC__stream_decoder_get_state(decoder.flac); + if (!ok) { + die("decode process failed: %s", + FLAC__StreamDecoderStateString[state]); + } + decoder.done = state == FLAC__STREAM_DECODER_END_OF_STREAM; +} + +static uint8_t * +decoder_process_frame(size_t *cnt) +{ + size_t _cnt; + + if (decoder.sample_next < decoder.sample_cnt) { + _cnt = decoder.sample_cnt - decoder.sample_next; + } else { + if (decoder.done) { + decoder.end = true; + _cnt = 0; + } else { + decoder_flac_decode_next(); + _cnt = decoder.sample_cnt - decoder.sample_next; + } + } + + if (cnt) *cnt = _cnt; + + return decoder.samples + decoder.sample_next \ + * decoder.channels * decoder.sample_size; +} + +static void +decoder_init(const char *file) +{ + FLAC__StreamDecoderInitStatus init_status; + + decoder.sample_cnt = 0; + decoder.sample_next = 0; + decoder.sample_cap = 0; + decoder.sample_size = 0; + decoder.samples = NULL; + + decoder.seek = false; + decoder.pause = false; + decoder.done = false; + decoder.end = false; + + decoder.flac = FLAC__stream_decoder_new(); + init_status = FLAC__stream_decoder_init_file(decoder.flac, file, + decoder_write_callback, decoder_metadata_callback, + decoder_error_callback, NULL); + if (init_status != FLAC__STREAM_DECODER_INIT_STATUS_OK) { + die("flac decoder init failed: %s", + FLAC__StreamDecoderInitStatusString[init_status]); + } + + /* decode channel specs */ + decoder.init = false; + decoder_process_frame(NULL); + assert(decoder.init); +} + +static void +decoder_seek(size_t sample_pos) +{ + while (decoder.sample_next < sample_pos) { + decoder_process_frame(NULL); + if (decoder.end) { + mplay_status(MPLAY_INFO_EXIT, "from seek"); + exit(0); + } + } + decoder.sample_next = sample_pos; +} + + +static void +pa_stream_drain_callback(pa_stream *stream, int success, void *data) +{ + mplay_status(MPLAY_INFO_EXIT, NULL); + exit(0); +} + +static void +pa_stream_write_callback(pa_stream *stream, size_t requested, void *data) +{ + uint8_t *samples; + pa_operation *op; + ssize_t remaining; + size_t cnt, size; + + remaining = (ssize_t) requested; + while (remaining > 0) { + samples = decoder_process_frame(&cnt); + if (decoder.end) { + op = pa_stream_drain(pa_strm, + pa_stream_drain_callback, NULL); + pa_operation_unref(op); + return; + } + if (!cnt) continue; + + cnt = MIN(cnt, (size_t) remaining); + size = cnt * decoder.channels * decoder.sample_size; + pa_stream_write(stream, samples, size, NULL, 0, + decoder.seek ? PA_SEEK_RELATIVE_ON_READ : PA_SEEK_RELATIVE); + decoder.sample_next += cnt; + + decoder.seek = false; + remaining -= (ssize_t) size; + } +} + +static void +pa_stream_underflow_callback(struct pa_stream *stream, void *data) +{ + fprintf(stderr, "pulseaudio underflow!\n"); +} + +static void +pa_stream_overflow_callback(struct pa_stream *stream, void *data) +{ + fprintf(stderr, "pulseaudio overflow!\n"); +} + +static 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_sink, info, sizeof(pa_sink_input_info)); + pa_strm_sink_update = true; + pa_threaded_mainloop_signal(pa_mloop, true); +} + +static void +update_sink_input_info(void) +{ + pa_operation *op; + + pa_strm_sink_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) die("pa_context_get_sink_input_info failed"); + + while (!pa_strm_sink_update) { + pa_threaded_mainloop_unlock(pa_mloop); + pa_threaded_mainloop_wait(pa_mloop); + pa_threaded_mainloop_lock(pa_mloop); + } + + pa_threaded_mainloop_accept(pa_mloop); + + pa_operation_unref(op); +} + +static size_t +stream_samples_buffered(void) +{ + static size_t last_read_index = 0, last_write_index = 0; + const pa_timing_info *info; + size_t buffered; + + info = pa_stream_get_timing_info(pa_strm); + if (!info || info->write_index_corrupt || info->read_index_corrupt) + return 0; + + if (info->read_index != last_read_index) + last_write_index = (size_t) MAX(0, info->write_index); + + buffered = (last_write_index - last_read_index) + / (decoder.sample_size * decoder.channels); + + last_read_index = (size_t) MAX(0, info->read_index); + + return buffered; +} + +static double +user_vol(void) +{ + pa_volume_t vol; + + update_sink_input_info(); + if (!pa_strm_sink.has_volume) + return -1; + + vol = pa_cvolume_avg(&pa_strm_sink.volume); + + return (double) vol * 100.F / (double) PA_VOLUME_NORM; +} + +static double +user_pos(void) +{ + size_t sample_pos; + + sample_pos = MAX(0, decoder.sample_next - stream_samples_buffered()); + + return (double) sample_pos * 1.F / (double) decoder.rate; +} + +static double +user_end(void) +{ + return (double) decoder.sample_max * 1.F / (double) decoder.rate; +} + +static void +cmd_setvol(double vol, double delta) +{ + pa_operation *op; + + pa_threaded_mainloop_lock(pa_mloop); + + if (delta != 0) { + vol = user_vol() + delta; + } + + update_sink_input_info(); + pa_cvolume_set(&pa_strm_sink.volume, 2, + (pa_volume_t) (vol * PA_VOLUME_NORM / 100.F)); + if (pa_cvolume_avg(&pa_strm_sink.volume) > 2 * PA_VOLUME_NORM) + pa_cvolume_set(&pa_strm_sink.volume, 2, 2 * PA_VOLUME_NORM); + + op = pa_context_set_sink_input_volume(pa_ctx, + pa_stream_get_index(pa_strm), + &pa_strm_sink.volume, NULL, NULL); + pa_operation_unref(op); + + mplay_status(MPLAY_INFO_VOLUME, "%02.2f", (double) pa_cvolume_avg(&pa_strm_sink.volume) + * 100.f / (double) PA_VOLUME_NORM); + + pa_threaded_mainloop_unlock(pa_mloop); +} + +static void +cmd_seek(double time_sec, double delta) +{ + size_t sample_pos; + pa_operation *op; + + pa_threaded_mainloop_lock(pa_mloop); + + if (delta != 0) { + time_sec = user_pos() + delta; + } + + if (time_sec < 0) time_sec = 0; + sample_pos = (size_t) (time_sec * decoder.rate); + decoder_seek(sample_pos); + + op = pa_stream_flush(pa_strm, NULL, NULL); + pa_operation_unref(op); + decoder.seek = true; + + mplay_status(MPLAY_INFO_SEEK, "%02.2f", user_pos()); + + pa_threaded_mainloop_unlock(pa_mloop); +} + +static void +cmd_playpause(void) +{ + pa_operation *op; + + pa_threaded_mainloop_lock(pa_mloop); + + decoder.pause ^= 1; + op = pa_stream_cork(pa_strm, decoder.pause, NULL, NULL); + pa_operation_unref(op); + + mplay_status(MPLAY_INFO_PAUSE, "%i", decoder.pause); + + pa_threaded_mainloop_unlock(pa_mloop); +} + +static void +cmd_status(void) +{ + pa_threaded_mainloop_lock(pa_mloop); + + update_sink_input_info(); + + mplay_status(MPLAY_INFO_STATUS, + "volume=%02.2f,pos=%02.2f,end=%02.2f,pause=%i", + user_vol(), user_pos(), user_end(), decoder.pause); + + pa_threaded_mainloop_unlock(pa_mloop); +} + +static bool +key_input(void) +{ + char linebuf[16], *end; + float fval; + int key; + + while ((key = mplay_getkey()) == MPLAY_KEY_EOF); + + switch (mplay_action(key)) { + case MPLAY_ACTION_VOLUME: + get_line(linebuf, sizeof(linebuf)); + fval = strtof(linebuf, &end); + if (!end || *end) { + mplay_status(MPLAY_INFO_VOLUME, "fail:bad float"); + break; + } + cmd_setvol(fval, 0); + break; + case MPLAY_ACTION_VOLUME_UP: + cmd_setvol(0, 5); + break; + case MPLAY_ACTION_VOLUME_DOWN: + cmd_setvol(0, -5); + break; + case MPLAY_ACTION_SEEK: + get_line(linebuf, sizeof(linebuf)); + fval = strtof(linebuf, &end); + if (!end || *end) { + mplay_status(MPLAY_INFO_SEEK, "fail:bad float"); + break; + } + cmd_seek(fval, 0); + break; + case MPLAY_ACTION_SEEK_BWD: + cmd_seek(0, -5); + break; + case MPLAY_ACTION_SEEK_FWD: + cmd_seek(0, 5); + break; + case MPLAY_ACTION_PAUSE: + cmd_playpause(); + break; + case MPLAY_ACTION_STATUS: + cmd_status(); + break; + default: + mplay_status(MPLAY_INFO_INPUT, "fail"); + break; + } + + return true; +} + +static void * +input_worker(void *arg) +{ + input_worker_alive = true; + + setvbuf(stdin, NULL, _IONBF, 0); + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); + + if (istty) { + if (tcgetattr(0, &term_old)) + die("tcgetattr:"); + term_new = term_old; + term_new.c_iflag |= BRKINT; + term_new.c_iflag &= ~(0U | IGNBRK); + term_set = true; + + term_set_raw(); + } + + mplay_status(MPLAY_INFO_READY, NULL); + + while (key_input()); + + return NULL; +} + +static void +pulse_context_init(void) +{ + pa_mainloop_api *pa_mloop_api; + int ret; + + pa_mloop_api = pa_threaded_mainloop_get_api(pa_mloop); + if (!pa_mloop_api) die("pa_threaded_mainloop_get_api"); + + pa_ctx = pa_context_new(pa_mloop_api, "mplay.flac"); + if (!pa_ctx) die("pa_context_new"); + + ret = pa_context_connect(pa_ctx, NULL, 0, NULL); + if (ret) die("pa_context_connect: %s", + pa_strerror(pa_context_errno(pa_ctx))); + + while (pa_context_get_state(pa_ctx) != PA_CONTEXT_READY) { + pa_threaded_mainloop_unlock(pa_mloop); + pa_threaded_mainloop_wait(pa_mloop); + pa_threaded_mainloop_lock(pa_mloop); + } +} + +static void +pulse_stream_init(void) +{ + struct pa_sample_spec pa_spec; + pa_channel_map pa_chmap; + int ret; + + pa_channel_map_init_stereo(&pa_chmap); + + pa_spec.channels = decoder.channels; + pa_spec.rate = decoder.rate; + pa_spec.format = decoder.format; + + pa_strm = pa_stream_new(pa_ctx, "mplay.flac", &pa_spec, &pa_chmap); + if (!pa_strm) die("pa_stream_new: %s", + pa_strerror(pa_context_errno(pa_ctx))); + + pa_stream_set_write_callback(pa_strm, pa_stream_write_callback, NULL); + + if (verbose) { + pa_stream_set_underflow_callback(pa_strm, + pa_stream_underflow_callback, NULL); + pa_stream_set_overflow_callback(pa_strm, + pa_stream_overflow_callback, NULL); + } + + ret = pa_stream_connect_playback(pa_strm, NULL, + &pa_buf, pa_stream_flags, NULL, NULL); + if (ret) die("pa_stream_connect_playback failed"); + + while (pa_stream_get_state(pa_strm) != PA_STREAM_READY) { + pa_threaded_mainloop_unlock(pa_mloop); + pa_threaded_mainloop_wait(pa_mloop); + pa_threaded_mainloop_lock(pa_mloop); + } +} + +static void +sigint_handler(int sig) +{ + exit(0); +} + +static void +cleanup(void) +{ + if (term_set) tcsetattr(0, TCSANOW, &term_old); + + pa_stream_disconnect(pa_strm); + pa_stream_unref(pa_strm); + + pa_context_disconnect(pa_ctx); + pa_context_unref(pa_ctx); + + if (input_worker_alive) + pthread_kill(input_worker_thread, SIGINT); +} + +static void +usage(void) +{ + fprintf(stderr, "Usage: mplay.flac [-v] [" MPLAY_FLAG_HEADLESS "] FILE\n"); + exit(1); +} + +int +main(int argc, const char **argv) +{ + pthread_mutex_t lock; + const char **arg; + const char *file; + + file = NULL; + for (arg = argv + 1; *arg; arg++) { + if (!strcmp(*arg, MPLAY_FLAG_HEADLESS)) { + headless = true; + } else if (!strcmp(*arg, "-v")) { + verbose = true; + } else { + if (file) usage(); + file = *arg; + } + } + if (!file) usage(); + + istty = isatty(0); + + decoder_init(file); + + pa_mloop = pa_threaded_mainloop_new(); + if (!pa_mloop) die("pa_threaded_mainloop_new"); + + pa_threaded_mainloop_start(pa_mloop); + + pa_threaded_mainloop_lock(pa_mloop); + pulse_context_init(); + pulse_stream_init(); + pa_stream_cork(pa_strm, 0, NULL, NULL); + pa_threaded_mainloop_unlock(pa_mloop); + + atexit(cleanup); + signal(SIGINT, sigint_handler); + + if (!headless) { + if (pthread_create(&input_worker_thread, NULL, input_worker, NULL)) + die("pthread_create"); + pthread_join(input_worker_thread, NULL); + } else { + pthread_mutex_init(&lock, NULL); + pthread_mutex_lock(&lock); + pthread_mutex_lock(&lock); + } + + return 0; +}