nirv.git

nirv.c

espurr
/* nirv - music player
 * Copyright (C) 2023 ArcNyxx
 * see LICENCE file for licensing information */

#include <limits.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <alsa/asoundlib.h>
#include <FLAC/stream_decoder.h>

#define SONG "/tmp/nirv-song"
#define INFO "/tmp/nirv-info"

#define ST meta->data.stream_info
#define CM meta->data.vorbis_comment
#define COMMENT(var, name)                                                    \
if (!memcmp(CM.comments[i].entry, name, sizeof(name) - 1)) {                  \
	if ((var = strdup((char *)                                            \
			CM.comments[i].entry + sizeof(name) - 1)) == NULL)    \
		DIEN("unable to allocate memory: ");                          \
}

#define DIEN(fmt) die("nirv: " fmt)
#define DIES(fmt) die("nirv: " fmt ": %s\n", snd_strerror(err))
#define DIEV(fmt, ...) die("nirv: " fmt, __VA_ARGS__)

static volatile sig_atomic_t run = 1;
static uint32_t data[FLAC__MAX_CHANNELS][FLAC__MAX_BLOCK_SIZE],
		len, bits, chan, rate;
static char path[PATH_MAX], *song, *album, *artist;
static uint64_t elapsed, duration;

static void die(const char *fmt, ...);
static void term(int signo);
static FLAC__StreamDecoderWriteStatus sound(const FLAC__StreamDecoder *dec,
		const FLAC__Frame *frame, const FLAC__int32 *const *buf,
		void *data);
static void meta(const FLAC__StreamDecoder *dec,
		const FLAC__StreamMetadata *meta, void *data);
static void error(const FLAC__StreamDecoder *dec,
		FLAC__StreamDecoderErrorStatus err, void *data);

static void
die(const char *fmt, ...)
{
	va_list list;
	va_start(list, fmt);
	vfprintf(stderr, fmt, list);
	va_end(list);

	if (fmt[strlen(fmt) - 1] != '\n')
		perror(NULL);
	exit(1);
}

static void
term(int signo)
{
	run = 0;
}

static FLAC__StreamDecoderWriteStatus
sound(const FLAC__StreamDecoder *dec, const FLAC__Frame *frame,
		const FLAC__int32 *const *buf, void *null)
{
	if (frame->header.bits_per_sample != bits || frame->header.channels !=
			chan || frame->header.sample_rate != rate)
		DIEN("flac file does not conform to subset format\n");
	len = frame->header.blocksize;
	for (uint32_t i = 0; i < chan; ++i)
		memcpy(data[i], buf[i], len * sizeof(int));
	return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
}

static void
meta(const FLAC__StreamDecoder *dec, const FLAC__StreamMetadata *meta,
		void *null)
{
	if (meta->type == FLAC__METADATA_TYPE_STREAMINFO) {
		duration = ST.total_samples; elapsed = 0; chan = ST.channels;
		bits = ST.bits_per_sample; rate = ST.sample_rate; 
		return;
	}

	free(song); free(album); free(artist); song = album = artist = NULL;
	for (uint32_t i = 0; i < CM.num_comments; ++i)
		COMMENT(song, "TITLE=") else COMMENT(artist, "ARTIST=")
			else COMMENT(album, "ALBUM=")
}

static void
error(const FLAC__StreamDecoder *dec, FLAC__StreamDecoderErrorStatus err,
		void *null)
{
	DIEV("flac decoding error: %s\n",
			FLAC__StreamDecoderErrorStatusString[err]);
}

int
main(void)
{
	struct sigaction act = { .sa_handler = term };
	sigfillset(&act.sa_mask);
	sigaction(SIGINT, &act, NULL);
	sigaction(SIGTERM, &act, NULL);
	sigdelset(&act.sa_mask, SIGINT);
	sigdelset(&act.sa_mask, SIGTERM);
	sigprocmask(SIG_BLOCK, &act.sa_mask, NULL);
	sigemptyset(&act.sa_mask);
	sigaddset(&act.sa_mask, SIGRTMIN+1);
	sigaddset(&act.sa_mask, SIGRTMIN+2);

	int err;
	snd_pcm_t *pcm;
	if ((err = snd_pcm_open(&pcm, "default", SND_PCM_STREAM_PLAYBACK, 0)))
		DIES("unable to open default pcm device");
	FLAC__StreamDecoder *dec;
	if ((dec = FLAC__stream_decoder_new()) == NULL)
		DIEN("unable to allocate new flac decoder\n");

	int num;
	if ((num = snd_pcm_poll_descriptors_count(pcm)) <= 0)
		DIEN("unable to get poll descriptors count\n");
	struct pollfd fds[num];
	if (snd_pcm_poll_descriptors(pcm, fds, num) != num)
		DIEN("unable to get poll descriptors\n");
	void *arr[FLAC__MAX_CHANNELS];
	for (uint32_t i = 0; i < FLAC__MAX_CHANNELS; ++i)
		arr[i] = &data[i];

song:
	while (run && (realpath(SONG, path) == NULL || !strcmp(SONG, path)))
		sleep(1);
	if (!run)
		goto quit;

	if (!FLAC__stream_decoder_set_metadata_respond(dec,
			FLAC__METADATA_TYPE_VORBIS_COMMENT))
		DIEN("unable to set flac decoder metadata response\n");
	if ((err = FLAC__stream_decoder_init_file(dec, path, sound, meta,
			error, NULL)) != FLAC__STREAM_DECODER_INIT_STATUS_OK)
		DIEV("unable to initialise flac decoder: %s\n",
				FLAC__StreamDecoderInitStatusString[err]);

	const uint32_t _bits = bits, _chan = chan, _rate = rate;
	if (!FLAC__stream_decoder_process_until_end_of_metadata(dec))
		DIEN("unable to process flac metadata\n");
	int pause = 0;
	if (_bits == bits && _chan == chan && _rate == rate)
		goto loop;

	static const snd_pcm_format_t fmts[4] = { SND_PCM_FORMAT_S8,
		SND_PCM_FORMAT_S16, SND_PCM_FORMAT_S24, SND_PCM_FORMAT_S32 };
	snd_pcm_format_t fmt = bits % 8 ? bits == 20 ? SND_PCM_FORMAT_S20 :
			SND_PCM_FORMAT_UNKNOWN : fmts[bits / 8 - 1];

	if (snd_pcm_state(pcm) == SND_PCM_STATE_RUNNING &&
			(err = snd_pcm_drain(pcm)))
		DIES("unable to drain pcm device");

	snd_pcm_hw_params_t *param;
	snd_pcm_hw_params_alloca(&param);
	snd_pcm_hw_params_any(pcm, param);
	if ((err = snd_pcm_hw_params_set_access(pcm, param,
			SND_PCM_ACCESS_RW_NONINTERLEAVED)))
		DIES("unable to set alsa hw param: access format");
	if ((err = snd_pcm_hw_params_set_format(pcm, param, fmt)))
		DIES("unable to set alsa hw param: bit format");
	if ((err = snd_pcm_hw_params_set_channels(pcm, param, chan)))
		DIES("unable to set alsa hw param: number of channels");
	if ((err = snd_pcm_hw_params_set_rate(pcm, param, rate, 0)))
		DIES("unable to set alsa hw param: sample rate");
	if ((err = snd_pcm_hw_params_set_buffer_size(pcm, param, 4096)))
		DIES("unable to set alsa hw param: buffer size");
	if ((err = snd_pcm_hw_params(pcm, param)))
		DIES("unable to set alsa hw params");

	snd_pcm_sw_params_t *swparam;
	snd_pcm_sw_params_alloca(&swparam);
	snd_pcm_sw_params_current(pcm, swparam);
	if ((err = snd_pcm_sw_params_set_start_threshold(pcm, swparam, 4096)))
		DIES("unable to set alsa sw param: start threshold");
	if ((err = snd_pcm_sw_params_set_avail_min(pcm, swparam, 256)))
		DIES("unable to set alsa sw param: period size");
	if ((err = snd_pcm_sw_params_set_period_event(pcm, swparam, true)))
		DIES("unable to set alsa sw param: period event trigger");
	if ((err = snd_pcm_sw_params(pcm, swparam)))
		DIES("unable to set alsa sw params");

loop: ;
	siginfo_t sig;
	while (sigtimedwait(&act.sa_mask, &sig, &(struct timespec)
			{ pause, 0 }) != -1) {
		if (sig.si_signo == SIGRTMIN+1) {
			if ((pause = !pause)) {
				if ((err = snd_pcm_drain(pcm)))
					DIES("unable to drain pcm device");
			} else {
				if ((err = snd_pcm_prepare(pcm)))
					DIES("unable to start pcm device");
			}
		} else if (sig.si_signo == SIGRTMIN+2) {
			const int64_t change = sig.si_value.sival_int * rate;
			if (sig.si_value.sival_int >= 0) {
				if ((elapsed += change) >= duration)
					goto done;
			} else {
				if ((elapsed -= -change) >= duration)
					elapsed = 0;
			}
			if (!FLAC__stream_decoder_reset(dec))
				DIEN("unable to reset flac decoder\n");
			if (!FLAC__stream_decoder_seek_absolute(dec, elapsed))
				DIEN("unable to seek sample in flac file\n");
		}
	}
	if (!run)
		goto done;
	if (pause)
		goto loop;

	if (poll(fds, num, 1000) == -1 && errno != EINTR)
		DIEN("unable to poll descriptors: ");
	unsigned short evt;
	if ((err = snd_pcm_poll_descriptors_revents(pcm, fds, num, &evt)))
		DIES("unable to merge mangled poll revents");
	if (evt & POLLERR && snd_pcm_prepare(pcm))
		DIES("pcm device buffer underrun");
	if (!(evt & POLLOUT))
		goto loop;

	if (len != 0) {
		if ((err = snd_pcm_avail_update(pcm)) < 0)
			DIES("unable to query amount of pcm data to write");
		const uint32_t amt = (uint32_t)err < len ? (uint32_t)err : len;
		if ((err = snd_pcm_writen(pcm, arr, amt)) < 0) {
			switch (err) {
			default: break;
			case -EPIPE:
				if ((err = snd_pcm_prepare(pcm)))
					DIES("pcm device buffer underrun");
				break;
			case -ESTRPIPE:
				while ((err = snd_pcm_resume(pcm)) == -EAGAIN);
				if (err != 0 || (err = snd_pcm_prepare(pcm)))
					DIES("pcm device suspended");
			}
			if ((err = snd_pcm_writen(pcm, arr, amt)) < 0)
				DIES("unable to write pcm data");
		}
		len -= err;
		for (uint32_t i = 0; i < chan; ++i)
			memmove(data[i], data[i] + err, len * sizeof(int));
		elapsed += err;
	}
	if (len == 0) {
		if (FLAC__stream_decoder_get_state(dec) ==
				FLAC__STREAM_DECODER_END_OF_STREAM)
			goto done;
		if (!FLAC__stream_decoder_process_single(dec))
			DIEN("unable to decode flac file\n");
	}

	int fd;
	if ((fd = open(INFO, O_CREAT | O_WRONLY, 0666)) == -1)
		DIEN("unable to create or open file: " INFO ": ");
	if (dprintf(fd, "%s\n%s\n%s\n%s\n%02ld:%02ld\n%02ld:%02ld\n",
			path, song, album, artist,
			elapsed / rate / 60, elapsed / rate % 60,
			duration / rate / 60, duration / rate % 60) < 0)
		DIEN("unable to completely write buffer to file: " INFO ": ");
	if (close(fd) == -1)
		DIEN("unable to close file: " INFO ": ");
	if (run)
		goto loop;

done:
	FLAC__stream_decoder_finish(dec);
	if (unlink(INFO) == -1 && errno != ENOENT)
		DIEN("unable to remove file: " INFO ": ");
	if (run)
		goto song;

quit:
	FLAC__stream_decoder_delete(dec);
	snd_pcm_close(pcm);
}