mplay.mp3

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

mplay.mp3.c (12426B)


      1#include "mplay.h"
      2
      3#define MINIMP3_IMPLEMENTATION
      4#include "minimp3_ex.h"
      5
      6#include <pulse/sample.h>
      7#include <pulse/introspect.h>
      8#include <pulse/operation.h>
      9#include <pulse/stream.h>
     10#include <pulse/thread-mainloop.h>
     11#include <pulse/volume.h>
     12#include <pulse/pulseaudio.h>
     13#include <pulse/def.h>
     14
     15#include <sys/mman.h>
     16#include <sys/stat.h>
     17#include <pthread.h>
     18#include <fcntl.h>
     19#include <termios.h>
     20#include <unistd.h>
     21
     22#include <stdarg.h>
     23#include <stdint.h>
     24#include <stdbool.h>
     25#include <stdio.h>
     26#include <stdlib.h>
     27
     28struct decoder {
     29	mp3dec_ex_t dec;
     30	size_t sample_next;
     31	size_t samples_max;
     32	size_t samples_read;
     33
     34	int rate;
     35	int channels;
     36
     37	bool seek;
     38	bool init;
     39	bool pause;
     40};
     41
     42static pa_buffer_attr pa_buf = {
     43	.fragsize = UINT32_MAX,
     44	.maxlength = UINT32_MAX,
     45	.tlength = UINT32_MAX,
     46	.prebuf = UINT32_MAX,
     47	.minreq = UINT32_MAX,
     48};
     49
     50static pa_stream_flags_t pa_stream_flags = PA_STREAM_START_CORKED
     51	| PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY;
     52
     53static pa_threaded_mainloop *pa_mloop;
     54static pa_context *pa_ctx;
     55static pa_stream *pa_strm;
     56static pa_sink_input_info pa_strm_sink;
     57static bool pa_strm_sink_update;
     58
     59static struct decoder decoder = { 0 };
     60
     61static pthread_t input_worker_thread;
     62static bool input_worker_alive = false;
     63
     64static struct termios term_old;
     65static struct termios term_new;
     66static bool term_set = false;
     67
     68static bool headless = false;
     69static bool verbose = false;
     70static bool istty = false;
     71
     72static void
     73__attribute__((noreturn))
     74__attribute__((format(printf, 1, 2)))
     75die(const char *fmt, ...)
     76{
     77	va_list ap;
     78
     79	fputs("mplay.mp3: ", stderr);
     80	va_start(ap, fmt);
     81	vfprintf(stderr, fmt, ap);
     82	va_end(ap);
     83	if (*fmt && fmt[strlen(fmt)-1] == ':') {
     84		perror(NULL);
     85	} else {
     86		fputc('\n', stderr);
     87	}
     88
     89	exit(1);
     90}
     91
     92static void
     93term_set_canonical(void)
     94{
     95	term_new.c_lflag |= ICANON;
     96	term_new.c_lflag |= ECHO;
     97	if (tcsetattr(0, TCSANOW, &term_new))
     98		die("tcsetattr:");
     99}
    100
    101static void
    102term_set_raw(void)
    103{
    104	term_new.c_lflag &= ~(0U | ICANON);
    105	term_new.c_lflag &= ~(0U | ECHO);
    106	if (tcsetattr(0, TCSANOW, &term_new))
    107		die("tcsetattr:");
    108}
    109
    110static void
    111get_line(char *buf, int size)
    112{
    113	char *c;
    114
    115	if (istty) putc('>', stderr);
    116	if (istty) term_set_canonical();
    117	fgets(buf, size, stdin);
    118	if ((c = strchr(buf, '\n'))) *c = '\0';
    119	if (istty) term_set_raw();
    120}
    121
    122static void
    123decoder_init(const char *filepath)
    124{
    125	if (mp3dec_ex_open(&decoder.dec, filepath, MP3D_SEEK_TO_SAMPLE))
    126		die("mp3dec_ex_open");
    127
    128	decoder.sample_next = 0;
    129	decoder.samples_max = 0;
    130	decoder.samples_max = decoder.dec.samples / (size_t)decoder.dec.info.channels;
    131
    132	decoder.channels = decoder.dec.info.channels;
    133	decoder.rate = decoder.dec.info.hz;
    134
    135	decoder.seek = false;
    136	decoder.pause = false;
    137	decoder.init = true;
    138}
    139
    140static void
    141decoder_seek(size_t sample_pos)
    142{
    143	mp3dec_ex_seek(&decoder.dec, sample_pos * (size_t)decoder.channels);
    144	decoder.sample_next = sample_pos;
    145}
    146
    147static void
    148pa_stream_drain_callback(pa_stream *stream, int success, void *data)
    149{
    150	mplay_status(MPLAY_INFO_EXIT, NULL);
    151	exit(0);
    152}
    153
    154static void
    155pa_stream_write_callback(pa_stream *stream, size_t requested, void *data)
    156{
    157	mp3dec_frame_info_t info;
    158	mp3d_sample_t *samples;
    159	pa_operation *op;
    160	ssize_t remaining;
    161	size_t cnt;
    162
    163	remaining = (ssize_t) requested;
    164	while (remaining > 0) {
    165		cnt = mp3dec_ex_read_frame(&decoder.dec, &samples, &info, requested);
    166		if (!cnt) {
    167			op = pa_stream_drain(pa_strm,
    168				pa_stream_drain_callback, NULL);
    169			pa_operation_unref(op);
    170			return;
    171		}
    172
    173		pa_stream_write(stream, samples,
    174			cnt * sizeof(mp3d_sample_t), NULL, 0,
    175			decoder.seek ? PA_SEEK_RELATIVE_ON_READ: PA_SEEK_RELATIVE);
    176
    177		decoder.seek = false;
    178		decoder.sample_next += cnt / (size_t)decoder.channels;
    179		decoder.samples_read = decoder.sample_next;
    180		decoder.samples_max = MAX(decoder.samples_max, decoder.samples_read);
    181		remaining -= (ssize_t) (cnt * (size_t)decoder.channels * sizeof(mp3d_sample_t));
    182	}
    183}
    184
    185static void
    186pa_stream_underflow_callback(struct pa_stream *stream, void *data)
    187{
    188	fprintf(stderr, "pulseaudio underflow!\n");
    189}
    190
    191static void
    192pa_stream_overflow_callback(struct pa_stream *stream, void *data)
    193{
    194	fprintf(stderr, "pulseaudio overflow!\n");
    195}
    196
    197static void
    198update_sink_input_info_callback(struct pa_context *ctx,
    199	const pa_sink_input_info *info, int eol, void *data)
    200{
    201	if (eol) return;
    202	memcpy(&pa_strm_sink, info, sizeof(pa_sink_input_info));
    203	pa_strm_sink_update = true;
    204	pa_threaded_mainloop_signal(pa_mloop, true);
    205}
    206
    207static void
    208update_sink_input_info(void)
    209{
    210	pa_operation *op;
    211
    212	pa_strm_sink_update = false;
    213	op = pa_context_get_sink_input_info(pa_ctx,
    214		pa_stream_get_index(pa_strm),
    215		update_sink_input_info_callback, NULL);
    216	if (!op) die("pa_context_get_sink_input_info failed");
    217
    218	while (!pa_strm_sink_update) {
    219		pa_threaded_mainloop_unlock(pa_mloop);
    220		pa_threaded_mainloop_wait(pa_mloop);
    221		pa_threaded_mainloop_lock(pa_mloop);
    222	}
    223
    224	pa_threaded_mainloop_accept(pa_mloop);
    225
    226	pa_operation_unref(op);
    227}
    228
    229static size_t
    230stream_samples_buffered(void)
    231{
    232	static size_t last_read_index = 0, last_write_index = 0;
    233	const pa_timing_info *info;
    234	size_t buffered;
    235
    236	info = pa_stream_get_timing_info(pa_strm);
    237	if (!info || info->write_index_corrupt || info->read_index_corrupt)
    238		return 0;
    239
    240	if (info->read_index != last_read_index)
    241		last_write_index = (size_t) MAX(0, info->write_index);
    242
    243	buffered = (last_write_index - last_read_index)
    244		/ ((size_t) decoder.channels * sizeof(mp3d_sample_t));
    245
    246	last_read_index = (size_t) MAX(0, info->read_index);
    247
    248	return buffered;
    249}
    250
    251static double
    252user_vol(void)
    253{
    254	pa_volume_t vol;
    255
    256	update_sink_input_info();
    257	if (!pa_strm_sink.has_volume)
    258		return -1;
    259
    260	vol = pa_cvolume_avg(&pa_strm_sink.volume);
    261
    262	return (double) vol * 100.F / (double) PA_VOLUME_NORM;
    263}
    264
    265static double
    266user_pos(void)
    267{
    268	ssize_t sample_pos;
    269
    270	sample_pos = (ssize_t) decoder.samples_read;
    271	sample_pos -= (ssize_t) stream_samples_buffered();
    272	sample_pos = MAX(0, sample_pos);
    273
    274	return (double) sample_pos * 1.F / (double) decoder.rate;
    275}
    276
    277static double
    278user_end(void)
    279{
    280	return (double) decoder.samples_max * 1.F / (double) decoder.rate;
    281}
    282
    283static void
    284cmd_setvol(double vol, double delta)
    285{
    286	pa_operation *op;
    287
    288	pa_threaded_mainloop_lock(pa_mloop);
    289
    290	if (delta != 0) {
    291		vol = user_vol() + delta;
    292	}
    293
    294	update_sink_input_info();
    295	pa_cvolume_set(&pa_strm_sink.volume, 2,
    296		(pa_volume_t) (vol * PA_VOLUME_NORM / 100.F));
    297	if (pa_cvolume_avg(&pa_strm_sink.volume) > 2 * PA_VOLUME_NORM)
    298		pa_cvolume_set(&pa_strm_sink.volume, 2, 2 * PA_VOLUME_NORM);
    299
    300	op = pa_context_set_sink_input_volume(pa_ctx,
    301		pa_stream_get_index(pa_strm),
    302		&pa_strm_sink.volume, NULL, NULL);
    303	pa_operation_unref(op);
    304
    305	mplay_status(MPLAY_INFO_VOLUME, "%02.2f", (double) pa_cvolume_avg(&pa_strm_sink.volume)
    306		* 100.f / (double) PA_VOLUME_NORM);
    307	
    308	pa_threaded_mainloop_unlock(pa_mloop);
    309}
    310
    311static void
    312cmd_seek(double time_sec, double delta)
    313{
    314	size_t sample_pos;
    315	pa_operation *op;
    316
    317	pa_threaded_mainloop_lock(pa_mloop);
    318
    319	if (delta != 0) {
    320		time_sec = user_pos() + delta;
    321	}
    322
    323	if (time_sec < 0) time_sec = 0;
    324	sample_pos = (size_t) (time_sec * decoder.rate);
    325	decoder_seek(sample_pos);
    326
    327	op = pa_stream_flush(pa_strm, NULL, NULL);
    328	pa_operation_unref(op);
    329	decoder.seek = true;
    330
    331	mplay_status(MPLAY_INFO_SEEK, "%02.2f", user_pos());
    332
    333	pa_threaded_mainloop_unlock(pa_mloop);
    334}
    335
    336static void
    337cmd_playpause(void)
    338{
    339	pa_operation *op;
    340
    341	pa_threaded_mainloop_lock(pa_mloop);
    342
    343	decoder.pause ^= 1;
    344	op = pa_stream_cork(pa_strm, decoder.pause, NULL, NULL);
    345	pa_operation_unref(op);
    346
    347	mplay_status(MPLAY_INFO_PAUSE, "%i", decoder.pause);
    348
    349	pa_threaded_mainloop_unlock(pa_mloop);
    350}
    351
    352static void
    353cmd_status(void)
    354{
    355	pa_threaded_mainloop_lock(pa_mloop);
    356
    357	update_sink_input_info();
    358
    359	mplay_status(MPLAY_INFO_STATUS,
    360		"volume=%02.2f,pos=%02.2f,end=%02.2f,pause=%i",
    361		user_vol(), user_pos(), user_end(), decoder.pause);
    362
    363	pa_threaded_mainloop_unlock(pa_mloop);
    364}
    365
    366static bool
    367key_input(void)
    368{
    369	char linebuf[16], *end;
    370	float fval;
    371	int key;
    372
    373	while ((key = mplay_getkey()) == MPLAY_KEY_INV);
    374	if (key == MPLAY_KEY_EOF) return false;
    375
    376	switch (mplay_action(key)) {
    377	case MPLAY_ACTION_VOLUME:
    378		get_line(linebuf, sizeof(linebuf));
    379		fval = strtof(linebuf, &end);
    380		if (!end || *end) {
    381			mplay_status(MPLAY_INFO_VOLUME, "fail:bad float");
    382			break;
    383		}
    384		cmd_setvol(fval, 0);
    385		break;
    386	case MPLAY_ACTION_VOLUME_UP:
    387		cmd_setvol(0, 5);
    388		break;
    389	case MPLAY_ACTION_VOLUME_DOWN:
    390		cmd_setvol(0, -5);
    391		break;
    392	case MPLAY_ACTION_SEEK:
    393		get_line(linebuf, sizeof(linebuf));
    394		fval = strtof(linebuf, &end);
    395		if (!end || *end) {
    396			mplay_status(MPLAY_INFO_SEEK, "fail:bad float");
    397			break;
    398		}
    399		cmd_seek(fval, 0);
    400		break;
    401	case MPLAY_ACTION_SEEK_BWD:
    402		cmd_seek(0, -5);
    403		break;
    404	case MPLAY_ACTION_SEEK_FWD:
    405		cmd_seek(0, 5);
    406		break;
    407	case MPLAY_ACTION_PAUSE:
    408		cmd_playpause();
    409		break;
    410	case MPLAY_ACTION_STATUS:
    411		cmd_status();
    412		break;
    413	default:
    414		mplay_status(MPLAY_INFO_INPUT, "fail");
    415		break;
    416	}
    417
    418	return true;
    419}
    420
    421static void *
    422input_worker(void *arg)
    423{
    424	input_worker_alive = true;
    425
    426	setvbuf(stdin, NULL, _IONBF, 0);
    427	setvbuf(stdout, NULL, _IONBF, 0);
    428	setvbuf(stderr, NULL, _IONBF, 0);
    429
    430	if (istty) {
    431		if (tcgetattr(0, &term_old))
    432			die("tcgetattr:");
    433		term_new = term_old;
    434		term_new.c_iflag |= BRKINT;
    435		term_new.c_iflag &= ~(0U | IGNBRK);
    436		term_set = true;
    437
    438		term_set_raw();
    439	}
    440
    441	mplay_status(MPLAY_INFO_READY, NULL);
    442
    443	while (key_input());
    444
    445	return NULL;
    446}
    447
    448static void
    449pulse_context_init(void)
    450{
    451	pa_mainloop_api *pa_mloop_api;
    452	int ret;
    453
    454	pa_mloop_api = pa_threaded_mainloop_get_api(pa_mloop);
    455	if (!pa_mloop_api) die("pa_threaded_mainloop_get_api");
    456
    457	pa_ctx = pa_context_new(pa_mloop_api, "mplay.mp3");
    458	if (!pa_ctx) die("pa_context_new");
    459
    460	ret = pa_context_connect(pa_ctx, NULL, 0, NULL);
    461	if (ret) die("pa_context_connect: %s",
    462		pa_strerror(pa_context_errno(pa_ctx)));
    463
    464	while (pa_context_get_state(pa_ctx) != PA_CONTEXT_READY) {
    465		pa_threaded_mainloop_unlock(pa_mloop);
    466		pa_threaded_mainloop_wait(pa_mloop);
    467		pa_threaded_mainloop_lock(pa_mloop);
    468	}
    469}
    470
    471static void
    472pulse_stream_init(void)
    473{
    474	pa_channel_map pa_chmap;
    475	pa_sample_spec pa_spec;
    476	int ret;
    477
    478	pa_spec.format = PA_SAMPLE_S16LE;
    479	pa_spec.channels = (uint8_t) decoder.channels;
    480	pa_spec.rate = (uint32_t) decoder.rate;
    481
    482	pa_channel_map_init_stereo(&pa_chmap);
    483
    484	pa_strm = pa_stream_new(pa_ctx, "mplay.mp3", &pa_spec, &pa_chmap);
    485	if (!pa_strm) die("pa_stream_new: %s",
    486		pa_strerror(pa_context_errno(pa_ctx)));
    487
    488	pa_stream_set_write_callback(pa_strm, pa_stream_write_callback, NULL);
    489
    490	if (verbose) {
    491		pa_stream_set_underflow_callback(pa_strm,
    492			pa_stream_underflow_callback, NULL);
    493		pa_stream_set_overflow_callback(pa_strm,
    494			pa_stream_overflow_callback, NULL);
    495	}
    496
    497	ret = pa_stream_connect_playback(pa_strm, NULL,
    498		&pa_buf, pa_stream_flags, NULL, NULL);
    499	if (ret) die("pa_stream_connect_playback failed");
    500
    501	while (pa_stream_get_state(pa_strm) != PA_STREAM_READY) {
    502		pa_threaded_mainloop_unlock(pa_mloop);
    503		pa_threaded_mainloop_wait(pa_mloop);
    504		pa_threaded_mainloop_lock(pa_mloop);
    505	}
    506}
    507
    508static void
    509sigint_handler(int sig)
    510{
    511	exit(0);
    512}
    513
    514static void
    515cleanup(void)
    516{
    517	if (term_set) tcsetattr(0, TCSANOW, &term_old);
    518
    519	pa_stream_disconnect(pa_strm);
    520	pa_stream_unref(pa_strm);
    521
    522	pa_context_disconnect(pa_ctx);
    523	pa_context_unref(pa_ctx);
    524
    525	if (input_worker_alive)
    526		pthread_kill(input_worker_thread, SIGINT);
    527}
    528
    529static void
    530usage(void)
    531{
    532	fprintf(stderr, "Usage: mplay.mp3 [" MPLAY_FLAG_HEADLESS "] FILE\n");
    533	exit(1);
    534}
    535
    536int
    537main(int argc, const char **argv)
    538{
    539	pthread_mutex_t lock;
    540	const char **arg;
    541	const char *file;
    542
    543	file = NULL;
    544	for (arg = argv + 1; *arg; arg++) {
    545		if (!strcmp(*arg, MPLAY_FLAG_HEADLESS)) {
    546			headless = true;
    547		} else if (!strcmp(*arg, "-v")) {
    548			verbose = true;
    549		} else {
    550			if (file) usage();
    551			file = *arg;
    552		}
    553	}
    554	if (!file) usage();
    555
    556	istty = isatty(0);
    557
    558	decoder_init(file);
    559
    560	pa_mloop = pa_threaded_mainloop_new();
    561	if (!pa_mloop) die("pa_threaded_mainloop_new");
    562
    563	pa_threaded_mainloop_start(pa_mloop);
    564
    565	pa_threaded_mainloop_lock(pa_mloop);
    566	pulse_context_init();
    567	pulse_stream_init();
    568	pa_stream_cork(pa_strm, 0, NULL, NULL);
    569	pa_threaded_mainloop_unlock(pa_mloop);
    570
    571	atexit(cleanup);
    572	signal(SIGINT, sigint_handler);
    573
    574	if (!headless) {
    575		if (pthread_create(&input_worker_thread, NULL, input_worker, NULL))
    576			die("pthread_create");
    577		pthread_join(input_worker_thread, NULL);
    578	} else {
    579		pthread_mutex_init(&lock, NULL);
    580		pthread_mutex_lock(&lock);
    581		pthread_mutex_lock(&lock);
    582	}
    583
    584	return 0;
    585}