diff
.gitignore | 1 +
Makefile | 32 +
README.md | 45 +
jam.c | 2836 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
jamrc | 24 +
5 files changed, 2938 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b883a94
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+jam
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..daeec29
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,32 @@
+CC ?= cc
+PREFIX ?= /usr
+DESTDIR ?=
+
+CFLAGS ?= -O2 -Wall -Wextra -Wpedantic
+LDFLAGS ?=
+LDLIBS ?= -lasound
+
+FFMPEG_CFLAGS ?=
+FFMPEG_LIBS ?=
+
+ifneq ($(strip $(FFMPEG_LIBS)),)
+CFLAGS += -DHAVE_FFMPEG $(FFMPEG_CFLAGS)
+LDLIBS += $(FFMPEG_LIBS)
+endif
+
+all: jam
+
+jam: jam.c
+ $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LDLIBS)
+
+install: jam
+ mkdir -p $(DESTDIR)$(PREFIX)/bin
+ cp -f jam $(DESTDIR)$(PREFIX)/bin/jam
+
+uninstall:
+ rm -f $(DESTDIR)$(PREFIX)/bin/jam
+
+clean:
+ rm -f jam
+
+.PHONY: all install uninstall clean
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..67d4640
--- /dev/null
+++ b/README.md
@@ -0,0 +1,45 @@
+JAM - Jank (ASS) Alsa Music-player
+
+dependencies:
+ alsa
+ ffmpeg # optional, for mp3/m4a/flac/etc support
+
+build:
+ make
+ # with ffmpeg support
+ make FFMPEG_LIBS="-lavformat -lavcodec -lswresample -lavutil"
+
+make (un)install:
+ make install
+ make uninstall
+
+usage:
+ jam song.wav
+ jam .
+ jam dir/*
+ jam -R -r 48000 -c 2 -v 80 -s 10 song.pcm
+ cat song.pcm | jam -R -r 44100 -c 2
+
+arguments:
+ -r RATE sample rate [default 44100]
+ -c CHANNELS channels [default 2 ]
+ -v VOLUME 0-100 [default 100 ]
+ -s SECONDS seek start [wav only ]
+ -d DEVICE alsa device [or --device ]
+ -R raw PCM 16-b little endian [no header ]
+
+default keybinds:
+ space pause/resume
+ h seek -5s
+ l seek +5s
+ k volume +5%
+ j volume -5%
+ n next file
+ p previous file
+ r toggle repeat
+ s toggle shuffle
+ / search
+ q quit
+
+configuration:
+ refer to jamrc
diff --git a/jam.c b/jam.c
new file mode 100644
index 0000000..54482a0
--- /dev/null
+++ b/jam.c
@@ -0,0 +1,2836 @@
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <alsa/asoundlib.h>
+#include <ctype.h>
+#include <dirent.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <termios.h>
+#include <time.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+
+#ifdef HAVE_FFMPEG
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+#include <libavutil/channel_layout.h>
+#include <libavutil/version.h>
+#include <libavutil/opt.h>
+#include <libswresample/swresample.h>
+#include <libswresample/version.h>
+#endif
+
+#define DEFAULT_RATE 44100
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_VOLUME 100
+#define BUFFER_FRAMES 1024
+#define SEEK_SECONDS 5.0
+#define MAX_LINE 80
+
+static volatile sig_atomic_t g_stop = 0;
+static volatile sig_atomic_t g_quit = 0;
+static char g_search_path[PATH_MAX];
+
+struct wav_info {
+ unsigned int rate;
+ unsigned int channels;
+ unsigned int bits;
+ off_t data_offset;
+ off_t data_size;
+ bool ok;
+};
+
+struct keyseq {
+ char seq[16];
+ size_t len;
+};
+
+struct keylist {
+ struct keyseq seqs[8];
+ int count;
+};
+
+struct keymap {
+ struct keylist quit;
+ struct keylist pause;
+ struct keylist vol_down;
+ struct keylist vol_up;
+ struct keylist seek_back;
+ struct keylist seek_fwd;
+ struct keylist next;
+ struct keylist prev;
+ struct keylist repeat;
+ struct keylist shuffle;
+ struct keylist search;
+};
+
+struct player_state {
+ snd_pcm_t *pcm;
+ struct keymap *keys;
+ bool *repeat;
+ bool *shuffle;
+ bool paused;
+ bool seekable;
+ unsigned int rate;
+ unsigned int channels;
+ unsigned int bits;
+ int volume;
+ off_t data_offset;
+ off_t data_size;
+ off_t data_pos;
+ size_t frame_bytes;
+};
+
+enum play_action {
+ PLAY_DONE = 0,
+ PLAY_NEXT,
+ PLAY_PREV,
+ PLAY_SEARCH,
+ PLAY_QUIT,
+ PLAY_ERR
+};
+
+enum key_action {
+ KA_NONE = 0,
+ KA_QUIT,
+ KA_PAUSE,
+ KA_VOL_DOWN,
+ KA_VOL_UP,
+ KA_SEEK_BACK,
+ KA_SEEK_FWD,
+ KA_NEXT,
+ KA_PREV,
+ KA_REPEAT,
+ KA_SHUFFLE,
+ KA_SEARCH
+};
+
+static const char *progname;
+
+static void on_sigint(int);
+static uint32_t rd_u32le(const unsigned char *);
+static uint16_t rd_u16le(const unsigned char *);
+static struct wav_info parse_wav_header(int);
+static int set_raw_mode_fd(int, struct termios *);
+static void restore_term_fd(int, const struct termios *);
+static double now_sec(void);
+static void format_time(char *, size_t, double);
+static void apply_volume(int16_t *, size_t, int);
+static void format_status_line(char *, size_t, const char *,
+ const struct player_state *);
+static void print_status(const char *, const struct player_state *);
+static int open_pcm(snd_pcm_t **, const char *, unsigned int, unsigned int);
+static int seek_to(struct player_state *, int, double);
+static void usage(void);
+static int cmp_str(const void *, const void *);
+static int add_path(char ***, int *, int *, const char *);
+static int add_dir_files(char ***, int *, int *, const char *);
+static int collect_paths(char ***, int *, int, char **, int);
+static int reset_paths(char ***, int *, const char *);
+static int normalize_device_opt(int *, char ***);
+static void trim_line(char *);
+static void keylist_clear(struct keylist *);
+static void keylist_add(struct keylist *, const char *, size_t);
+static void keymap_defaults(struct keymap *);
+static int token_to_seq(const char *, char *, size_t *);
+static int parse_fkey(const char *);
+static void add_fkey(struct keylist *, int);
+static void parse_key_list(const char *, struct keylist *);
+static enum key_action match_keyseq(const struct keymap *, const char *, size_t);
+static bool keybuf_is_prefix(const struct keymap *, const char *, size_t);
+static bool keybuf_is_prefix_longer(const struct keymap *, const char *, size_t);
+static enum key_action keybuf_consume(const struct keymap *,
+ char *, size_t *);
+static enum key_action read_action(const struct keymap *, int,
+ char *, size_t *);
+static enum play_action handle_key(struct player_state *, enum key_action,
+ int *, bool);
+static int load_config(char *, size_t, unsigned int *, unsigned int *,
+ int *, bool *, bool *, struct keymap *);
+static enum play_action play_pcm(struct player_state *, int, int,
+ const char *, bool, bool);
+static void keymap_lists(const struct keymap *, const struct keylist ***,
+ size_t *);
+static void close_pcm(snd_pcm_t *);
+static void maybe_update_ui(const char *, const struct player_state *,
+ double *);
+static double bytes_per_sec(const struct player_state *);
+static double bytes_to_sec(off_t, const struct player_state *);
+enum search_result {
+ SEARCH_NONE = 0,
+ SEARCH_CANCEL,
+ SEARCH_SUBMIT
+};
+struct search_state {
+ bool active;
+ bool rendered;
+ bool dirty;
+ bool tab_active;
+ bool explicit_path;
+ size_t tab_len;
+ int tab_index;
+ size_t len;
+ char buf[PATH_MAX];
+ char tab_query[PATH_MAX];
+};
+static void search_start(struct search_state *);
+static void search_end(const char *, const struct player_state *,
+ struct search_state *, double *);
+static enum search_result search_handle_input(struct search_state *, int,
+ const char *, char *, size_t);
+static void search_render(const char *, const struct player_state *,
+ struct search_state *, double *);
+static int fuzzy_score(const char *, const char *);
+static int best_match_in_dir(const char *, const char *, char *, size_t,
+ bool *);
+static int nth_match_in_dir(const char *, const char *, int, char *, size_t,
+ bool *);
+static int resolve_search_path(const char *, const char *, char *, size_t);
+static void compute_base_dir(const char *, char *, size_t);
+static int term_cols(void);
+static void recompute_explicit_path(struct search_state *);
+static void get_search_display(const struct search_state *, char *, size_t);
+static bool build_full_path(const char *, const char *, char *, size_t);
+static int join_path(const char *, const char *, char *, size_t);
+#ifdef HAVE_FFMPEG
+static int64_t sec_to_ts(double, AVRational);
+static const char *format_tag_name(char *, size_t, const char *,
+ AVFormatContext *);
+static enum play_action play_ffmpeg(struct player_state *, const char *, int,
+ bool, const char *);
+static enum play_action ffmpeg_cleanup(enum play_action, struct player_state *,
+ SwrContext **, AVCodecContext **, AVFormatContext **,
+ AVPacket **, AVFrame **, uint8_t **);
+#endif
+static size_t sanitize_utf8_trunc(const char *, size_t, char *, size_t);
+static void save_config_volume(int);
+
+static void
+on_sigint(int sig)
+{
+ (void)sig;
+ g_stop = 1;
+ g_quit = 1;
+}
+
+static uint32_t
+rd_u32le(const unsigned char *p)
+{
+ return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
+ ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
+}
+
+static uint16_t
+rd_u16le(const unsigned char *p)
+{
+ return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
+}
+
+static struct wav_info
+parse_wav_header(int fd)
+{
+ struct wav_info info = {0};
+ unsigned char hdr[12];
+ unsigned char chunk_hdr[8];
+ bool fmt_ok = false;
+ bool data_ok = false;
+ off_t off;
+ ssize_t n;
+ uint32_t csize;
+
+ n = pread(fd, hdr, sizeof(hdr), 0);
+ if (n != (ssize_t)sizeof(hdr))
+ return info;
+ if (memcmp(hdr, "RIFF", 4) != 0 || memcmp(hdr + 8, "WAVE", 4) != 0)
+ return info;
+
+ off = 12;
+ for (;;) {
+ n = pread(fd, chunk_hdr, sizeof(chunk_hdr), off);
+ if (n != (ssize_t)sizeof(chunk_hdr))
+ break;
+ csize = rd_u32le(chunk_hdr + 4);
+ if (memcmp(chunk_hdr, "fmt ", 4) == 0) {
+ unsigned char fmt[16];
+ uint16_t audio_format;
+
+ if (csize < 16)
+ break;
+ n = pread(fd, fmt, sizeof(fmt), off + 8);
+ if (n != (ssize_t)sizeof(fmt))
+ break;
+ audio_format = rd_u16le(fmt + 0);
+ info.channels = rd_u16le(fmt + 2);
+ info.rate = rd_u32le(fmt + 4);
+ info.bits = rd_u16le(fmt + 14);
+ if (audio_format != 1)
+ break;
+ fmt_ok = true;
+ } else if (memcmp(chunk_hdr, "data", 4) == 0) {
+ info.data_offset = off + 8;
+ info.data_size = csize;
+ data_ok = true;
+ }
+ off += 8 + csize + (csize & 1);
+ if (fmt_ok && data_ok)
+ break;
+ }
+
+ info.ok = fmt_ok && data_ok;
+ return info;
+}
+
+static int
+set_raw_mode_fd(int fd, struct termios *orig)
+{
+ struct termios raw;
+ int flags;
+
+ if (fd < 0 || !isatty(fd))
+ return 0;
+ if (tcgetattr(fd, orig) != 0)
+ return -1;
+ raw = *orig;
+ raw.c_lflag &= (tcflag_t)~(ECHO | ICANON | ISIG);
+ raw.c_cc[VMIN] = 0;
+ raw.c_cc[VTIME] = 0;
+ if (tcsetattr(fd, TCSANOW, &raw) != 0)
+ return -1;
+ flags = fcntl(fd, F_GETFL, 0);
+ if (flags >= 0)
+ fcntl(fd, F_SETFL, flags | O_NONBLOCK);
+ return 0;
+}
+
+static void
+restore_term_fd(int fd, const struct termios *orig)
+{
+ if (fd >= 0 && isatty(fd))
+ tcsetattr(fd, TCSANOW, orig);
+}
+
+static double
+now_sec(void)
+{
+ struct timespec ts;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
+}
+
+static void
+format_time(char *buf, size_t len, double sec)
+{
+ int m;
+ int s;
+
+ if (sec < 0)
+ sec = 0;
+ m = (int)(sec / 60.0);
+ s = (int)(sec) % 60;
+ snprintf(buf, len, "%02d:%02d", m, s);
+}
+
+static void
+apply_volume(int16_t *samples, size_t count, int volume)
+{
+ int v;
+ size_t i;
+
+ if (volume >= 100)
+ return;
+ v = volume;
+ for (i = 0; i < count; ++i) {
+ int32_t x;
+
+ x = samples[i];
+ x = (x * v) / 100;
+ if (x > 32767)
+ x = 32767;
+ if (x < -32768)
+ x = -32768;
+ samples[i] = (int16_t)x;
+ }
+}
+
+static void
+format_status_line(char *line, size_t line_len, const char *name,
+ const struct player_state *st)
+{
+ char posbuf[16];
+ char durbuf[16];
+ char tail[128];
+ char namebuf[256];
+ const char *state;
+ const char *rep;
+ const char *shf;
+ const char *base;
+ const char *slash;
+ double pos;
+ int tlen;
+ int max_name;
+ size_t blen;
+
+ if (line == NULL || line_len == 0)
+ return;
+ line[0] = '\0';
+ namebuf[0] = '\0';
+
+ pos = 0.0;
+ if (st->rate && st->channels && st->bits) {
+ off_t bytes;
+ double bps;
+
+ bytes = st->data_pos - st->data_offset;
+ bps = (double)st->rate * (double)st->channels *
+ (double)(st->bits / 8);
+ if (bps > 0)
+ pos = (double)bytes / bps;
+ }
+ format_time(posbuf, sizeof(posbuf), pos);
+
+ if (st->data_size > 0) {
+ double bps;
+ double dur;
+
+ bps = (double)st->rate * (double)st->channels *
+ (double)(st->bits / 8);
+ dur = (bps > 0) ? (double)st->data_size / bps : 0.0;
+ format_time(durbuf, sizeof(durbuf), dur);
+ } else
+ snprintf(durbuf, sizeof(durbuf), "--:--");
+
+ state = st->paused ? "paused" : "playing";
+ rep = (st->repeat && *st->repeat) ? "[repeat] " : "";
+ shf = (st->shuffle && *st->shuffle) ? "[shuffle] " : "";
+ base = name ? name : "-";
+ slash = strrchr(base, '/');
+ if (slash && slash[1])
+ base = slash + 1;
+
+ tlen = snprintf(tail, sizeof(tail), " %s/%s vol %3d%% %s%s%s",
+ posbuf, durbuf, st->volume, shf, rep, state);
+ if (tlen < 0)
+ return;
+ if (tlen >= (int)sizeof(tail))
+ tlen = (int)sizeof(tail) - 1;
+
+ max_name = term_cols() - tlen;
+ if (max_name < 0)
+ max_name = 0;
+
+ blen = strlen(base);
+ if ((int)blen > max_name) {
+ size_t max_keep;
+
+ if (max_name <= 0) {
+ namebuf[0] = '\0';
+ } else if (max_name < 3) {
+ sanitize_utf8_trunc(base, (size_t)max_name,
+ namebuf, sizeof(namebuf));
+ } else {
+ max_keep = (size_t)max_name - 3;
+ sanitize_utf8_trunc(base, max_keep,
+ namebuf, sizeof(namebuf));
+ strncat(namebuf, "...",
+ sizeof(namebuf) - strlen(namebuf) - 1);
+ }
+ } else {
+ sanitize_utf8_trunc(base, blen, namebuf, sizeof(namebuf));
+ }
+ base = namebuf;
+
+ snprintf(line, line_len, "%s%s", base, tail);
+}
+
+static void
+print_status(const char *name, const struct player_state *st)
+{
+ char line[512];
+ format_status_line(line, sizeof(line), name, st);
+ fprintf(stderr, "\r%s\x1b[K", line);
+ fflush(stderr);
+}
+
+static int
+open_pcm(snd_pcm_t **pcm, const char *device, unsigned int rate,
+ unsigned int channels)
+{
+ const char *dev;
+ int rc;
+
+ dev = (device && device[0]) ? device : "default";
+ rc = snd_pcm_open(pcm, dev, SND_PCM_STREAM_PLAYBACK, 0);
+ if (rc < 0)
+ return rc;
+
+ rc = snd_pcm_set_params(*pcm, SND_PCM_FORMAT_S16_LE,
+ SND_PCM_ACCESS_RW_INTERLEAVED, channels, rate, 1, 500000);
+ return rc;
+}
+
+static void
+close_pcm(snd_pcm_t *pcm)
+{
+ if (pcm == NULL)
+ return;
+ if (g_quit)
+ snd_pcm_drop(pcm);
+ else
+ snd_pcm_drain(pcm);
+ snd_pcm_close(pcm);
+}
+
+static void
+maybe_update_ui(const char *name, const struct player_state *st, double *last_ui)
+{
+ double now;
+
+ if (last_ui == NULL)
+ return;
+ now = now_sec();
+ if (now - *last_ui > 0.1) {
+ print_status(name, st);
+ *last_ui = now;
+ }
+}
+
+static double
+bytes_per_sec(const struct player_state *st)
+{
+ if (st == NULL || st->rate == 0 || st->channels == 0 || st->bits == 0)
+ return 0.0;
+ return (double)st->rate * (double)st->channels *
+ (double)(st->bits / 8);
+}
+
+static double
+bytes_to_sec(off_t bytes, const struct player_state *st)
+{
+ double bps;
+
+ bps = bytes_per_sec(st);
+ if (bps <= 0.0)
+ return 0.0;
+ return (double)bytes / bps;
+}
+
+static int
+term_cols(void)
+{
+ return MAX_LINE;
+}
+
+static int
+join_path(const char *dir, const char *base, char *out, size_t out_sz)
+{
+ size_t dlen;
+
+ if (out == NULL || out_sz == 0 || dir == NULL || base == NULL)
+ return -1;
+ dlen = strlen(dir);
+ if (dlen == 0) {
+ if (snprintf(out, out_sz, "%s", base) >= (int)out_sz)
+ return -1;
+ return 0;
+ }
+ if (dir[dlen - 1] == '/')
+ return (snprintf(out, out_sz, "%s%s", dir, base) < (int)out_sz)
+ ? 0 : -1;
+ return (snprintf(out, out_sz, "%s/%s", dir, base) < (int)out_sz)
+ ? 0 : -1;
+}
+
+static bool
+build_full_path(const char *dir, const char *name, char *out, size_t out_sz)
+{
+ size_t dlen;
+ size_t nlen;
+ size_t pos;
+
+ if (out == NULL || out_sz == 0 || dir == NULL || name == NULL)
+ return false;
+ dlen = strlen(dir);
+ nlen = strlen(name);
+ pos = 0;
+
+ if (dlen == 0) {
+ if (nlen + 1 > out_sz)
+ return false;
+ memcpy(out, name, nlen);
+ out[nlen] = '\0';
+ return true;
+ }
+
+ if (dlen + nlen + 2 > out_sz)
+ return false;
+ memcpy(out, dir, dlen);
+ pos = dlen;
+ if (out[pos - 1] != '/')
+ out[pos++] = '/';
+ memcpy(out + pos, name, nlen);
+ out[pos + nlen] = '\0';
+ return true;
+}
+
+static void
+search_start(struct search_state *s)
+{
+ if (s == NULL)
+ return;
+ s->active = true;
+ s->rendered = false;
+ s->dirty = true;
+ s->tab_active = false;
+ s->explicit_path = false;
+ s->tab_len = 0;
+ s->tab_index = 0;
+ s->len = 0;
+ s->buf[0] = '\0';
+ s->tab_query[0] = '\0';
+}
+
+static void
+search_end(const char *name, const struct player_state *st,
+ struct search_state *s, double *last_ui)
+{
+ if (s == NULL || !s->active)
+ return;
+ if (s->rendered) {
+ fprintf(stderr, "\r\x1b[K\x1b[1A\r\x1b[K");
+ fflush(stderr);
+ }
+ s->active = false;
+ s->rendered = false;
+ s->dirty = false;
+ if (last_ui)
+ *last_ui = 0.0;
+ print_status(name, st);
+}
+
+static void
+recompute_explicit_path(struct search_state *s)
+{
+ if (s == NULL)
+ return;
+ if (s->len == 0) {
+ s->explicit_path = false;
+ return;
+ }
+ if (s->buf[0] == '~' || strchr(s->buf, '/') != NULL)
+ s->explicit_path = true;
+ else
+ s->explicit_path = false;
+}
+
+static void
+get_search_display(const struct search_state *s, char *out, size_t out_sz)
+{
+ const char *home;
+ const char *disp;
+ const char *slash;
+
+ if (out == NULL || out_sz == 0)
+ return;
+ out[0] = '\0';
+ if (s == NULL)
+ return;
+ if (!s->explicit_path) {
+ disp = s->buf;
+ slash = strrchr(disp, '/');
+ if (slash && slash[1])
+ disp = slash + 1;
+ snprintf(out, out_sz, "%s", disp);
+ return;
+ }
+ disp = s->buf;
+ home = getenv("HOME");
+ if (home && home[0]) {
+ size_t hlen = strlen(home);
+
+ if (strncmp(disp, home, hlen) == 0) {
+ if (disp[hlen] == '/' || disp[hlen] == '\0') {
+ snprintf(out, out_sz, "~%s", disp + hlen);
+ return;
+ }
+ }
+ }
+ snprintf(out, out_sz, "%s", disp);
+}
+
+static int
+fuzzy_score(const char *pattern, const char *text)
+{
+ size_t pi = 0;
+ size_t ti = 0;
+ size_t plen;
+ size_t tlen;
+ int score = 0;
+ int last = -1;
+
+ if (pattern == NULL || text == NULL)
+ return -1;
+ plen = strlen(pattern);
+ tlen = strlen(text);
+ if (plen == 0)
+ return 0;
+
+ for (pi = 0; pi < plen; ++pi) {
+ char pc = (char)tolower((unsigned char)pattern[pi]);
+ bool found = false;
+
+ while (ti < tlen) {
+ char tc = (char)tolower((unsigned char)text[ti]);
+
+ if (tc == pc) {
+ if (last < 0)
+ score += (int)(ti * 2);
+ else
+ score += (int)(ti - (size_t)last - 1);
+ last = (int)ti;
+ ti++;
+ found = true;
+ break;
+ }
+ ti++;
+ }
+ if (!found)
+ return -1;
+ }
+ if (last >= 0)
+ score += (int)(tlen - (size_t)last - 1);
+ return score;
+}
+
+static int
+best_match_in_dir(const char *dir, const char *pattern, char *out, size_t out_sz,
+ bool *out_is_dir)
+{
+ DIR *d;
+ struct dirent *ent;
+ int best_score = -1;
+ char best_name[NAME_MAX + 1];
+ bool best_is_dir = false;
+
+ if (out == NULL || out_sz == 0)
+ return -1;
+ out[0] = '\0';
+ if (out_is_dir)
+ *out_is_dir = false;
+
+ d = opendir(dir);
+ if (d == NULL)
+ return -1;
+
+ while ((ent = readdir(d)) != NULL) {
+ int score;
+ bool is_dir = false;
+
+ if (strcmp(ent->d_name, ".") == 0 ||
+ strcmp(ent->d_name, "..") == 0)
+ continue;
+ if (pattern == NULL || pattern[0] == '\0')
+ score = 0;
+ else
+ score = fuzzy_score(pattern, ent->d_name);
+ if (score < 0)
+ continue;
+ if ((pattern == NULL || pattern[0] != '.') &&
+ ent->d_name[0] == '.')
+ continue;
+
+#ifdef DT_DIR
+ if (ent->d_type == DT_DIR)
+ is_dir = true;
+ else if (ent->d_type == DT_UNKNOWN)
+#endif
+ {
+ char full[PATH_MAX];
+ struct stat st;
+
+ if (strlen(dir) + 1 + strlen(ent->d_name) >= sizeof(full))
+ continue;
+ if (!build_full_path(dir, ent->d_name, full,
+ sizeof(full)))
+ continue;
+ if (stat(full, &st) == 0 && S_ISDIR(st.st_mode))
+ is_dir = true;
+ }
+
+ if (best_score < 0 || score < best_score ||
+ (score == best_score &&
+ strcasecmp(ent->d_name, best_name) < 0)) {
+ best_score = score;
+ strncpy(best_name, ent->d_name, sizeof(best_name) - 1);
+ best_name[sizeof(best_name) - 1] = '\0';
+ best_is_dir = is_dir;
+ }
+ }
+ closedir(d);
+
+ if (best_score < 0)
+ return -1;
+ strncpy(out, best_name, out_sz - 1);
+ out[out_sz - 1] = '\0';
+ if (out_is_dir)
+ *out_is_dir = best_is_dir;
+ return 0;
+}
+
+static int
+nth_match_in_dir(const char *dir, const char *pattern, int index, char *out,
+ size_t out_sz, bool *out_is_dir)
+{
+ struct candidate {
+ char name[NAME_MAX + 1];
+ int score;
+ bool is_dir;
+ };
+ struct candidate *list = NULL;
+ size_t count = 0;
+ size_t cap = 0;
+ DIR *d;
+ struct dirent *ent;
+ int rc = -1;
+ int i;
+
+ if (out == NULL || out_sz == 0 || index < 0)
+ return -1;
+ out[0] = '\0';
+ if (out_is_dir)
+ *out_is_dir = false;
+
+ d = opendir(dir);
+ if (d == NULL)
+ return -1;
+
+ while ((ent = readdir(d)) != NULL) {
+ int score;
+ bool is_dir = false;
+
+ if (strcmp(ent->d_name, ".") == 0 ||
+ strcmp(ent->d_name, "..") == 0)
+ continue;
+ if ((pattern == NULL || pattern[0] != '.') &&
+ ent->d_name[0] == '.')
+ continue;
+
+ if (pattern == NULL || pattern[0] == '\0')
+ score = 0;
+ else
+ score = fuzzy_score(pattern, ent->d_name);
+ if (score < 0)
+ continue;
+
+#ifdef DT_DIR
+ if (ent->d_type == DT_DIR)
+ is_dir = true;
+ else if (ent->d_type == DT_UNKNOWN)
+#endif
+ {
+ char full[PATH_MAX];
+ struct stat st;
+
+ if (!build_full_path(dir, ent->d_name, full,
+ sizeof(full)))
+ continue;
+ if (stat(full, &st) == 0 && S_ISDIR(st.st_mode))
+ is_dir = true;
+ }
+
+ if (count >= cap) {
+ size_t ncap = (cap == 0) ? 16 : cap * 2;
+ struct candidate *nl =
+ realloc(list, ncap * sizeof(*list));
+ if (nl == NULL) {
+ free(list);
+ closedir(d);
+ return -1;
+ }
+ list = nl;
+ cap = ncap;
+ }
+ strncpy(list[count].name, ent->d_name,
+ sizeof(list[count].name) - 1);
+ list[count].name[sizeof(list[count].name) - 1] = '\0';
+ list[count].score = score;
+ list[count].is_dir = is_dir;
+ count++;
+ }
+ closedir(d);
+ if (count == 0) {
+ free(list);
+ return -1;
+ }
+
+ for (i = 0; i < (int)count - 1; ++i) {
+ int j;
+
+ for (j = i + 1; j < (int)count; ++j) {
+ struct candidate tmp;
+ bool swap = false;
+
+ if (list[j].score < list[i].score)
+ swap = true;
+ else if (list[j].score == list[i].score &&
+ strcasecmp(list[j].name, list[i].name) < 0)
+ swap = true;
+ if (!swap)
+ continue;
+ tmp = list[i];
+ list[i] = list[j];
+ list[j] = tmp;
+ }
+ }
+
+ index = index % (int)count;
+ if (index < 0)
+ index = 0;
+ strncpy(out, list[index].name, out_sz - 1);
+ out[out_sz - 1] = '\0';
+ if (out_is_dir)
+ *out_is_dir = list[index].is_dir;
+ rc = 0;
+ free(list);
+ return rc;
+}
+
+static int
+resolve_search_path(const char *query, const char *base_dir, char *out,
+ size_t out_sz)
+{
+ char expanded[PATH_MAX];
+ char dir[PATH_MAX];
+ char base[PATH_MAX];
+ char *slash;
+ struct stat st;
+ bool is_dir = false;
+ bool is_abs = false;
+ bool has_slash = false;
+
+ if (out == NULL || out_sz == 0)
+ return -1;
+ out[0] = '\0';
+ if (query == NULL || query[0] == '\0')
+ return -1;
+
+ if (strchr(query, '/') != NULL)
+ has_slash = true;
+ if (query[0] == '/' || query[0] == '~')
+ is_abs = true;
+
+ if (query[0] == '~') {
+ const char *home = getenv("HOME");
+
+ if (home && home[0]) {
+ if (query[1] == '/' || query[1] == '\0')
+ snprintf(expanded, sizeof(expanded), "%s%s",
+ home, query + 1);
+ else
+ snprintf(expanded, sizeof(expanded), "%s", query);
+ } else
+ snprintf(expanded, sizeof(expanded), "%s", query);
+ } else {
+ if (is_abs)
+ snprintf(expanded, sizeof(expanded), "%s", query);
+ else if ((has_slash || query[0] == '.') &&
+ base_dir != NULL && base_dir[0] != '\0') {
+ if (join_path(base_dir, query, expanded,
+ sizeof(expanded)) < 0)
+ snprintf(expanded, sizeof(expanded), "%s", query);
+ } else if (has_slash || base_dir == NULL || base_dir[0] == '\0') {
+ snprintf(expanded, sizeof(expanded), "%s", query);
+ }
+ else if (join_path(base_dir, query, expanded,
+ sizeof(expanded)) < 0)
+ snprintf(expanded, sizeof(expanded), "%s", query);
+ }
+
+ if (stat(expanded, &st) == 0) {
+ snprintf(out, out_sz, "%s", expanded);
+ return 0;
+ }
+
+ snprintf(dir, sizeof(dir), "%s", expanded);
+ slash = strrchr(dir, '/');
+ if (slash != NULL) {
+ *slash = '\0';
+ snprintf(base, sizeof(base), "%s", slash + 1);
+ if (dir[0] == '\0')
+ snprintf(dir, sizeof(dir), "/");
+ } else {
+ snprintf(dir, sizeof(dir), ".");
+ snprintf(base, sizeof(base), "%s", expanded);
+ }
+
+ (void)best_match_in_dir(dir, base, base, sizeof(base), &is_dir);
+ if (base[0] == '\0')
+ return -1;
+ if (join_path(dir, base, out, out_sz) < 0)
+ return -1;
+ return 0;
+}
+
+static enum search_result
+search_handle_input(struct search_state *s, int ctrl_fd,
+ const char *base_dir, char *out_path, size_t out_len)
+{
+ unsigned char buf[64];
+ ssize_t r;
+ ssize_t i;
+
+ if (s == NULL || !s->active)
+ return SEARCH_NONE;
+ r = (ctrl_fd >= 0) ? read(ctrl_fd, buf, sizeof(buf)) : -1;
+ if (r < 0) {
+ if (errno == EAGAIN || errno == EWOULDBLOCK)
+ return SEARCH_NONE;
+ return SEARCH_NONE;
+ }
+ for (i = 0; i < r; ++i) {
+ unsigned char c = buf[i];
+
+ if (c == 0x1b) {
+ s->dirty = true;
+ s->tab_active = false;
+ return SEARCH_CANCEL;
+ }
+ if (c == '\r' || c == '\n') {
+ if (out_path && out_len > 0 &&
+ resolve_search_path(s->buf, base_dir, out_path,
+ out_len) == 0) {
+ s->dirty = true;
+ s->tab_active = false;
+ return SEARCH_SUBMIT;
+ }
+ s->dirty = true;
+ s->tab_active = false;
+ return SEARCH_CANCEL;
+ }
+ if (c == '\t') {
+ char dir[PATH_MAX];
+ char base[PATH_MAX];
+ char match[PATH_MAX];
+ char rawdir[PATH_MAX];
+ char prefix[PATH_MAX];
+ char *slash;
+ bool is_dir = false;
+ int ok;
+
+ if (!s->tab_active) {
+ snprintf(s->tab_query, sizeof(s->tab_query),
+ "%s", s->buf);
+ s->tab_len = strlen(s->tab_query);
+ s->tab_index = 0;
+ s->tab_active = true;
+ } else {
+ s->tab_index++;
+ }
+
+ snprintf(rawdir, sizeof(rawdir), "%s", s->tab_query);
+ slash = strrchr(rawdir, '/');
+ if (slash != NULL) {
+ size_t plen = (size_t)(slash - rawdir + 1);
+
+ if (plen >= sizeof(prefix))
+ plen = sizeof(prefix) - 1;
+ memcpy(prefix, rawdir, plen);
+ prefix[plen] = '\0';
+
+ *slash = '\0';
+ snprintf(base, sizeof(base), "%s", slash + 1);
+ if (rawdir[0] == '\0')
+ snprintf(rawdir, sizeof(rawdir), "/");
+ } else {
+ prefix[0] = '\0';
+ snprintf(base, sizeof(base), "%s", s->tab_query);
+ }
+
+ if (rawdir[0] == '\0') {
+ if (base_dir && base_dir[0])
+ snprintf(dir, sizeof(dir), "%s", base_dir);
+ else
+ snprintf(dir, sizeof(dir), ".");
+ } else if (rawdir[0] == '~') {
+ const char *home = getenv("HOME");
+
+ if (home && home[0]) {
+ if (rawdir[1] == '/' || rawdir[1] == '\0')
+ snprintf(dir, sizeof(dir), "%s%s",
+ home, rawdir + 1);
+ else
+ snprintf(dir, sizeof(dir), "%s",
+ rawdir);
+ } else
+ snprintf(dir, sizeof(dir), "%s", rawdir);
+ } else if (rawdir[0] == '/') {
+ snprintf(dir, sizeof(dir), "%s", rawdir);
+ } else if (base_dir && base_dir[0]) {
+ if (join_path(base_dir, rawdir, dir,
+ sizeof(dir)) < 0)
+ snprintf(dir, sizeof(dir), "%s", rawdir);
+ } else {
+ snprintf(dir, sizeof(dir), "%s", rawdir);
+ }
+
+ ok = nth_match_in_dir(dir, base, s->tab_index, match,
+ sizeof(match), &is_dir);
+ if (ok == 0) {
+ if (snprintf(s->buf, sizeof(s->buf), "%s%s%s",
+ prefix, match, is_dir ? "/" : "") >=
+ (int)sizeof(s->buf)) {
+ s->buf[sizeof(s->buf) - 1] = '\0';
+ }
+ s->len = strlen(s->buf);
+ s->dirty = true;
+ recompute_explicit_path(s);
+ }
+ continue;
+ }
+ if (c == 0x7f || c == 0x08) {
+ if (s->len > 0) {
+ s->len--;
+ s->buf[s->len] = '\0';
+ s->dirty = true;
+ s->tab_active = false;
+ recompute_explicit_path(s);
+ }
+ continue;
+ }
+ if (c < 0x20 || c == 0x7f)
+ continue;
+ if (s->len + 1 < sizeof(s->buf)) {
+ s->buf[s->len++] = (char)c;
+ s->buf[s->len] = '\0';
+ s->dirty = true;
+ s->tab_active = false;
+ if (c == '/' || (c == '~' && s->len == 1))
+ s->explicit_path = true;
+ }
+ }
+ return SEARCH_NONE;
+}
+
+static void
+search_render(const char *name, const struct player_state *st,
+ struct search_state *s, double *last_ui)
+{
+ char line[512];
+ char disp[PATH_MAX];
+ char disp_trunc[PATH_MAX];
+ int cols;
+ int avail;
+ bool force = false;
+
+ if (s == NULL || !s->active)
+ return;
+ if (s->dirty)
+ force = true;
+ if (last_ui == NULL)
+ return;
+ if (!force && (now_sec() - *last_ui) <= 0.1)
+ return;
+
+ format_status_line(line, sizeof(line), name, st);
+ get_search_display(s, disp, sizeof(disp));
+ cols = term_cols();
+ avail = cols - (int)strlen("search: ");
+ if (avail < 0)
+ avail = 0;
+ sanitize_utf8_trunc(disp, (size_t)avail, disp_trunc,
+ sizeof(disp_trunc));
+ if (s->rendered)
+ fprintf(stderr, "\x1b[1A");
+ fprintf(stderr, "\r%s\x1b[K\nsearch: %s\x1b[K", line, disp_trunc);
+ fflush(stderr);
+
+ s->rendered = true;
+ s->dirty = false;
+ *last_ui = now_sec();
+}
+
+static void
+compute_base_dir(const char *path, char *out, size_t out_sz)
+{
+ char tmp[PATH_MAX];
+ char *slash;
+
+ if (out == NULL || out_sz == 0)
+ return;
+ if (path == NULL || path[0] == '\0') {
+ snprintf(out, out_sz, ".");
+ return;
+ }
+ snprintf(tmp, sizeof(tmp), "%s", path);
+ slash = strrchr(tmp, '/');
+ if (slash != NULL) {
+ *slash = '\0';
+ if (tmp[0] == '\0')
+ snprintf(out, out_sz, "/");
+ else
+ snprintf(out, out_sz, "%s", tmp);
+ } else {
+ snprintf(out, out_sz, ".");
+ }
+}
+static int
+seek_to(struct player_state *st, int fd, double sec)
+{
+ double bps;
+ off_t target;
+ off_t rel;
+
+ if (!st->seekable || st->data_size <= 0)
+ return -1;
+ bps = (double)st->rate * (double)st->channels *
+ (double)(st->bits / 8);
+ if (bps <= 0)
+ return -1;
+ target = st->data_offset + (off_t)(sec * bps);
+ if (target < st->data_offset)
+ target = st->data_offset;
+ if (target > st->data_offset + st->data_size)
+ target = st->data_offset + st->data_size;
+ if (st->frame_bytes > 0) {
+ rel = target - st->data_offset;
+ rel = (rel / (off_t)st->frame_bytes) * (off_t)st->frame_bytes;
+ target = st->data_offset + rel;
+ }
+ if (lseek(fd, target, SEEK_SET) < 0)
+ return -1;
+ st->data_pos = target;
+ snd_pcm_drop(st->pcm);
+ snd_pcm_prepare(st->pcm);
+ return 0;
+}
+
+static void
+usage(void)
+{
+ fprintf(stderr,
+ "usage: %s [-R] [-d device] [-r rate] [-c channels] [-s seconds] "
+ "[-v volume] [file|-]\n",
+ progname ? progname : "jam");
+ exit(1);
+}
+
+static int
+cmp_str(const void *a, const void *b)
+{
+ const char *const *sa = a;
+ const char *const *sb = b;
+
+ return strcmp(*sa, *sb);
+}
+
+static int
+add_path(char ***list, int *count, int *cap, const char *path)
+{
+ char **nl;
+ int ncap;
+
+ if (*count >= *cap) {
+ ncap = (*cap == 0) ? 16 : (*cap * 2);
+ nl = realloc(*list, (size_t)ncap * sizeof(char *));
+ if (nl == NULL)
+ return -1;
+ *list = nl;
+ *cap = ncap;
+ }
+ (*list)[*count] = strdup(path);
+ if ((*list)[*count] == NULL)
+ return -1;
+ (*count)++;
+ return 0;
+}
+
+static int
+add_dir_files(char ***list, int *count, int *cap, const char *dirpath)
+{
+ struct dirent *ent;
+ DIR *d;
+
+ d = opendir(dirpath);
+ if (d == NULL)
+ return -1;
+ while ((ent = readdir(d)) != NULL) {
+ char *full;
+ struct stat st;
+ size_t len;
+
+ if (strcmp(ent->d_name, ".") == 0 ||
+ strcmp(ent->d_name, "..") == 0)
+ continue;
+ len = strlen(dirpath) + strlen(ent->d_name) + 2;
+ full = malloc(len);
+ if (full == NULL) {
+ closedir(d);
+ return -1;
+ }
+ snprintf(full, len, "%s/%s", dirpath, ent->d_name);
+
+ if (stat(full, &st) == 0 && S_ISREG(st.st_mode)) {
+ if (add_path(list, count, cap, full) < 0) {
+ free(full);
+ closedir(d);
+ return -1;
+ }
+ }
+ free(full);
+ }
+ closedir(d);
+ return 0;
+}
+
+static int
+collect_paths(char ***out, int *out_count, int argc, char **argv, int start)
+{
+ char **list = NULL;
+ int count = 0;
+ int cap = 0;
+ int i;
+ bool saw_stdin = false;
+
+ for (i = start; i < argc; ++i) {
+ const char *p = argv[i];
+ struct stat st;
+
+ if (strcmp(p, "-") == 0) {
+ saw_stdin = true;
+ if (add_path(&list, &count, &cap, p) < 0)
+ goto fail;
+ continue;
+ }
+ if (stat(p, &st) == 0 && S_ISDIR(st.st_mode)) {
+ if (add_dir_files(&list, &count, &cap, p) < 0)
+ goto fail;
+ } else {
+ if (add_path(&list, &count, &cap, p) < 0)
+ goto fail;
+ }
+ }
+ if (count > 1 && !saw_stdin)
+ qsort(list, (size_t)count, sizeof(char *), cmp_str);
+ *out = list;
+ *out_count = count;
+ return 0;
+fail:
+ if (list != NULL) {
+ for (i = 0; i < count; ++i)
+ free(list[i]);
+ free(list);
+ }
+ return -1;
+}
+
+static int
+reset_paths(char ***paths, int *path_count, const char *path)
+{
+ char **list = NULL;
+ int count = 0;
+ char *argv[1];
+ int i;
+
+ if (paths == NULL || path_count == NULL || path == NULL)
+ return -1;
+
+ argv[0] = (char *)path;
+ if (collect_paths(&list, &count, 1, argv, 0) < 0)
+ return -1;
+
+ if (*paths != NULL) {
+ for (i = 0; i < *path_count; ++i)
+ free((*paths)[i]);
+ free(*paths);
+ }
+ *paths = list;
+ *path_count = count;
+ return 0;
+}
+
+static int
+normalize_device_opt(int *argc, char ***argv)
+{
+ int i;
+
+ for (i = 1; i < *argc; ++i) {
+ if (strcmp((*argv)[i], "--device") == 0)
+ (*argv)[i] = (char *)"-d";
+ }
+ return 0;
+}
+
+static void
+trim_line(char *s)
+{
+ size_t len;
+ char *p;
+
+ len = strlen(s);
+ while (len > 0 && (s[len - 1] == '\n' || s[len - 1] == '\r' ||
+ s[len - 1] == ' ' || s[len - 1] == '\t'))
+ s[--len] = '\0';
+ p = s;
+ while (*p == ' ' || *p == '\t')
+ p++;
+ if (p != s)
+ memmove(s, p, strlen(p) + 1);
+}
+
+static void
+keylist_clear(struct keylist *kl)
+{
+ if (kl == NULL)
+ return;
+ kl->count = 0;
+}
+
+static void
+keylist_add(struct keylist *kl, const char *seq, size_t len)
+{
+ if (kl == NULL || seq == NULL || len == 0)
+ return;
+ if (kl->count >= (int)(sizeof(kl->seqs) / sizeof(kl->seqs[0])))
+ return;
+ if (len >= sizeof(kl->seqs[kl->count].seq))
+ len = sizeof(kl->seqs[kl->count].seq) - 1;
+ memcpy(kl->seqs[kl->count].seq, seq, len);
+ kl->seqs[kl->count].seq[len] = '\0';
+ kl->seqs[kl->count].len = len;
+ kl->count++;
+}
+
+static void
+keymap_defaults(struct keymap *km)
+{
+ if (km == NULL)
+ return;
+ keylist_clear(&km->quit);
+ keylist_clear(&km->pause);
+ keylist_clear(&km->vol_down);
+ keylist_clear(&km->vol_up);
+ keylist_clear(&km->seek_back);
+ keylist_clear(&km->seek_fwd);
+ keylist_clear(&km->next);
+ keylist_clear(&km->prev);
+ keylist_clear(&km->repeat);
+ keylist_clear(&km->shuffle);
+ keylist_clear(&km->search);
+
+ keylist_add(&km->quit, "q", 1);
+ keylist_add(&km->pause, " ", 1);
+ keylist_add(&km->vol_down, "j", 1);
+ keylist_add(&km->vol_up, "k", 1);
+ keylist_add(&km->seek_back, "h", 1);
+ keylist_add(&km->seek_fwd, "l", 1);
+ keylist_add(&km->next, "n", 1);
+ keylist_add(&km->prev, "p", 1);
+ keylist_add(&km->repeat, "r", 1);
+ keylist_add(&km->shuffle, "s", 1);
+ keylist_add(&km->search, "/", 1);
+}
+
+static void
+keymap_lists(const struct keymap *km, const struct keylist ***out, size_t *count)
+{
+ static const struct keylist *lists[11];
+
+ if (out == NULL || count == NULL) {
+ return;
+ }
+ if (km == NULL) {
+ *out = NULL;
+ *count = 0;
+ return;
+ }
+ lists[0] = &km->quit;
+ lists[1] = &km->pause;
+ lists[2] = &km->vol_down;
+ lists[3] = &km->vol_up;
+ lists[4] = &km->seek_back;
+ lists[5] = &km->seek_fwd;
+ lists[6] = &km->next;
+ lists[7] = &km->prev;
+ lists[8] = &km->repeat;
+ lists[9] = &km->shuffle;
+ lists[10] = &km->search;
+
+ *out = lists;
+ *count = sizeof(lists) / sizeof(lists[0]);
+}
+
+static const struct {
+ const char *name;
+ const char *seq;
+ size_t len;
+} keynames[] = {
+ { "space", " ", 1 },
+ { "esc", "\x1b", 1 },
+ { "escape", "\x1b", 1 },
+ { "enter", "\n", 1 },
+ { "return", "\n", 1 },
+ { "tab", "\t", 1 },
+ { "backspace", "\x7f", 1 },
+ { "del", "\x1b[3~", 4 },
+ { "delete", "\x1b[3~", 4 },
+ { "ins", "\x1b[2~", 4 },
+ { "insert", "\x1b[2~", 4 },
+ { "home", "\x1b[H", 3 },
+ { "end", "\x1b[F", 3 },
+ { "pgup", "\x1b[5~", 4 },
+ { "pageup", "\x1b[5~", 4 },
+ { "pgdn", "\x1b[6~", 4 },
+ { "pagedown", "\x1b[6~", 4 },
+ { "up", "\x1b[A", 3 },
+ { "down", "\x1b[B", 3 },
+ { "right", "\x1b[C", 3 },
+ { "left", "\x1b[D", 3 }
+};
+
+static int
+token_to_seq(const char *tok, char *out, size_t *out_len)
+{
+ char low[32];
+ size_t n;
+ size_t i;
+ char c;
+
+ if (tok == NULL || out == NULL || out_len == NULL)
+ return -1;
+ if (tok[0] == '\0')
+ return -1;
+ if (strlen(tok) == 1) {
+ out[0] = tok[0];
+ *out_len = 1;
+ return 0;
+ }
+ n = strlen(tok);
+ if (n >= sizeof(low))
+ n = sizeof(low) - 1;
+ for (i = 0; i < n; ++i)
+ low[i] = (char)tolower((unsigned char)tok[i]);
+ low[n] = '\0';
+
+ if (strncmp(low, "alt+", 4) == 0 && tok[4] && !tok[5]) {
+ c = tok[4];
+ out[0] = 0x1b;
+ out[1] = c;
+ *out_len = 2;
+ return 0;
+ }
+
+ for (i = 0; i < sizeof(keynames) / sizeof(keynames[0]); ++i) {
+ if (strcmp(low, keynames[i].name) == 0) {
+ memcpy(out, keynames[i].seq, keynames[i].len);
+ *out_len = keynames[i].len;
+ return 0;
+ }
+ }
+ if (strncmp(low, "ctrl+", 5) == 0 && low[5] && !low[6]) {
+ c = tok[5];
+ if (c >= 'A' && c <= 'Z')
+ c = (char)(c - 'A' + 'a');
+ if (c >= 'a' && c <= 'z') {
+ out[0] = (char)(c - 'a' + 1);
+ *out_len = 1;
+ return 0;
+ }
+ }
+ return -1;
+}
+
+static int
+parse_fkey(const char *tok)
+{
+ char low[16];
+ size_t n;
+ size_t i;
+ char *end;
+ long v;
+
+ if (tok == NULL)
+ return -1;
+ n = strlen(tok);
+ if (n < 2)
+ return -1;
+ if (n >= sizeof(low))
+ n = sizeof(low) - 1;
+ for (i = 0; i < n; ++i)
+ low[i] = (char)tolower((unsigned char)tok[i]);
+ low[n] = '\0';
+ if (low[0] != 'f')
+ return -1;
+ v = strtol(low + 1, &end, 10);
+ if (*end != '\0')
+ return -1;
+ if (v < 1 || v > 24)
+ return -1;
+ return (int)v;
+}
+
+static void
+add_fkey(struct keylist *kl, int fn)
+{
+ static const struct {
+ const char *seq;
+ size_t len;
+ } ftab[][2] = {
+ { { "\x1bOP", 3 }, { "\x1b[11~", 5 } }, /* F1 */
+ { { "\x1bOQ", 3 }, { "\x1b[12~", 5 } }, /* F2 */
+ { { "\x1bOR", 3 }, { "\x1b[13~", 5 } }, /* F3 */
+ { { "\x1bOS", 3 }, { "\x1b[14~", 5 } }, /* F4 */
+ { { "\x1b[15~", 5 }, { NULL, 0 } }, /* F5 */
+ { { "\x1b[17~", 5 }, { NULL, 0 } }, /* F6 */
+ { { "\x1b[18~", 5 }, { NULL, 0 } }, /* F7 */
+ { { "\x1b[19~", 5 }, { NULL, 0 } }, /* F8 */
+ { { "\x1b[20~", 5 }, { NULL, 0 } }, /* F9 */
+ { { "\x1b[21~", 5 }, { NULL, 0 } }, /* F10 */
+ { { "\x1b[23~", 5 }, { NULL, 0 } }, /* F11 */
+ { { "\x1b[24~", 5 }, { NULL, 0 } }, /* F12 */
+ { { "\x1b[25~", 5 }, { NULL, 0 } }, /* F13 */
+ { { "\x1b[26~", 5 }, { NULL, 0 } }, /* F14 */
+ { { "\x1b[28~", 5 }, { NULL, 0 } }, /* F15 */
+ { { "\x1b[29~", 5 }, { NULL, 0 } }, /* F16 */
+ { { "\x1b[31~", 5 }, { NULL, 0 } }, /* F17 */
+ { { "\x1b[32~", 5 }, { NULL, 0 } }, /* F18 */
+ { { "\x1b[33~", 5 }, { NULL, 0 } }, /* F19 */
+ { { "\x1b[34~", 5 }, { NULL, 0 } }, /* F20 */
+ { { "\x1b[42~", 5 }, { NULL, 0 } }, /* F21 */
+ { { "\x1b[43~", 5 }, { NULL, 0 } }, /* F22 */
+ { { "\x1b[44~", 5 }, { NULL, 0 } }, /* F23 */
+ { { "\x1b[45~", 5 }, { NULL, 0 } } /* F24 */
+ };
+ int idx;
+
+ if (kl == NULL)
+ return;
+ if (fn < 1 || fn > 24)
+ return;
+ idx = fn - 1;
+ keylist_add(kl, ftab[idx][0].seq, ftab[idx][0].len);
+ if (ftab[idx][1].seq != NULL)
+ keylist_add(kl, ftab[idx][1].seq, ftab[idx][1].len);
+}
+
+static void
+parse_key_list(const char *val, struct keylist *kl)
+{
+ const char *p;
+
+ if (val == NULL || kl == NULL)
+ return;
+ keylist_clear(kl);
+
+ p = val;
+ while (*p) {
+ char tok[64];
+ size_t tlen = 0;
+
+ while (*p == ' ' || *p == '\t' || *p == ',')
+ p++;
+ if (*p == '\0')
+ break;
+ if (*p == '"') {
+ p++;
+ while (*p && *p != '"' && tlen + 1 < sizeof(tok))
+ tok[tlen++] = *p++;
+ if (*p == '"')
+ p++;
+ } else {
+ while (*p && *p != ',' && *p != ' ' && *p != '\t' &&
+ tlen + 1 < sizeof(tok))
+ tok[tlen++] = *p++;
+ }
+ tok[tlen] = '\0';
+ if (tlen > 0) {
+ char seq[16];
+ size_t slen = 0;
+ int fn;
+
+ fn = parse_fkey(tok);
+ if (fn > 0) {
+ add_fkey(kl, fn);
+ } else if (token_to_seq(tok, seq, &slen) == 0) {
+ keylist_add(kl, seq, slen);
+ }
+ }
+ }
+}
+
+static enum key_action
+match_keyseq(const struct keymap *km, const char *buf, size_t len)
+{
+ const struct keylist **lists = NULL;
+ size_t list_count = 0;
+ const enum key_action actions[] = {
+ KA_QUIT, KA_PAUSE, KA_VOL_DOWN, KA_VOL_UP, KA_SEEK_BACK,
+ KA_SEEK_FWD, KA_NEXT, KA_PREV, KA_REPEAT, KA_SHUFFLE,
+ KA_SEARCH
+ };
+ size_t i;
+ int j;
+
+ if (km == NULL || buf == NULL || len == 0)
+ return KA_NONE;
+ keymap_lists(km, &lists, &list_count);
+ if (list_count != sizeof(actions) / sizeof(actions[0]))
+ return KA_NONE;
+ for (i = 0; i < list_count; ++i) {
+ const struct keylist *kl = lists[i];
+
+ for (j = 0; j < kl->count; ++j) {
+ if (kl->seqs[j].len == len &&
+ memcmp(kl->seqs[j].seq, buf, len) == 0)
+ return actions[i];
+ }
+ }
+ return KA_NONE;
+}
+
+static bool
+keybuf_is_prefix(const struct keymap *km, const char *buf, size_t len)
+{
+ const struct keylist **lists = NULL;
+ size_t list_count = 0;
+ size_t i;
+ int j;
+
+ if (km == NULL || buf == NULL || len == 0)
+ return false;
+ keymap_lists(km, &lists, &list_count);
+ for (i = 0; i < list_count; ++i) {
+ const struct keylist *kl = lists[i];
+
+ for (j = 0; j < kl->count; ++j) {
+ if (kl->seqs[j].len >= len &&
+ memcmp(kl->seqs[j].seq, buf, len) == 0)
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool
+keybuf_is_prefix_longer(const struct keymap *km, const char *buf, size_t len)
+{
+ const struct keylist **lists = NULL;
+ size_t list_count = 0;
+ size_t i;
+ int j;
+
+ if (km == NULL || buf == NULL || len == 0)
+ return false;
+ keymap_lists(km, &lists, &list_count);
+ for (i = 0; i < list_count; ++i) {
+ const struct keylist *kl = lists[i];
+
+ for (j = 0; j < kl->count; ++j) {
+ if (kl->seqs[j].len > len &&
+ memcmp(kl->seqs[j].seq, buf, len) == 0)
+ return true;
+ }
+ }
+ return false;
+}
+
+static enum key_action
+keybuf_consume(const struct keymap *km, char *buf, size_t *len)
+{
+ enum key_action act;
+
+ while (*len > 0) {
+ act = match_keyseq(km, buf, *len);
+ if (act != KA_NONE) {
+ if (keybuf_is_prefix_longer(km, buf, *len))
+ return KA_NONE;
+ *len = 0;
+ return act;
+ }
+ if (keybuf_is_prefix(km, buf, *len))
+ return KA_NONE;
+ memmove(buf, buf + 1, *len - 1);
+ (*len)--;
+ }
+ return KA_NONE;
+}
+
+static enum key_action
+read_action(const struct keymap *km, int ctrl_fd, char *buf, size_t *len)
+{
+ char tmp[16];
+ ssize_t r;
+ ssize_t i;
+ enum key_action act;
+ static double last_input = 0.0;
+ double now;
+
+ now = now_sec();
+ r = (ctrl_fd >= 0) ? read(ctrl_fd, tmp, sizeof(tmp)) : -1;
+ if (r > 0) {
+ last_input = now;
+ for (i = 0; i < r; ++i) {
+ if (*len < 16) {
+ buf[*len] = tmp[i];
+ (*len)++;
+ } else {
+ memmove(buf, buf + 1, 15);
+ buf[15] = tmp[i];
+ }
+ act = keybuf_consume(km, buf, len);
+ if (act != KA_NONE)
+ return act;
+ }
+ } else if (r < 0 && errno != EAGAIN && errno != EWOULDBLOCK)
+ return KA_NONE;
+
+ if (*len > 0 && (now - last_input) > 0.05) {
+ act = match_keyseq(km, buf, *len);
+ if (act != KA_NONE) {
+ *len = 0;
+ return act;
+ }
+ *len = 0;
+ }
+ return keybuf_consume(km, buf, len);
+}
+
+static enum play_action
+handle_key(struct player_state *st, enum key_action ka, int *seek_dir,
+ bool allow_nextprev)
+{
+ if (ka == KA_NONE)
+ return PLAY_DONE;
+
+ switch (ka) {
+ case KA_QUIT:
+ g_quit = 1;
+ return PLAY_QUIT;
+ case KA_PAUSE:
+ st->paused = !st->paused;
+ if (snd_pcm_pause(st->pcm, st->paused ? 1 : 0) < 0) {
+ snd_pcm_drop(st->pcm);
+ snd_pcm_prepare(st->pcm);
+ }
+ break;
+ case KA_VOL_DOWN:
+ st->volume -= 5;
+ if (st->volume < 0)
+ st->volume = 0;
+ break;
+ case KA_VOL_UP:
+ st->volume += 5;
+ if (st->volume > 100)
+ st->volume = 100;
+ break;
+ case KA_SEEK_BACK:
+ if (seek_dir)
+ *seek_dir = -1;
+ break;
+ case KA_SEEK_FWD:
+ if (seek_dir)
+ *seek_dir = 1;
+ break;
+ case KA_NEXT:
+ if (allow_nextprev)
+ return PLAY_NEXT;
+ break;
+ case KA_PREV:
+ if (allow_nextprev)
+ return PLAY_PREV;
+ break;
+ case KA_REPEAT:
+ if (st->repeat)
+ *st->repeat = !*st->repeat;
+ break;
+ case KA_SHUFFLE:
+ if (st->shuffle)
+ *st->shuffle = !*st->shuffle;
+ break;
+ case KA_SEARCH:
+ break;
+ default:
+ break;
+ }
+ return PLAY_DONE;
+}
+
+static int
+load_config(char *device, size_t device_len, unsigned int *rate,
+ unsigned int *channels, int *volume, bool *repeat, bool *shuffle,
+ struct keymap *keys)
+{
+ FILE *f;
+ char line[512];
+ char path[512];
+ const char *home;
+
+ home = getenv("HOME");
+ if (home == NULL || home[0] == '\0')
+ return 0;
+ snprintf(path, sizeof(path), "%s/.jamrc", home);
+ f = fopen(path, "r");
+ if (f == NULL)
+ return 0;
+
+ while (fgets(line, sizeof(line), f)) {
+ char *eq;
+ char *key;
+ char *val;
+ unsigned int v;
+ int iv;
+
+ trim_line(line);
+ if (line[0] == '\0' || line[0] == '#')
+ continue;
+ eq = strchr(line, '=');
+ if (eq == NULL)
+ continue;
+ *eq = '\0';
+ key = line;
+ val = eq + 1;
+ trim_line(key);
+ trim_line(val);
+
+ if (strcmp(key, "rate") == 0) {
+ v = (unsigned int)atoi(val);
+ if (v > 0)
+ *rate = v;
+ } else if (strcmp(key, "channels") == 0) {
+ v = (unsigned int)atoi(val);
+ if (v > 0)
+ *channels = v;
+ } else if (strcmp(key, "volume") == 0) {
+ iv = atoi(val);
+ if (iv < 0)
+ iv = 0;
+ if (iv > 100)
+ iv = 100;
+ *volume = iv;
+ } else if (strcmp(key, "device") == 0) {
+ if (device != NULL && device_len > 0) {
+ strncpy(device, val, device_len - 1);
+ device[device_len - 1] = '\0';
+ }
+ } else if (strcmp(key, "repeat") == 0) {
+ *repeat = (strcmp(val, "1") == 0 ||
+ strcmp(val, "true") == 0 ||
+ strcmp(val, "yes") == 0);
+ } else if (strcmp(key, "shuffle") == 0) {
+ *shuffle = (strcmp(val, "1") == 0 ||
+ strcmp(val, "true") == 0 ||
+ strcmp(val, "yes") == 0);
+ } else if (keys != NULL) {
+ if (strcmp(key, "key_quit") == 0)
+ parse_key_list(val, &keys->quit);
+ else if (strcmp(key, "key_pause") == 0)
+ parse_key_list(val, &keys->pause);
+ else if (strcmp(key, "key_vol_down") == 0)
+ parse_key_list(val, &keys->vol_down);
+ else if (strcmp(key, "key_vol_up") == 0)
+ parse_key_list(val, &keys->vol_up);
+ else if (strcmp(key, "key_seek_back") == 0)
+ parse_key_list(val, &keys->seek_back);
+ else if (strcmp(key, "key_seek_fwd") == 0)
+ parse_key_list(val, &keys->seek_fwd);
+ else if (strcmp(key, "key_next") == 0)
+ parse_key_list(val, &keys->next);
+ else if (strcmp(key, "key_prev") == 0)
+ parse_key_list(val, &keys->prev);
+ else if (strcmp(key, "key_repeat") == 0)
+ parse_key_list(val, &keys->repeat);
+ else if (strcmp(key, "key_shuffle") == 0)
+ parse_key_list(val, &keys->shuffle);
+ else if (strcmp(key, "key_search") == 0)
+ parse_key_list(val, &keys->search);
+ }
+ }
+ fclose(f);
+ return 0;
+}
+
+static void
+save_config_volume(int volume)
+{
+ FILE *in;
+ FILE *out;
+ char line[512];
+ char path[512];
+ char tmp[512];
+ const char *home;
+ bool wrote = false;
+ struct stat st;
+ int fd;
+
+ home = getenv("HOME");
+ if (home == NULL || home[0] == '\0')
+ return;
+ snprintf(path, sizeof(path), "%s/.jamrc", home);
+
+ in = fopen(path, "r");
+ if (in == NULL) {
+ out = fopen(path, "w");
+ if (out == NULL)
+ return;
+ fprintf(out, "volume=%d\n", volume);
+ fclose(out);
+ return;
+ }
+
+ if (stat(path, &st) != 0)
+ st.st_mode = 0600;
+
+ snprintf(tmp, sizeof(tmp), "%s/.jamrc.tmpXXXXXX", home);
+ fd = mkstemp(tmp);
+ if (fd < 0) {
+ fclose(in);
+ return;
+ }
+ (void)fchmod(fd, st.st_mode & 0777);
+ out = fdopen(fd, "w");
+ if (out == NULL) {
+ close(fd);
+ unlink(tmp);
+ fclose(in);
+ return;
+ }
+
+ while (fgets(line, sizeof(line), in)) {
+ char work[512];
+ char *eq;
+ char *key;
+ char *val;
+
+ strncpy(work, line, sizeof(work) - 1);
+ work[sizeof(work) - 1] = '\0';
+ trim_line(work);
+ if (work[0] == '\0' || work[0] == '#') {
+ fputs(line, out);
+ continue;
+ }
+ eq = strchr(work, '=');
+ if (eq == NULL) {
+ fputs(line, out);
+ continue;
+ }
+ *eq = '\0';
+ key = work;
+ val = eq + 1;
+ trim_line(key);
+ trim_line(val);
+
+ if (strcmp(key, "volume") == 0) {
+ fprintf(out, "volume=%d\n", volume);
+ wrote = true;
+ continue;
+ }
+ fputs(line, out);
+ }
+
+ if (!wrote)
+ fprintf(out, "volume=%d\n", volume);
+
+ fclose(in);
+ fflush(out);
+ fclose(out);
+ (void)rename(tmp, path);
+}
+
+static enum play_action
+play_pcm(struct player_state *st, int fd, int ctrl_fd, const char *name,
+ bool allow_seek, bool allow_nextprev)
+{
+ uint8_t *buf;
+ enum key_action ka;
+ enum play_action act;
+ size_t buf_bytes;
+ size_t buf_frames;
+ size_t frames;
+ size_t offset;
+ size_t keylen = 0;
+ char keybuf[16];
+ size_t out_frames;
+ ssize_t nread;
+ snd_pcm_sframes_t wrote;
+ double last_ui;
+ int seek_dir;
+ struct search_state search = {0};
+ char base_dir[PATH_MAX];
+
+ buf_frames = BUFFER_FRAMES;
+ buf_bytes = buf_frames * st->frame_bytes;
+ buf = malloc(buf_bytes);
+ if (buf == NULL)
+ return PLAY_ERR;
+
+ last_ui = 0.0;
+ act = PLAY_DONE;
+
+ compute_base_dir(name, base_dir, sizeof(base_dir));
+ while (!g_stop) {
+ seek_dir = 0;
+ if (search.active) {
+ enum search_result sr =
+ search_handle_input(&search, ctrl_fd, base_dir,
+ g_search_path, sizeof(g_search_path));
+ if (sr == SEARCH_SUBMIT) {
+ search_end(name, st, &search, &last_ui);
+ act = PLAY_SEARCH;
+ goto out;
+ }
+ if (sr == SEARCH_CANCEL)
+ search_end(name, st, &search, &last_ui);
+ } else {
+ ka = read_action(st->keys, ctrl_fd, keybuf, &keylen);
+ if (ka == KA_SEARCH && ctrl_fd >= 0) {
+ search_start(&search);
+ } else {
+ act = handle_key(st, ka, &seek_dir,
+ allow_nextprev);
+ if (act == PLAY_QUIT || act == PLAY_NEXT ||
+ act == PLAY_PREV)
+ goto out;
+ }
+ }
+ if (seek_dir != 0 && allow_seek) {
+ double pos = bytes_to_sec(st->data_pos - st->data_offset,
+ st);
+
+ seek_to(st, fd, pos +
+ (seek_dir < 0 ? -SEEK_SECONDS : SEEK_SECONDS));
+ }
+
+ if (st->paused) {
+ struct timespec ts = {0, 100000000};
+
+ nanosleep(&ts, NULL);
+ } else {
+ nread = read(fd, buf, buf_bytes);
+ if (nread == 0) {
+ act = PLAY_DONE;
+ goto out;
+ }
+ if (nread < 0) {
+ if (errno == EINTR)
+ continue;
+ act = PLAY_ERR;
+ goto out;
+ }
+ st->data_pos += nread;
+
+ frames = (size_t)nread / st->frame_bytes;
+ apply_volume((int16_t *)buf, frames * st->channels,
+ st->volume);
+
+ offset = 0;
+ out_frames = frames;
+ while (out_frames > 0) {
+ wrote = snd_pcm_writei(st->pcm, buf + offset,
+ out_frames);
+ if (wrote < 0) {
+ if (wrote == -EPIPE) {
+ snd_pcm_prepare(st->pcm);
+ continue;
+ }
+ act = PLAY_ERR;
+ goto out;
+ }
+ offset += (size_t)wrote * st->frame_bytes;
+ out_frames -= (size_t)wrote;
+ }
+ }
+
+ if (search.active)
+ search_render(name, st, &search, &last_ui);
+ else
+ maybe_update_ui(name, st, &last_ui);
+
+ if (st->data_size > 0 &&
+ st->data_pos >= st->data_offset + st->data_size) {
+ act = PLAY_DONE;
+ goto out;
+ }
+ }
+
+out:
+ free(buf);
+ return act;
+}
+
+#ifdef HAVE_FFMPEG
+static int64_t
+sec_to_ts(double sec, AVRational tb)
+{
+ return (int64_t)(sec / av_q2d(tb));
+}
+
+static const char *
+format_tag_name(char *out, size_t len, const char *path, AVFormatContext *fmt)
+{
+ const char *base;
+ const char *slash;
+ AVDictionaryEntry *title;
+ AVDictionaryEntry *artist;
+
+ if (out == NULL || len == 0)
+ return path;
+ out[0] = '\0';
+ base = path ? path : "-";
+ slash = strrchr(base, '/');
+ if (slash && slash[1])
+ base = slash + 1;
+
+ title = av_dict_get(fmt->metadata, "title", NULL, 0);
+ artist = av_dict_get(fmt->metadata, "artist", NULL, 0);
+ if (title && title->value && title->value[0]) {
+ if (artist && artist->value && artist->value[0])
+ snprintf(out, len, "%s - %s", artist->value,
+ title->value);
+ else
+ snprintf(out, len, "%s", title->value);
+ return out;
+ }
+ snprintf(out, len, "%s", base);
+ return out;
+}
+
+static enum play_action
+ffmpeg_cleanup(enum play_action act, struct player_state *st, SwrContext **swr,
+ AVCodecContext **dec, AVFormatContext **fmt, AVPacket **pkt,
+ AVFrame **frm, uint8_t **outbuf)
+{
+ if (pkt)
+ av_packet_free(pkt);
+ if (frm)
+ av_frame_free(frm);
+ if (outbuf)
+ av_freep(outbuf);
+ if (st && st->pcm)
+ close_pcm(st->pcm);
+ if (swr && *swr)
+ swr_free(swr);
+ if (dec && *dec)
+ avcodec_free_context(dec);
+ if (fmt && *fmt)
+ avformat_close_input(fmt);
+ return act;
+}
+
+static enum play_action
+play_ffmpeg(struct player_state *st, const char *path, int ctrl_fd,
+ bool allow_nextprev, const char *device)
+{
+ AVFormatContext *fmt = NULL;
+ AVCodecContext *dec = NULL;
+ AVPacket *pkt = NULL;
+ AVFrame *frm = NULL;
+ SwrContext *swr = NULL;
+ AVStream *stm;
+ const AVCodec *codec;
+ char namebuf[256];
+ const char *disp;
+ enum key_action ka;
+ enum play_action act;
+ char keybuf[16];
+ size_t keylen = 0;
+ double last_ui;
+ uint8_t *outbuf = NULL;
+ int outbuf_size = 0;
+ int stream;
+ int out_rate;
+ int out_ch;
+ int ret;
+ double duration_sec;
+ int seek_dir;
+#if LIBAVUTIL_VERSION_MAJOR >= 57
+ AVChannelLayout out_layout;
+#else
+ int64_t out_layout;
+#endif
+ struct search_state search = {0};
+ char base_dir[PATH_MAX];
+
+ st->pcm = NULL;
+ av_log_set_level(AV_LOG_QUIET);
+
+ if (avformat_open_input(&fmt, path, NULL, NULL) < 0)
+ return ffmpeg_cleanup(PLAY_ERR, st, &swr, &dec, &fmt,
+ &pkt, &frm, &outbuf);
+ if (avformat_find_stream_info(fmt, NULL) < 0) {
+ return ffmpeg_cleanup(PLAY_ERR, st, &swr, &dec, &fmt,
+ &pkt, &frm, &outbuf);
+ }
+
+ stream = av_find_best_stream(fmt, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
+ if (stream < 0) {
+ return ffmpeg_cleanup(PLAY_ERR, st, &swr, &dec, &fmt,
+ &pkt, &frm, &outbuf);
+ }
+
+ stm = fmt->streams[stream];
+ codec = avcodec_find_decoder(stm->codecpar->codec_id);
+ if (codec == NULL) {
+ return ffmpeg_cleanup(PLAY_ERR, st, &swr, &dec, &fmt,
+ &pkt, &frm, &outbuf);
+ }
+
+ dec = avcodec_alloc_context3(codec);
+ if (dec == NULL) {
+ return ffmpeg_cleanup(PLAY_ERR, st, &swr, &dec, &fmt,
+ &pkt, &frm, &outbuf);
+ }
+ if (avcodec_parameters_to_context(dec, stm->codecpar) < 0) {
+ return ffmpeg_cleanup(PLAY_ERR, st, &swr, &dec, &fmt,
+ &pkt, &frm, &outbuf);
+ }
+ if (avcodec_open2(dec, codec, NULL) < 0) {
+ return ffmpeg_cleanup(PLAY_ERR, st, &swr, &dec, &fmt,
+ &pkt, &frm, &outbuf);
+ }
+
+#if LIBAVUTIL_VERSION_MAJOR >= 57
+ out_rate = dec->sample_rate;
+ out_ch = dec->ch_layout.nb_channels > 0 ?
+ dec->ch_layout.nb_channels : 2;
+ out_layout = dec->ch_layout;
+
+ if (out_layout.nb_channels == 0)
+ av_channel_layout_default(&out_layout, out_ch);
+
+#if LIBSWRESAMPLE_VERSION_MAJOR >= 4
+ if (swr_alloc_set_opts2(&swr, &out_layout, AV_SAMPLE_FMT_S16,
+ out_rate, &out_layout, dec->sample_fmt, dec->sample_rate,
+ 0, NULL) < 0) {
+ avcodec_free_context(&dec);
+ avformat_close_input(&fmt);
+ return PLAY_ERR;
+ }
+#else
+ swr = swr_alloc_set_opts(NULL, out_layout.u.mask, AV_SAMPLE_FMT_S16,
+ out_rate, out_layout.u.mask, dec->sample_fmt, dec->sample_rate,
+ 0, NULL);
+#endif
+#else
+ out_rate = dec->sample_rate;
+ out_ch = dec->channels > 0 ? dec->channels : 2;
+ out_layout = dec->channel_layout;
+
+ if (out_layout == 0)
+ out_layout = av_get_default_channel_layout(out_ch);
+
+ swr = swr_alloc_set_opts(NULL, out_layout, AV_SAMPLE_FMT_S16,
+ out_rate, out_layout, dec->sample_fmt, dec->sample_rate, 0, NULL);
+#endif
+ if (swr == NULL || swr_init(swr) < 0) {
+ return ffmpeg_cleanup(PLAY_ERR, st, &swr, &dec, &fmt,
+ &pkt, &frm, &outbuf);
+ }
+
+ st->rate = (unsigned int)out_rate;
+ st->channels = (unsigned int)out_ch;
+ st->bits = 16;
+ st->frame_bytes = (size_t)out_ch * 2;
+ st->data_offset = 0;
+ st->data_pos = 0;
+
+ duration_sec = 0.0;
+ if (stm->duration != AV_NOPTS_VALUE)
+ duration_sec = (double)stm->duration * av_q2d(stm->time_base);
+ else if (fmt->duration > 0)
+ duration_sec = (double)fmt->duration / (double)AV_TIME_BASE;
+ if (duration_sec > 0) {
+ double bps = bytes_per_sec(st);
+
+ st->data_size = (bps > 0.0) ? (off_t)(duration_sec * bps) : 0;
+ } else
+ st->data_size = 0;
+
+ ret = open_pcm(&st->pcm, device, st->rate, st->channels);
+ if (ret < 0) {
+ warnx("alsa: %s", snd_strerror(ret));
+ return ffmpeg_cleanup(PLAY_ERR, st, &swr, &dec, &fmt,
+ &pkt, &frm, &outbuf);
+ }
+
+ disp = format_tag_name(namebuf, sizeof(namebuf), path, fmt);
+ pkt = av_packet_alloc();
+ frm = av_frame_alloc();
+ if (pkt == NULL || frm == NULL) {
+ return ffmpeg_cleanup(PLAY_ERR, st, &swr, &dec, &fmt,
+ &pkt, &frm, &outbuf);
+ }
+
+ last_ui = 0.0;
+
+ compute_base_dir(path, base_dir, sizeof(base_dir));
+ while (!g_stop) {
+ seek_dir = 0;
+ if (search.active) {
+ enum search_result sr =
+ search_handle_input(&search, ctrl_fd, base_dir,
+ g_search_path, sizeof(g_search_path));
+ if (sr == SEARCH_SUBMIT) {
+ search_end(disp, st, &search, &last_ui);
+ return ffmpeg_cleanup(PLAY_SEARCH, st, &swr, &dec,
+ &fmt, &pkt, &frm, &outbuf);
+ }
+ if (sr == SEARCH_CANCEL)
+ search_end(disp, st, &search, &last_ui);
+ } else {
+ ka = read_action(st->keys, ctrl_fd, keybuf, &keylen);
+ if (ka == KA_SEARCH && ctrl_fd >= 0) {
+ search_start(&search);
+ } else {
+ act = handle_key(st, ka, &seek_dir,
+ allow_nextprev);
+ if (act == PLAY_QUIT || act == PLAY_NEXT ||
+ act == PLAY_PREV) {
+ return ffmpeg_cleanup(act, st, &swr, &dec,
+ &fmt, &pkt, &frm, &outbuf);
+ }
+ }
+ }
+ if (seek_dir != 0) {
+ double bps = bytes_per_sec(st);
+ double pos = bytes_to_sec(st->data_pos, st);
+
+ pos += (seek_dir < 0) ? -SEEK_SECONDS : SEEK_SECONDS;
+ if (pos < 0)
+ pos = 0;
+ int64_t ts = sec_to_ts(pos, stm->time_base);
+
+ if (av_seek_frame(fmt, stream, ts,
+ AVSEEK_FLAG_BACKWARD) >= 0) {
+ avcodec_flush_buffers(dec);
+ snd_pcm_drop(st->pcm);
+ snd_pcm_prepare(st->pcm);
+ st->data_pos = (bps > 0.0) ? (off_t)(pos * bps) : 0;
+ }
+ }
+
+ if (st->paused) {
+ struct timespec ts = {0, 100000000};
+
+ nanosleep(&ts, NULL);
+ if (search.active)
+ search_render(disp, st, &search, &last_ui);
+ else
+ maybe_update_ui(disp, st, &last_ui);
+ continue;
+ }
+
+ ret = av_read_frame(fmt, pkt);
+ if (ret < 0)
+ avcodec_send_packet(dec, NULL);
+ else if (pkt->stream_index == stream)
+ avcodec_send_packet(dec, pkt);
+ av_packet_unref(pkt);
+
+ for (;;) {
+ ret = avcodec_receive_frame(dec, frm);
+ if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
+ break;
+ if (ret < 0) {
+ return ffmpeg_cleanup(PLAY_ERR, st, &swr, &dec,
+ &fmt, &pkt, &frm, &outbuf);
+ }
+
+ int out_samples = av_rescale_rnd(
+ swr_get_delay(swr, dec->sample_rate) +
+ frm->nb_samples, out_rate, dec->sample_rate,
+ AV_ROUND_UP);
+ int needed = av_samples_get_buffer_size(NULL, out_ch,
+ out_samples, AV_SAMPLE_FMT_S16, 1);
+
+ if (needed > outbuf_size) {
+ av_freep(&outbuf);
+ outbuf = av_malloc((size_t)needed);
+ outbuf_size = needed;
+ }
+ if (outbuf == NULL) {
+ return ffmpeg_cleanup(PLAY_ERR, st, &swr, &dec,
+ &fmt, &pkt, &frm, &outbuf);
+ }
+
+ uint8_t *out_planes[] = { outbuf };
+ int converted = swr_convert(swr, out_planes, out_samples,
+ (const uint8_t **)frm->data, frm->nb_samples);
+ if (converted < 0)
+ continue;
+
+ size_t out_bytes = (size_t)converted * st->frame_bytes;
+ apply_volume((int16_t *)outbuf,
+ (size_t)converted * st->channels, st->volume);
+
+ size_t frames = (size_t)converted;
+ size_t offset = 0;
+ while (frames > 0) {
+ snd_pcm_sframes_t wrote =
+ snd_pcm_writei(st->pcm, outbuf + offset,
+ frames);
+ if (wrote < 0) {
+ if (wrote == -EPIPE) {
+ snd_pcm_prepare(st->pcm);
+ continue;
+ }
+ break;
+ }
+ offset += (size_t)wrote * st->frame_bytes;
+ frames -= (size_t)wrote;
+ }
+
+ st->data_pos += (off_t)out_bytes;
+
+ if (search.active)
+ search_render(disp, st, &search, &last_ui);
+ else
+ maybe_update_ui(disp, st, &last_ui);
+ }
+
+ if (ret == AVERROR_EOF)
+ break;
+ }
+
+ return ffmpeg_cleanup(PLAY_DONE, st, &swr, &dec, &fmt,
+ &pkt, &frm, &outbuf);
+}
+#endif
+
+static size_t
+sanitize_utf8_trunc(const char *s, size_t max_bytes, char *out, size_t out_sz)
+{
+ size_t i = 0;
+ size_t o = 0;
+
+ if (out_sz == 0)
+ return 0;
+ if (s == NULL) {
+ out[0] = '\0';
+ return 0;
+ }
+
+ while (s[i] != '\0' && i < max_bytes && o + 1 < out_sz) {
+ unsigned char c = (unsigned char)s[i];
+ size_t seq_len = 0;
+ size_t k;
+ bool ok = true;
+
+ if (c < 0x20 || c == 0x7f) {
+ out[o++] = '?';
+ i++;
+ continue;
+ }
+ if (c < 0x80) {
+ out[o++] = (char)c;
+ i++;
+ continue;
+ }
+
+ if (c >= 0xC2 && c <= 0xDF)
+ seq_len = 2;
+ else if (c >= 0xE0 && c <= 0xEF)
+ seq_len = 3;
+ else if (c >= 0xF0 && c <= 0xF4)
+ seq_len = 4;
+ else
+ seq_len = 1;
+
+ if (seq_len == 1 || i + seq_len > max_bytes) {
+ out[o++] = '?';
+ i++;
+ continue;
+ }
+ for (k = 1; k < seq_len; ++k) {
+ unsigned char cc = (unsigned char)s[i + k];
+ if ((cc & 0xC0) != 0x80) {
+ ok = false;
+ break;
+ }
+ }
+ if (!ok || o + seq_len >= out_sz) {
+ out[o++] = '?';
+ i++;
+ continue;
+ }
+ memcpy(out + o, s + i, seq_len);
+ o += seq_len;
+ i += seq_len;
+ }
+
+ out[o] = '\0';
+ return o;
+}
+
+int
+main(int argc, char *argv[])
+{
+ char **paths = NULL;
+ struct termios orig_term;
+ struct keymap keys;
+ char device[128];
+ int path_count = 0;
+ int ctrl_fd = -1;
+ bool term_set = false;
+ unsigned int rate = DEFAULT_RATE;
+ unsigned int channels = DEFAULT_CHANNELS;
+ unsigned int bits = 16;
+ int volume = DEFAULT_VOLUME;
+ double seek_start = 0.0;
+ bool raw = false;
+ bool repeat = false;
+ bool shuffle = false;
+ int cur_index = 0;
+ bool use_stdin;
+ int ch;
+ int exit_code = 0;
+
+ progname = argv[0];
+ if (progname == NULL || progname[0] == '\0')
+ progname = "jam";
+
+ device[0] = '\0';
+ memset(&orig_term, 0, sizeof(orig_term));
+ keymap_defaults(&keys);
+ load_config(device, sizeof(device), &rate, &channels, &volume,
+ &repeat, &shuffle, &keys);
+ normalize_device_opt(&argc, &argv);
+
+ while ((ch = getopt(argc, argv, "Rd:r:c:v:s:h")) != -1) {
+ switch (ch) {
+ case 'R':
+ raw = true;
+ break;
+ case 'd':
+ if (optarg && optarg[0]) {
+ strncpy(device, optarg, sizeof(device) - 1);
+ device[sizeof(device) - 1] = '\0';
+ }
+ break;
+ case 'r':
+ rate = (unsigned int)atoi(optarg);
+ break;
+ case 'c':
+ channels = (unsigned int)atoi(optarg);
+ break;
+ case 'v':
+ volume = atoi(optarg);
+ if (volume < 0)
+ volume = 0;
+ if (volume > 100)
+ volume = 100;
+ break;
+ case 's':
+ seek_start = atof(optarg);
+ if (seek_start < 0)
+ seek_start = 0;
+ break;
+ case 'h':
+ default:
+ usage();
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (argc > 0) {
+ if (collect_paths(&paths, &path_count, argc, argv, 0) < 0) {
+ warnx("failed to read paths");
+ exit_code = 1;
+ goto out;
+ }
+ }
+
+ use_stdin = (path_count == 0 || strcmp(paths[0], "-") == 0);
+ if (use_stdin) {
+ ctrl_fd = open("/dev/tty", O_RDONLY);
+ } else if (isatty(STDIN_FILENO))
+ ctrl_fd = STDIN_FILENO;
+
+ if (set_raw_mode_fd(ctrl_fd, &orig_term) == 0)
+ term_set = true;
+
+ signal(SIGINT, on_sigint);
+ signal(SIGTERM, on_sigint);
+
+ srand((unsigned int)time(NULL));
+
+ if (use_stdin && !raw) {
+ warnx("stdin requires -R (raw PCM 16-bit LE)");
+ exit_code = 1;
+ goto out;
+ }
+
+ while (!g_stop) {
+ const char *path = use_stdin ? "-" : paths[cur_index];
+ const char *name = use_stdin ? "-" : path;
+ struct player_state st = {0};
+ enum play_action act = PLAY_DONE;
+ int rc;
+ int fd = STDIN_FILENO;
+
+ st.volume = volume;
+ st.repeat = &repeat;
+ st.shuffle = &shuffle;
+ st.keys = &keys;
+
+ if (!use_stdin) {
+ fd = open(path, O_RDONLY);
+ if (fd < 0) {
+ warn("%s", path);
+ exit_code = 1;
+ act = PLAY_ERR;
+ goto loop_done;
+ }
+ }
+
+ if (raw) {
+ st.rate = rate;
+ st.channels = channels;
+ st.bits = bits;
+ st.frame_bytes = (size_t)channels * (bits / 8);
+ st.seekable = !use_stdin;
+ st.data_offset = 0;
+ st.data_size = 0;
+ st.data_pos = 0;
+
+ rc = open_pcm(&st.pcm, device, rate, channels);
+ if (rc < 0) {
+ warnx("alsa: %s", snd_strerror(rc));
+ exit_code = 1;
+ act = PLAY_ERR;
+ goto loop_done;
+ }
+ act = play_pcm(&st, fd, ctrl_fd, name, false,
+ (!use_stdin && path_count > 1));
+ close_pcm(st.pcm);
+ } else {
+ struct wav_info info = {0};
+
+ if (!use_stdin)
+ info = parse_wav_header(fd);
+ if (!use_stdin && info.ok && info.bits == 16) {
+ rate = info.rate;
+ channels = info.channels;
+ bits = info.bits;
+ if (lseek(fd, info.data_offset, SEEK_SET) < 0) {
+ warn("lseek");
+ exit_code = 1;
+ act = PLAY_ERR;
+ goto loop_done;
+ }
+
+ st.rate = rate;
+ st.channels = channels;
+ st.bits = bits;
+ st.frame_bytes = (size_t)channels * (bits / 8);
+ st.seekable = true;
+ st.data_offset = info.data_offset;
+ st.data_size = info.data_size;
+ st.data_pos = info.data_offset;
+
+ if (seek_start > 0)
+ seek_to(&st, fd, seek_start);
+
+ rc = open_pcm(&st.pcm, device, rate, channels);
+ if (rc < 0) {
+ warnx("alsa: %s", snd_strerror(rc));
+ exit_code = 1;
+ act = PLAY_ERR;
+ goto loop_done;
+ }
+ act = play_pcm(&st, fd, ctrl_fd, name, true,
+ (!use_stdin && path_count > 1));
+ close_pcm(st.pcm);
+ } else {
+#ifdef HAVE_FFMPEG
+ if (!use_stdin) {
+ act = play_ffmpeg(&st, path, ctrl_fd,
+ (path_count > 1), device);
+ } else {
+ warnx("stdin requires -R (raw PCM 16-bit LE)");
+ exit_code = 1;
+ act = PLAY_ERR;
+ goto loop_done;
+ }
+#else
+ warnx("unsupported format (rebuild with ffmpeg)");
+ exit_code = 1;
+ act = PLAY_ERR;
+ goto loop_done;
+#endif
+ }
+ }
+
+loop_done:
+ if (!use_stdin)
+ close(fd);
+
+ volume = st.volume;
+
+ if (act == PLAY_ERR)
+ exit_code = 1;
+ if (act == PLAY_QUIT || act == PLAY_ERR)
+ break;
+ if (act == PLAY_SEARCH) {
+ if (reset_paths(&paths, &path_count, g_search_path) < 0) {
+ warnx("failed to read paths");
+ exit_code = 1;
+ break;
+ }
+ cur_index = 0;
+ use_stdin = (path_count == 0 || strcmp(paths[0], "-") == 0);
+ if (use_stdin && !raw) {
+ warnx("stdin requires -R (raw PCM 16-bit LE)");
+ exit_code = 1;
+ break;
+ }
+ continue;
+ }
+ if (use_stdin)
+ break;
+
+ if (repeat && act == PLAY_DONE)
+ continue;
+ if (shuffle && path_count > 1) {
+ cur_index = rand() % path_count;
+ continue;
+ }
+ if (act == PLAY_PREV) {
+ if (cur_index > 0)
+ cur_index--;
+ else
+ cur_index = 0;
+ } else {
+ if (cur_index + 1 < path_count)
+ cur_index++;
+ else
+ break;
+ }
+ }
+
+out:
+ fprintf(stderr, "\n");
+ if (term_set)
+ restore_term_fd(ctrl_fd, &orig_term);
+ if (ctrl_fd >= 0 && ctrl_fd != STDIN_FILENO)
+ close(ctrl_fd);
+ if (paths != NULL) {
+ int i;
+
+ for (i = 0; i < path_count; ++i)
+ free(paths[i]);
+ free(paths);
+ }
+ if (!g_quit)
+ save_config_volume(volume);
+ return exit_code;
+}
diff --git a/jamrc b/jamrc
new file mode 100644
index 0000000..66eabd4
--- /dev/null
+++ b/jamrc
@@ -0,0 +1,24 @@
+rate=44100
+channels=2
+volume=85
+device=default
+repeat=0
+shuffle=0
+
+# keybinds
+# value(s) can be single character (case-sensitive) or key name, comma-separated.
+# key names: space, esc, enter, tab, backspace, up, down, left, right, home, end, pgup, pgdn, ins, del
+# modifiers: ctrl+a..ctrl+z, alt+space
+# func keys: F1..F24
+
+key_quit= "q", "esc", "ctrl+c"
+key_pause= "p", "space"
+key_vol_down= "j", "-", "down"
+key_vol_up= "k", "+", "up"
+key_seek_back= "h", "left"
+key_seek_fwd= "l", "right"
+key_next= "n", "F8", "pgdn"
+key_prev= "p", "F7", "pgup"
+key_repeat= "r", "F2"
+key_shuffle= "s", "F3"
+key_search= "/", "F4"