#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_FFMPEG #include #include #include #include #include #include #include #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; }