first commit

This commit is contained in:
암냥 2026-06-23 08:36:15 +09:00
commit 10be425f9d
9 changed files with 96922 additions and 0 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.DS_Store
build/
tui-music-player
tui-music-player.exe
music/*
!music/.gitkeep

28
CMakeLists.txt Normal file
View file

@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.16)
project(tui_music_player C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
add_executable(tui-music-player src/main.c)
target_include_directories(tui-music-player PRIVATE vendor)
if(MSVC)
target_compile_options(tui-music-player PRIVATE /W4)
else()
target_compile_options(tui-music-player PRIVATE -Wall -Wextra -Wpedantic)
endif()
if(WIN32)
target_link_libraries(tui-music-player PRIVATE ole32 uuid winmm avrt)
elseif(APPLE)
target_link_libraries(tui-music-player PRIVATE
"-framework CoreAudio"
"-framework CoreFoundation"
"-framework AudioToolbox"
pthread
m
)
else()
target_link_libraries(tui-music-player PRIVATE dl pthread m)
endif()

31
Makefile Normal file
View file

@ -0,0 +1,31 @@
CC ?= cc
TARGET := tui-music-player
SRC := src/main.c
CFLAGS ?= -std=c11 -Wall -Wextra -Wpedantic -O2
CPPFLAGS += -Ivendor
UNAME_S := $(shell uname -s 2>/dev/null)
ifeq ($(OS),Windows_NT)
TARGET := tui-music-player.exe
LDLIBS += -lole32 -luuid -lwinmm -lavrt
else ifeq ($(UNAME_S),Darwin)
LDFLAGS += -framework CoreAudio -framework CoreFoundation -framework AudioToolbox
LDLIBS += -lpthread -lm
else
LDLIBS += -ldl -lpthread -lm
endif
.PHONY: all clean run
all: $(TARGET)
$(TARGET): $(SRC)
$(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) -o $@ $< $(LDLIBS)
run: $(TARGET)
./$(TARGET)
clean:
rm -f $(TARGET)

90
README.md Normal file
View file

@ -0,0 +1,90 @@
# TUI Music Player
외부 앱 없이 동작하는 크로스플랫폼 TUI 음악 플레이어입니다.
터미널 UI는 ANSI escape sequence와 OS별 키 입력 처리로 구현했고, 재생 엔진은 단일 헤더 라이브러리인 `miniaudio``vendor/miniaudio.h`에 포함했습니다.
## Requirements
- Windows, macOS, Linux
- C11 compiler
- CMake 3.16+ 또는 `make`
## Build
### Nix devshell
```sh
nix develop
tui-build
tui-run
```
한 줄로 실행할 수도 있습니다.
```sh
nix develop -c tui-run
```
devshell에는 `cmake`, `ninja`, `make`, `pkg-config`, `clang-tools`와 플랫폼별 오디오 빌드/런타임 패키지가 포함됩니다.
### CMake
```sh
cmake -S . -B build
cmake --build build
```
Windows에서는 Visual Studio, Ninja, MinGW 같은 CMake generator를 사용할 수 있습니다.
### Make
```sh
make
./tui-music-player
```
Windows + MinGW에서는 결과물이 `tui-music-player.exe`로 생성됩니다.
## Add Music
가장 쉬운 방법은 프로젝트의 `music/` 폴더에 음악 파일을 넣는 것입니다.
```text
TUIMusicPlayer/
music/
first-song.mp3
second-song.wav
```
앱을 실행하면 `music/` 안의 오디오 파일을 자동으로 플레이리스트에 넣습니다. 실행 중 파일을 추가했다면 `r` 키로 다시 스캔할 수 있습니다.
파일 경로를 직접 넘겨서 시작할 수도 있습니다.
```sh
./tui-music-player ~/Music/song.mp3 ~/Music/another.wav
```
실행 중에는 `a` 키로 파일 경로를 직접 추가할 수 있습니다.
`miniaudio`는 WAV/FLAC/MP3 등 일반적인 로컬 오디오 파일을 디코딩할 수 있습니다.
## Controls
| Key | Action |
| --- | --- |
| `j` / `Down` | Move selection down |
| `k` / `Up` | Move selection up |
| `Enter` | Play selected track |
| `p` / `P` / Space | Pause or resume |
| `s` / `S` | Shuffle playlist |
| `a` | Add a track path after the selected row |
| `r` | Rescan `music/` and add new files |
| `d` / `x` | Delete selected track |
| `n` | Next track |
| `b` | Previous track |
| `g` | Jump to top |
| `G` | Jump to bottom |
| `q` | Quit |
Vim 스타일 이동을 기본으로 두고, 요청하신 `P``S`도 대문자 그대로 동작합니다.

27
flake.lock generated Normal file
View file

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1781577229,
"narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "567a49d1913ce81ac6e9582e3553dd90a955875f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

115
flake.nix Normal file
View file

@ -0,0 +1,115 @@
{
description = "Cross-platform TUI music player development shell";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs =
{ nixpkgs, ... }:
let
systems = [
"aarch64-darwin"
"x86_64-darwin"
"aarch64-linux"
"x86_64-linux"
];
forAllSystems = nixpkgs.lib.genAttrs systems;
perSystem =
system:
let
pkgs = import nixpkgs { inherit system; };
lib = pkgs.lib;
linuxAudioRuntime = lib.optionals pkgs.stdenv.isLinux [
pkgs.alsa-lib
pkgs.pulseaudio
];
tui-build = pkgs.writeShellApplication {
name = "tui-build";
runtimeInputs = [
pkgs.cmake
pkgs.ninja
];
text = ''
cmake -S . -B build -G Ninja
cmake --build build
'';
};
tui-run = pkgs.writeShellApplication {
name = "tui-run";
runtimeInputs = [
tui-build
];
text = ''
tui-build
exec ./build/tui-music-player "$@"
'';
};
tui-clean = pkgs.writeShellApplication {
name = "tui-clean";
text = ''
rm -rf build tui-music-player tui-music-player.exe
'';
};
in
{
packages.default = pkgs.stdenv.mkDerivation {
pname = "tui-music-player";
version = "0.1.0";
src = ./.;
nativeBuildInputs = [
pkgs.cmake
pkgs.ninja
];
buildInputs = linuxAudioRuntime;
cmakeFlags = [
"-G"
"Ninja"
];
installPhase = ''
runHook preInstall
install -Dm755 tui-music-player "$out/bin/tui-music-player"
runHook postInstall
'';
};
devShells.default = pkgs.mkShell {
packages =
[
pkgs.cmake
pkgs.ninja
pkgs.gnumake
pkgs.pkg-config
pkgs.clang-tools
tui-build
tui-run
tui-clean
]
++ linuxAudioRuntime;
shellHook = ''
mkdir -p music
echo "TUI Music Player devshell"
echo " tui-build - configure and build with CMake + Ninja"
echo " tui-run - build, then run ./build/tui-music-player"
echo " tui-clean - remove generated build outputs"
echo "Put audio files in ./music, then run: tui-run"
'';
};
};
in
{
packages = forAllSystems (system: (perSystem system).packages);
devShells = forAllSystems (system: (perSystem system).devShells);
};
}

1
music/.gitkeep Normal file
View file

@ -0,0 +1 @@

759
src/main.c Normal file
View file

@ -0,0 +1,759 @@
#define MINIAUDIO_IMPLEMENTATION
#include "../vendor/miniaudio.h"
#include <ctype.h>
#include <errno.h>
#include <locale.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <conio.h>
#include <windows.h>
#else
#include <dirent.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <termios.h>
#include <unistd.h>
#endif
#define MAX_TRACKS 256
#define PATH_BUFFER 1024
#define STATUS_BUFFER 256
#define MUSIC_DIRECTORY "music"
typedef enum {
KEY_NONE = 0,
KEY_ARROW_UP = 1000,
KEY_ARROW_DOWN,
KEY_ENTER_VALUE
} Key;
typedef struct {
char *path;
} Track;
typedef struct {
Track tracks[MAX_TRACKS];
size_t count;
size_t selected;
int current;
bool paused;
bool has_sound;
ma_engine engine;
ma_sound sound;
char status[STATUS_BUFFER];
} App;
typedef struct {
int rows;
int cols;
#ifdef _WIN32
DWORD original_input_mode;
DWORD original_output_mode;
HANDLE input;
HANDLE output;
#else
struct termios original_termios;
#endif
} Terminal;
static char *copy_string(const char *value) {
size_t len = strlen(value) + 1;
char *copy = malloc(len);
if (copy == NULL) {
return NULL;
}
memcpy(copy, value, len);
return copy;
}
static const char *basename_for_display(const char *path) {
const char *slash = strrchr(path, '/');
const char *backslash = strrchr(path, '\\');
const char *separator = slash;
if (separator == NULL || (backslash != NULL && backslash > separator)) {
separator = backslash;
}
return separator == NULL ? path : separator + 1;
}
static void set_status(App *app, const char *message) {
snprintf(app->status, sizeof(app->status), "%s", message);
}
static void free_track(Track *track) {
free(track->path);
track->path = NULL;
}
static bool has_audio_extension(const char *path) {
const char *dot = strrchr(path, '.');
if (dot == NULL) {
return false;
}
char extension[8];
size_t i = 0;
for (; dot[i] != '\0' && i + 1 < sizeof(extension); ++i) {
extension[i] = (char)tolower((unsigned char)dot[i]);
}
extension[i] = '\0';
return strcmp(extension, ".mp3") == 0 || strcmp(extension, ".wav") == 0 || strcmp(extension, ".flac") == 0 ||
strcmp(extension, ".ogg") == 0 || strcmp(extension, ".m4a") == 0 || strcmp(extension, ".aac") == 0 ||
strcmp(extension, ".opus") == 0;
}
static bool track_exists(App *app, const char *path) {
for (size_t i = 0; i < app->count; ++i) {
if (strcmp(app->tracks[i].path, path) == 0) {
return true;
}
}
return false;
}
static int compare_tracks_by_name(const void *left, const void *right) {
const Track *a = (const Track *)left;
const Track *b = (const Track *)right;
return strcmp(basename_for_display(a->path), basename_for_display(b->path));
}
static void sort_playlist(App *app) {
char *current_path = NULL;
char *selected_path = NULL;
if (app->current >= 0 && (size_t)app->current < app->count) {
current_path = app->tracks[app->current].path;
}
if (app->selected < app->count) {
selected_path = app->tracks[app->selected].path;
}
qsort(app->tracks, app->count, sizeof(app->tracks[0]), compare_tracks_by_name);
app->current = -1;
app->selected = 0;
for (size_t i = 0; i < app->count; ++i) {
if (current_path != NULL && app->tracks[i].path == current_path) {
app->current = (int)i;
}
if (selected_path != NULL && app->tracks[i].path == selected_path) {
app->selected = i;
}
}
}
static bool terminal_begin(Terminal *terminal) {
#ifdef _WIN32
terminal->input = GetStdHandle(STD_INPUT_HANDLE);
terminal->output = GetStdHandle(STD_OUTPUT_HANDLE);
if (terminal->input == INVALID_HANDLE_VALUE || terminal->output == INVALID_HANDLE_VALUE) {
return false;
}
GetConsoleMode(terminal->input, &terminal->original_input_mode);
GetConsoleMode(terminal->output, &terminal->original_output_mode);
DWORD input_mode = terminal->original_input_mode;
input_mode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT);
SetConsoleMode(terminal->input, input_mode);
DWORD output_mode = terminal->original_output_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
SetConsoleMode(terminal->output, output_mode);
#else
if (tcgetattr(STDIN_FILENO, &terminal->original_termios) != 0) {
return false;
}
struct termios raw = terminal->original_termios;
raw.c_lflag &= (tcflag_t) ~(ECHO | ICANON);
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 0;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) != 0) {
return false;
}
#endif
printf("\x1b[?25l");
fflush(stdout);
return true;
}
static void terminal_end(Terminal *terminal) {
printf("\x1b[?25h\x1b[0m");
fflush(stdout);
#ifdef _WIN32
SetConsoleMode(terminal->input, terminal->original_input_mode);
SetConsoleMode(terminal->output, terminal->original_output_mode);
#else
tcsetattr(STDIN_FILENO, TCSAFLUSH, &terminal->original_termios);
#endif
}
static void terminal_refresh_size(Terminal *terminal) {
#ifdef _WIN32
CONSOLE_SCREEN_BUFFER_INFO info;
if (GetConsoleScreenBufferInfo(terminal->output, &info)) {
terminal->cols = info.srWindow.Right - info.srWindow.Left + 1;
terminal->rows = info.srWindow.Bottom - info.srWindow.Top + 1;
return;
}
#else
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0 && ws.ws_row > 0) {
terminal->cols = ws.ws_col;
terminal->rows = ws.ws_row;
return;
}
#endif
terminal->cols = 80;
terminal->rows = 24;
}
static void sleep_ms(int milliseconds) {
#ifdef _WIN32
Sleep((DWORD)milliseconds);
#else
usleep((useconds_t)milliseconds * 1000);
#endif
}
static int read_key(void) {
#ifdef _WIN32
if (!_kbhit()) {
return KEY_NONE;
}
int ch = _getch();
if (ch == 0 || ch == 224) {
int extended = _getch();
if (extended == 72) {
return KEY_ARROW_UP;
}
if (extended == 80) {
return KEY_ARROW_DOWN;
}
return KEY_NONE;
}
if (ch == '\r') {
return KEY_ENTER_VALUE;
}
return ch;
#else
fd_set set;
struct timeval timeout = {0, 0};
FD_ZERO(&set);
FD_SET(STDIN_FILENO, &set);
if (select(STDIN_FILENO + 1, &set, NULL, NULL, &timeout) <= 0) {
return KEY_NONE;
}
unsigned char ch;
if (read(STDIN_FILENO, &ch, 1) != 1) {
return KEY_NONE;
}
if (ch == '\n' || ch == '\r') {
return KEY_ENTER_VALUE;
}
if (ch == '\x1b') {
unsigned char seq[2];
if (read(STDIN_FILENO, &seq[0], 1) != 1 || read(STDIN_FILENO, &seq[1], 1) != 1) {
return KEY_NONE;
}
if (seq[0] == '[' && seq[1] == 'A') {
return KEY_ARROW_UP;
}
if (seq[0] == '[' && seq[1] == 'B') {
return KEY_ARROW_DOWN;
}
return KEY_NONE;
}
return ch;
#endif
}
static bool insert_track(App *app, size_t index, const char *path) {
if (app->count >= MAX_TRACKS || path[0] == '\0') {
set_status(app, "Cannot add track: playlist is full or path is empty.");
return false;
}
char *copy = copy_string(path);
if (copy == NULL) {
set_status(app, "Cannot add track: out of memory.");
return false;
}
if (index > app->count) {
index = app->count;
}
for (size_t i = app->count; i > index; --i) {
app->tracks[i] = app->tracks[i - 1];
}
app->tracks[index].path = copy;
app->count++;
app->selected = index;
set_status(app, "Track added.");
return true;
}
static void add_track_if_audio(App *app, const char *path, size_t *added) {
if (!has_audio_extension(path) || track_exists(app, path)) {
return;
}
if (insert_track(app, app->count, path)) {
(*added)++;
}
}
static size_t scan_music_directory(App *app, const char *directory) {
size_t added = 0;
#ifdef _WIN32
char pattern[PATH_BUFFER];
snprintf(pattern, sizeof(pattern), "%s\\*", directory);
WIN32_FIND_DATAA data;
HANDLE find = FindFirstFileA(pattern, &data);
if (find == INVALID_HANDLE_VALUE) {
return 0;
}
do {
if ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
continue;
}
char path[PATH_BUFFER];
snprintf(path, sizeof(path), "%s\\%s", directory, data.cFileName);
add_track_if_audio(app, path, &added);
} while (FindNextFileA(find, &data));
FindClose(find);
#else
DIR *dir = opendir(directory);
if (dir == NULL) {
return 0;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] == '.') {
continue;
}
char path[PATH_BUFFER];
snprintf(path, sizeof(path), "%s/%s", directory, entry->d_name);
add_track_if_audio(app, path, &added);
}
closedir(dir);
#endif
if (added > 0) {
sort_playlist(app);
snprintf(app->status, sizeof(app->status), "Loaded %zu track(s) from %s/.", added, directory);
}
return added;
}
static void load_startup_tracks(App *app, int argc, char **argv) {
size_t added = 0;
for (int i = 1; i < argc; ++i) {
add_track_if_audio(app, argv[i], &added);
}
if (argc <= 1) {
added = scan_music_directory(app, MUSIC_DIRECTORY);
} else if (added > 0) {
sort_playlist(app);
snprintf(app->status, sizeof(app->status), "Loaded %zu track(s) from command line.", added);
}
if (app->count == 0) {
snprintf(app->status, sizeof(app->status), "Put music files in %s/ or press 'a' to add a path.", MUSIC_DIRECTORY);
}
app->selected = 0;
}
static void stop_current(App *app) {
if (app->has_sound) {
ma_sound_stop(&app->sound);
ma_sound_uninit(&app->sound);
app->has_sound = false;
}
app->paused = false;
app->current = -1;
}
static bool play_track(App *app, size_t index) {
if (index >= app->count) {
set_status(app, "No track selected.");
return false;
}
stop_current(app);
ma_result result = ma_sound_init_from_file(&app->engine, app->tracks[index].path, MA_SOUND_FLAG_STREAM, NULL, NULL,
&app->sound);
if (result != MA_SUCCESS) {
snprintf(app->status, sizeof(app->status), "Cannot open: %s", app->tracks[index].path);
return false;
}
app->has_sound = true;
result = ma_sound_start(&app->sound);
if (result != MA_SUCCESS) {
stop_current(app);
set_status(app, "Playback failed.");
return false;
}
app->current = (int)index;
app->selected = index;
app->paused = false;
snprintf(app->status, sizeof(app->status), "Playing: %s", basename_for_display(app->tracks[index].path));
return true;
}
static void poll_audio(App *app) {
if (app->has_sound && !app->paused && ma_sound_at_end(&app->sound)) {
stop_current(app);
set_status(app, "Playback finished.");
}
}
static void delete_selected(App *app) {
if (app->count == 0) {
set_status(app, "Playlist is already empty.");
return;
}
size_t index = app->selected;
bool deleting_current = app->current == (int)index;
if (deleting_current) {
stop_current(app);
}
free_track(&app->tracks[index]);
for (size_t i = index; i + 1 < app->count; ++i) {
app->tracks[i] = app->tracks[i + 1];
}
app->count--;
if (app->count == 0) {
app->selected = 0;
app->current = -1;
} else if (app->selected >= app->count) {
app->selected = app->count - 1;
}
if (!deleting_current && app->current > (int)index) {
app->current--;
}
set_status(app, "Track deleted.");
}
static void shuffle_playlist(App *app) {
if (app->count < 2) {
set_status(app, "Need at least two tracks to shuffle.");
return;
}
char *current_path = NULL;
if (app->current >= 0 && (size_t)app->current < app->count) {
current_path = app->tracks[app->current].path;
}
for (size_t i = app->count - 1; i > 0; --i) {
size_t j = (size_t)(rand() % (int)(i + 1));
Track tmp = app->tracks[i];
app->tracks[i] = app->tracks[j];
app->tracks[j] = tmp;
}
app->selected = 0;
app->current = -1;
if (current_path != NULL) {
for (size_t i = 0; i < app->count; ++i) {
if (app->tracks[i].path == current_path) {
app->current = (int)i;
app->selected = i;
break;
}
}
}
set_status(app, "Playlist shuffled.");
}
static void toggle_pause(App *app) {
if (!app->has_sound) {
if (app->count > 0) {
play_track(app, app->selected);
} else {
set_status(app, "Nothing to pause: playlist is empty.");
}
return;
}
if (app->paused) {
if (ma_sound_start(&app->sound) == MA_SUCCESS) {
app->paused = false;
set_status(app, "Resumed.");
}
} else {
if (ma_sound_stop(&app->sound) == MA_SUCCESS) {
app->paused = true;
set_status(app, "Paused.");
}
}
}
static void move_selection(App *app, int delta) {
if (app->count == 0) {
return;
}
if (delta < 0 && app->selected > 0) {
app->selected--;
} else if (delta > 0 && app->selected + 1 < app->count) {
app->selected++;
}
}
static void play_relative(App *app, int delta) {
if (app->count == 0) {
set_status(app, "Playlist is empty.");
return;
}
int base = app->current >= 0 ? app->current : (int)app->selected;
int next = base + delta;
if (next < 0) {
next = (int)app->count - 1;
} else if (next >= (int)app->count) {
next = 0;
}
play_track(app, (size_t)next);
}
static void prompt_add_track(App *app, Terminal *terminal) {
char path[PATH_BUFFER];
terminal_end(terminal);
printf("\nAdd path: ");
fflush(stdout);
if (fgets(path, sizeof(path), stdin) == NULL) {
terminal_begin(terminal);
set_status(app, "Add cancelled.");
return;
}
path[strcspn(path, "\r\n")] = '\0';
terminal_begin(terminal);
if (path[0] == '\0') {
set_status(app, "Add cancelled.");
return;
}
size_t index = app->count == 0 ? 0 : app->selected + 1;
insert_track(app, index, path);
}
static void print_clipped(const char *text, int width) {
for (int i = 0; i < width && text[i] != '\0'; ++i) {
putchar(text[i]);
}
}
static void draw(App *app, Terminal *terminal) {
terminal_refresh_size(terminal);
int rows = terminal->rows;
int cols = terminal->cols;
printf("\x1b[2J\x1b[H");
if (rows < 10 || cols < 48) {
printf("Terminal is too small. Resize to at least 48x10.\n");
fflush(stdout);
return;
}
const char *state = "Stopped";
if (app->has_sound) {
state = app->paused ? "Paused" : "Playing";
}
printf("\x1b[1mTUI Music Player\x1b[0m\n");
printf("State: %-8s Tracks: %zu/%d\n", state, app->count, MAX_TRACKS);
for (int i = 0; i < cols; ++i) {
putchar('-');
}
putchar('\n');
int list_top = 4;
int list_bottom = rows - 5;
int visible = list_bottom - list_top + 1;
size_t offset = 0;
if (visible > 0 && app->selected >= (size_t)visible) {
offset = app->selected - (size_t)visible + 1;
}
if (app->count == 0) {
printf("Playlist is empty. Put files in music/ and press 'r', or press 'a' to add a path.\n");
} else {
for (int line = 0; line < visible; ++line) {
size_t index = offset + (size_t)line;
if (index >= app->count) {
putchar('\n');
continue;
}
bool selected = index == app->selected;
bool current = app->current == (int)index;
if (selected) {
printf("\x1b[7m");
}
if (current) {
printf("\x1b[1m");
}
printf("%c %3zu ", current ? '>' : ' ', index + 1);
print_clipped(basename_for_display(app->tracks[index].path), cols - 8);
printf("\x1b[0m\n");
}
}
printf("\x1b[%d;1H", rows - 3);
for (int i = 0; i < cols; ++i) {
putchar('-');
}
printf("\n");
print_clipped(
"j/k or arrows move | Enter play | P pause | S shuffle | a add | r rescan | d delete | n/b next/prev | q quit",
cols);
printf("\nStatus: ");
print_clipped(app->status, cols - 8);
fflush(stdout);
}
static void cleanup(App *app) {
stop_current(app);
ma_engine_uninit(&app->engine);
for (size_t i = 0; i < app->count; ++i) {
free_track(&app->tracks[i]);
}
}
int main(int argc, char **argv) {
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL));
App app = {
.count = 0,
.selected = 0,
.current = -1,
.paused = false,
.has_sound = false,
};
set_status(&app, "Starting.");
if (ma_engine_init(NULL, &app.engine) != MA_SUCCESS) {
fprintf(stderr, "Could not initialize audio engine.\n");
return EXIT_FAILURE;
}
load_startup_tracks(&app, argc, argv);
Terminal terminal = {0};
if (!terminal_begin(&terminal)) {
cleanup(&app);
fprintf(stderr, "Could not initialize terminal.\n");
return EXIT_FAILURE;
}
bool running = true;
while (running) {
poll_audio(&app);
draw(&app, &terminal);
int key = read_key();
switch (key) {
case KEY_NONE:
sleep_ms(40);
break;
case 'q':
running = false;
break;
case KEY_ARROW_UP:
case 'k':
move_selection(&app, -1);
break;
case KEY_ARROW_DOWN:
case 'j':
move_selection(&app, 1);
break;
case 'g':
app.selected = 0;
break;
case 'G':
if (app.count > 0) {
app.selected = app.count - 1;
}
break;
case KEY_ENTER_VALUE:
play_track(&app, app.selected);
break;
case 'p':
case 'P':
case ' ':
toggle_pause(&app);
break;
case 's':
case 'S':
shuffle_playlist(&app);
break;
case 'a':
prompt_add_track(&app, &terminal);
break;
case 'r': {
size_t added = scan_music_directory(&app, MUSIC_DIRECTORY);
if (added == 0) {
snprintf(app.status, sizeof(app.status), "No new audio files found in %s/.", MUSIC_DIRECTORY);
}
break;
}
case 'd':
case 'x':
delete_selected(&app);
break;
case 'n':
play_relative(&app, 1);
break;
case 'b':
play_relative(&app, -1);
break;
default:
if (isprint(key)) {
set_status(&app, "Unknown key.");
}
break;
}
}
terminal_end(&terminal);
cleanup(&app);
return EXIT_SUCCESS;
}

95864
vendor/miniaudio.h vendored Normal file

File diff suppressed because it is too large Load diff