commit 428a418313c7522f0e9b1d4dd40311295afaa04a
Author: Louis Burda <quent.burda@gmail.com>
Date: Sat, 9 Mar 2024 22:10:29 +0100
Initial version with mplay api
Diffstat:
7 files changed, 719 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,3 @@
+.cache
+compile_commands.json
+mplay.mp3
diff --git a/.gitmodules b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "lib/minimp3"]
+ path = lib/minimp3
+ url = git@github.com:lieff/minimp3.git
+[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 -I lib/minimp3/
+CFLAGS += -Wunused-variable -Wunused-function -Wconversion
+LDFLAGS = -lpulse -lpulse-simple
+
+ifeq ($(DEBUG),1)
+CFLAGS += -Og -g
+else
+CFLAGS += -O2
+endif
+
+all: mplay.mp3
+
+clean:
+ rm -f mplay.mp3
+
+mplay.mp3: mplay.mp3.c
+ $(CC) -o $@ $^ $(CFLAGS) $(LDFLAGS)
+
+install:
+ install -m755 mplay.mp3 -t "$(DESTDIR)$(PREFIX)$(BINDIR)"
+
+uninstall:
+ rm -f "$(DESTDIR)$(PREFIX)$(BINDIR)/mplay.mp3"
+
+.PHONY: all clean install uninstall
diff --git a/lib/minimp3 b/lib/minimp3
@@ -0,0 +1 @@
+Subproject commit afb604c06bc8beb145fecd42c0ceb5bda8795144
diff --git a/lib/mplay b/lib/mplay
@@ -0,0 +1 @@
+Subproject commit 5eb60cb925e4b3c477efd07016acb43510e7d6e9
diff --git a/mplay.mp3.c b/mplay.mp3.c
@@ -0,0 +1,659 @@
+#include "mplay.h"
+
+#define MINIMP3_IMPLEMENTATION
+#include "minimp3.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 <stdio.h>
+#include <stdlib.h>
+
+struct audiofile {
+ uint8_t *data;
+ size_t len;
+};
+
+struct mp3d {
+ 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;
+
+ int rate;
+ int channels;
+
+ bool init;
+ bool pause;
+};
+
+static pa_sample_spec pa_spec = {
+ .format = PA_SAMPLE_S16LE,
+ .rate = 44100,
+ .channels = 2
+};
+
+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 audiofile audiofile = { 0 };
+
+static struct mp3d mp3d = { 0 };
+
+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;
+
+__attribute__((noreturn)) static void
+die(const char *fmt, ...)
+{
+ va_list ap;
+
+ fputs("mplay.mp3: ", 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;
+
+ putc('>', stderr);
+ if (istty) term_set_canonical();
+ fgets(buf, size, stdin);
+ if ((c = strchr(buf, '\n'))) *c = '\0';
+ if (istty) term_set_raw();
+}
+
+static uint8_t *
+map_file(const char *path, size_t *len)
+{
+ struct stat attr;
+ uint8_t *buf;
+ int fd;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0) die("open %s:", path);
+
+ if (fstat(fd, &attr))
+ die("fstat %s:", path);
+
+ if ((attr.st_mode & S_IFMT) == S_IFDIR)
+ die("not a file: %s", path);
+
+ buf = mmap(NULL, (size_t) attr.st_size, PROT_READ, MAP_SHARED, fd, 0);
+ if (!buf) die("mmap %s:", path);
+
+ *len = (size_t) attr.st_size;
+
+ close(fd);
+
+ return buf;
+}
+
+static size_t
+decode_next_frame(mp3d_sample_t *samples, size_t *size)
+{
+ static const size_t max_cnt = MINIMP3_MAX_SAMPLES_PER_FRAME;
+ mp3dec_frame_info_t info;
+ size_t cnt;
+
+ if (size) *size = 0;
+
+ if (mp3d.sample_next < mp3d.sample_cnt) {
+ cnt = MIN(max_cnt, mp3d.sample_cnt - mp3d.sample_next);
+ *size = cnt * sizeof(mp3d_sample_t);
+ } else {
+ if (mp3d.sample_cnt + max_cnt > mp3d.sample_cap) {
+ mp3d.sample_cap = MAX(mp3d.sample_cap * 2,
+ mp3d.sample_cnt + max_cnt);
+ mp3d.samples = realloc(mp3d.samples,
+ mp3d.sample_cap * sizeof(mp3d_sample_t));
+ if (!mp3d.samples) die("realloc:");
+ }
+
+ cnt = (size_t) mp3dec_decode_frame(&mp3d.dec, mp3d.pos,
+ (int) mp3d.left, &mp3d.samples[mp3d.sample_cnt], &info);
+ cnt *= (size_t) info.channels;
+ mp3d.sample_cnt += cnt;
+
+ if (size) *size = (size_t) info.frame_bytes;
+
+ if (!info.frame_bytes)
+ return 0;
+
+ if (!mp3d.init) {
+ mp3d.rate = info.hz;
+ mp3d.channels = info.channels;
+ }
+
+ mp3d.pos += info.frame_bytes;
+ mp3d.left -= info.frame_bytes;
+ }
+
+ if (samples != NULL)
+ memcpy(samples, &mp3d.samples[mp3d.sample_next],
+ cnt * sizeof(mp3d_sample_t));
+ mp3d.sample_next += cnt;
+
+ return cnt;
+}
+
+static void
+decoder_init(void)
+{
+ mp3d_sample_t samples[MINIMP3_MAX_SAMPLES_PER_FRAME];
+ size_t size;
+
+ mp3dec_init(&mp3d.dec);
+ mp3d.pos = audiofile.data;
+ mp3d.left = (ssize_t) 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) die("malloc:");
+
+ /* decode channel specs */
+ decode_next_frame(samples, &size);
+ if (!size) die("invalid mp3");
+
+ mp3d.seek = false;
+ mp3d.pause = false;
+ mp3d.init = true;
+}
+
+static void
+decoder_seek(size_t sample_pos)
+{
+ size_t size, cnt;
+
+ while (mp3d.sample_next < sample_pos) {
+ cnt = decode_next_frame(NULL, &size);
+ if (!size) {
+ mplay_status(MPLAY_STATUS_EXIT, "from seek");
+ exit(0);
+ }
+ }
+ mp3d.sample_next = sample_pos;
+}
+
+static void
+pa_stream_drain_callback(pa_stream *stream, int success, void *data)
+{
+ mplay_status(MPLAY_STATUS_EXIT, NULL);
+ exit(0);
+}
+
+static void
+pa_stream_write_callback(pa_stream *stream, size_t requested, void *data)
+{
+ mp3d_sample_t samples[MINIMP3_MAX_SAMPLES_PER_FRAME];
+ pa_operation *op;
+ ssize_t remaining;
+ size_t cnt, size;
+
+ remaining = (ssize_t) requested;
+ while (remaining > 0) {
+ cnt = decode_next_frame(samples, &size);
+ if (!size) {
+ op = pa_stream_drain(pa_strm,
+ pa_stream_drain_callback, NULL);
+ pa_operation_unref(op);
+ return;
+ }
+ if (!cnt) continue;
+
+ 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 -= (ssize_t) (cnt * sizeof(mp3d_sample_t));
+ }
+}
+
+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)
+ / sizeof(mp3d_sample_t);
+
+ 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_time(void)
+{
+ size_t sample_pos;
+
+ sample_pos = MAX(0, mp3d.sample_next - stream_samples_buffered());
+
+ return (double) sample_pos * 1.F / (double) (mp3d.rate * mp3d.channels);
+}
+
+static void
+cmd_setvol(double vol)
+{
+ pa_operation *op;
+
+ 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_ACTION_STATUS_VOL, "%02.2f", (double) pa_cvolume_avg(&pa_strm_sink.volume)
+ * 100.f / (double) PA_VOLUME_NORM);
+}
+
+static void
+cmd_seek(double time_sec)
+{
+ size_t sample_pos;
+ pa_operation *op;
+
+ if (time_sec < 0) time_sec = 0;
+ sample_pos = (size_t) (time_sec * mp3d.channels * mp3d.rate);
+ decoder_seek(sample_pos);
+
+ op = pa_stream_flush(pa_strm, NULL, NULL);
+ pa_operation_unref(op);
+ mp3d.seek = true;
+
+ mplay_status(MPLAY_ACTION_STATUS_SEEK, "%02.2f", user_time());
+}
+
+static void
+cmd_playpause(void)
+{
+ pa_operation *op;
+
+ mp3d.pause ^= 1;
+ op = pa_stream_cork(pa_strm, mp3d.pause, NULL, NULL);
+ pa_operation_unref(op);
+
+ mplay_status(MPLAY_ACTION_STATUS_PLAYPAUSE, "%i", mp3d.pause);
+}
+
+static void
+cmd_status(void)
+{
+ update_sink_input_info();
+
+ mplay_status(MPLAY_ACTION_STATUS_STATUS,
+ "volume=%02.2f,time=%02.2f,pause=%i",
+ user_vol(), user_time(), mp3d.pause);
+}
+
+static bool
+key_input(void)
+{
+ char linebuf[16], *end;
+ float fval;
+ int key;
+
+ while ((key = mplay_getkey()) == MPLAY_KEY_EOF);
+
+ pa_threaded_mainloop_lock(pa_mloop);
+
+ switch (mplay_action(key)) {
+ case MPLAY_ACTION_VOL:
+ get_line(linebuf, sizeof(linebuf));
+ fval = strtof(linebuf, &end);
+ if (!end || *end) {
+ mplay_status(MPLAY_ACTION_STATUS_VOL, "fail:bad float");
+ break;
+ }
+ cmd_setvol(fval);
+ break;
+ case MPLAY_ACTION_VOL_UP:
+ cmd_setvol(user_vol() + 5);
+ break;
+ case MPLAY_ACTION_VOL_DOWN:
+ cmd_setvol(user_vol() - 5);
+ break;
+ case MPLAY_ACTION_SEEK:
+ get_line(linebuf, sizeof(linebuf));
+ fval = strtof(linebuf, &end);
+ if (!end || *end) {
+ mplay_status(MPLAY_ACTION_STATUS_SEEK, "fail:bad float");
+ break;
+ }
+ cmd_seek(fval);
+ break;
+ case MPLAY_ACTION_SEEK_BWD:
+ cmd_seek(user_time() - 5);
+ break;
+ case MPLAY_ACTION_SEEK_FWD:
+ cmd_seek(user_time() + 5);
+ break;
+ case MPLAY_ACTION_PLAYPAUSE:
+ cmd_playpause();
+ break;
+ case MPLAY_ACTION_STATUS:
+ cmd_status();
+ break;
+ default:
+ mplay_status(MPLAY_STATUS_INPUT, "fail");
+ break;
+ }
+
+ pa_threaded_mainloop_unlock(pa_mloop);
+
+ return true;
+}
+
+static void *
+input_worker(void *arg)
+{
+ input_worker_alive = true;
+
+ setvbuf(stdin, 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_STATUS_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.mp3");
+ 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)
+{
+ pa_channel_map pa_chmap;
+ int ret;
+
+ pa_channel_map_init_stereo(&pa_chmap);
+
+ pa_strm = pa_stream_new(pa_ctx, "mplay.mp3", &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);
+
+ munmap(audiofile.data, audiofile.len);
+
+ if (input_worker_alive)
+ pthread_kill(input_worker_thread, SIGINT);
+}
+
+static void
+usage(void)
+{
+ fprintf(stderr, "Usage: mplay.mp3 [" 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);
+
+ audiofile.data = map_file(file, &audiofile.len);
+ decoder_init();
+
+ pa_spec.channels = (uint8_t) mp3d.channels;
+ pa_spec.rate = (uint32_t) mp3d.rate;
+
+ 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;
+}