Compare commits
No commits in common. "master" and "0.1" have entirely different histories.
5 changed files with 448 additions and 1047 deletions
96
readme.md
96
readme.md
|
|
@ -2,21 +2,18 @@
|
||||||
|
|
||||||
**c**onsole **i**nterface **m**usic **p**layer
|
**c**onsole **i**nterface **m**usic **p**layer
|
||||||
|
|
||||||
A keyboard-driven music player for the terminal, written in QuickBASIC (QB64).
|
A lightweight, keyboard-driven music player for the terminal, written in
|
||||||
It plays audio files and M3U playlists directly from the command line, featuring
|
QuickBASIC (QB64). It plays audio files and M3U playlists directly from the
|
||||||
a minimal two-line display showing playback state and an IPC remote control
|
command line, with a minimal two-line display showing playback state.
|
||||||
interface.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Plays individual audio files or M3U playlists.
|
- Plays individual audio files or M3U playlists
|
||||||
- Real-time progress bar and time display (elapsed or remaining).
|
- Real-time progress bar and time display (elapsed or remaining)
|
||||||
- Volume control and seeking via arrow keys.
|
- Volume control and seeking via arrow keys
|
||||||
- Shuffle and repeat modes.
|
- Shuffle and repeat modes
|
||||||
- **Client/Server IPC Architecture**: Control a running instance of `cimp` from
|
- Cross-platform: works on Linux, macOS, and Windows
|
||||||
another terminal window or script.
|
- Unicode-aware display
|
||||||
- Cross-platform: works on Linux, macOS, and Windows.
|
|
||||||
- Unicode-aware display with support for full-width character width matching.
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
|
|
@ -39,12 +36,9 @@ qb64 -x cimp.bas
|
||||||
cimp [options] <file|playlist> [file2 ...]
|
cimp [options] <file|playlist> [file2 ...]
|
||||||
```
|
```
|
||||||
|
|
||||||
Pass one or more audio files or `.m3u` playlists. At least one file is required
|
Pass one or more audio files or `.m3u` playlists. At least one file is required.
|
||||||
unless sending a remote control command.
|
|
||||||
|
|
||||||
### Global & Server Options
|
### Options
|
||||||
|
|
||||||
These flags control the primary player instance when launched.
|
|
||||||
|
|
||||||
| Flag | Long form | Description |
|
| Flag | Long form | Description |
|
||||||
| -------- | -------------- | ---------------------------------------- |
|
| -------- | -------------- | ---------------------------------------- |
|
||||||
|
|
@ -53,65 +47,21 @@ These flags control the primary player instance when launched.
|
||||||
| `-r [1]` | `--repeat [1]` | Repeat all (`-r`) or repeat one (`-r 1`) |
|
| `-r [1]` | `--repeat [1]` | Repeat all (`-r`) or repeat one (`-r 1`) |
|
||||||
| `-n` | `--nooutput` | Run silently with no display |
|
| `-n` | `--nooutput` | Run silently with no display |
|
||||||
|
|
||||||
### Remote Control (Client Mode) Commands
|
|
||||||
|
|
||||||
If `cimp` is already running in a terminal, running it again with any of the
|
|
||||||
following arguments will send an instruction to the active player instead of
|
|
||||||
starting a new one:
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
| ------------------------- | ---------------------------------------------------------- |
|
|
||||||
| `--play` | Resume playback |
|
|
||||||
| `--pause` | Pause playback |
|
|
||||||
| `--stop` | Stop playback |
|
|
||||||
| `--next` | Skip to the next track |
|
|
||||||
| `--prev` | Skip to the previous track |
|
|
||||||
| `-v <n>` / `--volume <n>` | Remotely adjust volume (0–100) |
|
|
||||||
| `--shuffle` | Reshuffle the current playlist queue |
|
|
||||||
| `--repeat` | Set player to repeat all |
|
|
||||||
| `--repeat-1` | Set player to repeat current track |
|
|
||||||
| `--add <file>` | Append a new file or playlist to the active queue |
|
|
||||||
| `--playlist` | Fetch and print the current playback queue from the server |
|
|
||||||
| `--quit` | Remotely close the running player instance |
|
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
#### Play a single file normally
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
# Play a single file
|
||||||
cimp song.mp3
|
cimp song.mp3
|
||||||
```
|
|
||||||
|
|
||||||
### Remote control examples (Run from a separate terminal)
|
# Play a playlist shuffled at 80% volume
|
||||||
|
cimp -s -v 80 playlist.m3u
|
||||||
|
|
||||||
#### Pause the music
|
# Play multiple files with repeat
|
||||||
|
cimp -r track1.ogg track2.ogg track3.ogg
|
||||||
```shell
|
|
||||||
cimp --pause
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Lower the volume to 20%
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cimp --volume 20
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Add another track to the background player's active queue
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cimp --add extra_track.flac
|
|
||||||
```
|
|
||||||
|
|
||||||
#### View the active playlist queue
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cimp --playlist
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Keybindings
|
## Keybindings
|
||||||
|
|
||||||
When interacting directly with the player interface terminal, use these keys:
|
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
| ------------- | ----------------------------------------- |
|
| ------------- | ----------------------------------------- |
|
||||||
| `↑` / `↓` | Volume up / down |
|
| `↑` / `↓` | Volume up / down |
|
||||||
|
|
@ -135,19 +85,13 @@ both compilers:
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
| File | Description |
|
| File | Description |
|
||||||
| ------------- | --------------------------------------------------------------------------------------- |
|
| ------------- | ---------------------------------------------------------------------------- |
|
||||||
| `cimp.bas` | Main player — QB64-PE source handling client/server states and UI |
|
| `cimp.bas` | Main player — QB64-PE source |
|
||||||
| `terminkey.h` | C library for cross-platform raw keyboard input, IPC pipes/sockets, and terminal sizing |
|
| `terminkey.h` | C library for cross-platform raw keyboard input and terminal width detection |
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- **IPC Backend**: On Windows, IPC relies on Named Pipes (`\\.\pipe\cimp_ipc`).
|
|
||||||
On Linux/macOS, it binds to a local Unix Domain Socket at
|
|
||||||
`/tmp/cimp_ipc_<uid>.sock`.
|
|
||||||
- M3U paths can be absolute or relative to the playlist file's directory.
|
- M3U paths can be absolute or relative to the playlist file's directory.
|
||||||
- Volume adjustment is non-linear for a more natural response and better control
|
- Volume adjustment is non-linear for a more natural response and better control
|
||||||
at lower ranges.
|
at lower ranges.
|
||||||
|
|
||||||
```
|
|
||||||
```
|
|
||||||
|
|
|
||||||
237
terminkey.h
237
terminkey.h
|
|
@ -3,23 +3,11 @@
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <conio.h>
|
#include <conio.h>
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#else
|
#else
|
||||||
#include <termios.h>
|
#include <termios.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <sys/ioctl.h>
|
#include <sys/ioctl.h>
|
||||||
#include <sys/socket.h>
|
|
||||||
#include <sys/un.h>
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <limits.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Special key codes (unified across platforms)
|
// Special key codes (unified across platforms)
|
||||||
|
|
@ -33,17 +21,8 @@ void echooff();
|
||||||
void echoon();
|
void echoon();
|
||||||
int termwidth();
|
int termwidth();
|
||||||
|
|
||||||
int ipc_init();
|
|
||||||
int ipc_check_message(uintptr_t buffer_ptr, int max_len);
|
|
||||||
void ipc_send_message(const char* message);
|
|
||||||
void ipc_cleanup();
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|
||||||
static HANDLE hServerPipe = INVALID_HANDLE_VALUE;
|
|
||||||
static OVERLAPPED oOverlap;
|
|
||||||
static BOOL fPendingIO = FALSE;
|
|
||||||
|
|
||||||
int terminkey() {
|
int terminkey() {
|
||||||
if (_kbhit()) {
|
if (_kbhit()) {
|
||||||
int ch = _getch();
|
int ch = _getch();
|
||||||
|
|
@ -86,126 +65,8 @@ int termwidth() {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ipc_init() {
|
|
||||||
// Create a regular blocking pipe, but enable FILE_FLAG_OVERLAPPED for safe async I/O
|
|
||||||
hServerPipe = CreateNamedPipe(
|
|
||||||
"\\\\.\\pipe\\cimp_ipc",
|
|
||||||
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
|
|
||||||
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE,
|
|
||||||
1,
|
|
||||||
4096,
|
|
||||||
4096,
|
|
||||||
0,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
if (hServerPipe == INVALID_HANDLE_VALUE) {
|
|
||||||
DWORD err = GetLastError();
|
|
||||||
if (err == ERROR_ACCESS_DENIED || err == ERROR_PIPE_BUSY) {
|
|
||||||
return 0; // Already running (Client Mode)
|
|
||||||
}
|
|
||||||
return -1; // Other error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the overlapped structure and create an event
|
|
||||||
memset(&oOverlap, 0, sizeof(OVERLAPPED));
|
|
||||||
oOverlap.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);
|
|
||||||
fPendingIO = FALSE;
|
|
||||||
|
|
||||||
return 1; // Server mode successfully started
|
|
||||||
}
|
|
||||||
|
|
||||||
int ipc_check_message(uintptr_t buffer_ptr, int max_len) {
|
|
||||||
if (hServerPipe == INVALID_HANDLE_VALUE) return 0;
|
|
||||||
|
|
||||||
char* buffer = (char*)buffer_ptr;
|
|
||||||
DWORD bytesRead = 0;
|
|
||||||
|
|
||||||
if (!fPendingIO) {
|
|
||||||
// Start an asynchronous listen operation
|
|
||||||
if (!ConnectNamedPipe(hServerPipe, &oOverlap)) {
|
|
||||||
DWORD err = GetLastError();
|
|
||||||
if (err == ERROR_IO_PENDING) {
|
|
||||||
fPendingIO = TRUE;
|
|
||||||
} else if (err == ERROR_PIPE_CONNECTED) {
|
|
||||||
// Client already connected before we listened!
|
|
||||||
SetEvent(oOverlap.hEvent);
|
|
||||||
fPendingIO = TRUE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fPendingIO) {
|
|
||||||
// Check if the connection event has triggered without waiting (0ms timeout)
|
|
||||||
if (WaitForSingleObject(oOverlap.hEvent, 0) == WAIT_OBJECT_0) {
|
|
||||||
DWORD cbRet = 0;
|
|
||||||
// Connection is ready, let's see if there is data
|
|
||||||
if (GetOverlappedResult(hServerPipe, &oOverlap, &cbRet, FALSE)) {
|
|
||||||
DWORD bytesAvail = 0;
|
|
||||||
if (PeekNamedPipe(hServerPipe, NULL, 0, NULL, &bytesAvail, NULL) && bytesAvail > 0) {
|
|
||||||
// Data has arrived, read it synchronously
|
|
||||||
BOOL success = ReadFile(hServerPipe, buffer, max_len - 1, &bytesRead, NULL);
|
|
||||||
if (success && bytesRead > 0) {
|
|
||||||
buffer[bytesRead] = '\0';
|
|
||||||
|
|
||||||
// Clean up and reset for the next client connection
|
|
||||||
DisconnectNamedPipe(hServerPipe);
|
|
||||||
ResetEvent(oOverlap.hEvent);
|
|
||||||
fPendingIO = FALSE;
|
|
||||||
return (int)bytesRead;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Client disconnected or pipe broke before sending
|
|
||||||
DisconnectNamedPipe(hServerPipe);
|
|
||||||
ResetEvent(oOverlap.hEvent);
|
|
||||||
fPendingIO = FALSE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ipc_send_message(const char* message) {
|
|
||||||
HANDLE hPipe = CreateFile(
|
|
||||||
"\\\\.\\pipe\\cimp_ipc",
|
|
||||||
GENERIC_WRITE,
|
|
||||||
0,
|
|
||||||
NULL,
|
|
||||||
OPEN_EXISTING,
|
|
||||||
0,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
if (hPipe != INVALID_HANDLE_VALUE) {
|
|
||||||
DWORD bytesWritten = 0;
|
|
||||||
WriteFile(hPipe, message, strlen(message), &bytesWritten, NULL);
|
|
||||||
CloseHandle(hPipe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ipc_cleanup() {
|
|
||||||
if (hServerPipe != INVALID_HANDLE_VALUE) {
|
|
||||||
DisconnectNamedPipe(hServerPipe);
|
|
||||||
CloseHandle(hServerPipe);
|
|
||||||
hServerPipe = INVALID_HANDLE_VALUE;
|
|
||||||
}
|
|
||||||
if (oOverlap.hEvent != NULL) {
|
|
||||||
CloseHandle(oOverlap.hEvent);
|
|
||||||
oOverlap.hEvent = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
static int server_fd = -1;
|
|
||||||
static char socket_path[256] = "";
|
|
||||||
|
|
||||||
static void get_socket_path() {
|
|
||||||
if (socket_path[0] == '\0') {
|
|
||||||
uid_t uid = getuid();
|
|
||||||
snprintf(socket_path, sizeof(socket_path), "/tmp/cimp_ipc_%u.sock", (unsigned int)uid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int terminkey() {
|
int terminkey() {
|
||||||
struct termios oldt, newt;
|
struct termios oldt, newt;
|
||||||
int ch;
|
int ch;
|
||||||
|
|
@ -265,102 +126,4 @@ int termwidth() {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ipc_init() {
|
|
||||||
get_socket_path();
|
|
||||||
|
|
||||||
int test_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
||||||
if (test_fd >= 0) {
|
|
||||||
struct sockaddr_un addr;
|
|
||||||
memset(&addr, 0, sizeof(addr));
|
|
||||||
addr.sun_family = AF_UNIX;
|
|
||||||
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
|
|
||||||
if (connect(test_fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) {
|
|
||||||
close(test_fd);
|
|
||||||
return 0; // Already running
|
|
||||||
}
|
|
||||||
close(test_fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
unlink(socket_path);
|
|
||||||
|
|
||||||
server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
||||||
if (server_fd < 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int flags = fcntl(server_fd, F_GETFL, 0);
|
|
||||||
fcntl(server_fd, F_SETFL, flags | O_NONBLOCK);
|
|
||||||
|
|
||||||
struct sockaddr_un addr;
|
|
||||||
memset(&addr, 0, sizeof(addr));
|
|
||||||
addr.sun_family = AF_UNIX;
|
|
||||||
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
|
|
||||||
|
|
||||||
if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
|
||||||
close(server_fd);
|
|
||||||
server_fd = -1;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listen(server_fd, 5) < 0) {
|
|
||||||
close(server_fd);
|
|
||||||
server_fd = -1;
|
|
||||||
unlink(socket_path);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ipc_check_message(uintptr_t buffer_ptr, int max_len) {
|
|
||||||
if (server_fd < 0) return 0;
|
|
||||||
|
|
||||||
char* buffer = (char*)buffer_ptr;
|
|
||||||
int client_fd = accept(server_fd, NULL, NULL);
|
|
||||||
if (client_fd < 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int total_read = 0;
|
|
||||||
while (total_read < max_len - 1) {
|
|
||||||
int r = read(client_fd, buffer + total_read, max_len - 1 - total_read);
|
|
||||||
if (r <= 0) break;
|
|
||||||
total_read += r;
|
|
||||||
}
|
|
||||||
buffer[total_read] = '\0';
|
|
||||||
close(client_fd);
|
|
||||||
return total_read;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ipc_send_message(const char* message) {
|
|
||||||
get_socket_path();
|
|
||||||
int client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
||||||
if (client_fd < 0) return;
|
|
||||||
|
|
||||||
struct sockaddr_un addr;
|
|
||||||
memset(&addr, 0, sizeof(addr));
|
|
||||||
addr.sun_family = AF_UNIX;
|
|
||||||
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
|
|
||||||
|
|
||||||
if (connect(client_fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) {
|
|
||||||
int total_written = 0;
|
|
||||||
int len = strlen(message);
|
|
||||||
while (total_written < len) {
|
|
||||||
int w = write(client_fd, message + total_written, len - total_written);
|
|
||||||
if (w <= 0) break;
|
|
||||||
total_written += w;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(client_fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ipc_cleanup() {
|
|
||||||
if (server_fd >= 0) {
|
|
||||||
close(server_fd);
|
|
||||||
server_fd = -1;
|
|
||||||
}
|
|
||||||
get_socket_path();
|
|
||||||
unlink(socket_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
44
test_ipc.bas
44
test_ipc.bas
|
|
@ -1,44 +0,0 @@
|
||||||
$console:only
|
|
||||||
declare library "terminkey"
|
|
||||||
function ipc_init&()
|
|
||||||
function ipc_check_message%(byval buf as _offset, byval max_len as long)
|
|
||||||
sub ipc_send_message(buf as string)
|
|
||||||
sub ipc_cleanup()
|
|
||||||
sub get_absolute_path(rel_path as string, byval abs_path as _offset, byval max_len as long)
|
|
||||||
end declare
|
|
||||||
|
|
||||||
dim r as long
|
|
||||||
r = ipc_init
|
|
||||||
print "ipc_init returned:"; r
|
|
||||||
|
|
||||||
if r = 1 then
|
|
||||||
print "We are the server! Waiting for a message..."
|
|
||||||
dim msg as string
|
|
||||||
msg = space$(100) + chr$(0)
|
|
||||||
dim start_time as double
|
|
||||||
start_time = timer
|
|
||||||
do while timer - start_time < 3
|
|
||||||
dim n as long
|
|
||||||
n = ipc_check_message(_offset(msg), 100)
|
|
||||||
if n > 0 then
|
|
||||||
print "Received: "; left$(msg, n)
|
|
||||||
exit do
|
|
||||||
end if
|
|
||||||
_limit 10
|
|
||||||
loop
|
|
||||||
ipc_cleanup
|
|
||||||
elseif r = 0 then
|
|
||||||
print "We are the client! Sending a message..."
|
|
||||||
ipc_send_message "Hello from client!"
|
|
||||||
else
|
|
||||||
print "Error initializing IPC"
|
|
||||||
end if
|
|
||||||
|
|
||||||
dim rel as string
|
|
||||||
rel = "readme.md"
|
|
||||||
dim abs_path as string
|
|
||||||
abs_path = space$(512) + chr$(0)
|
|
||||||
get_absolute_path rel, _offset(abs_path), 512
|
|
||||||
print "Absolute path of '"; rel; "' is: '"; _trim$(abs_path); "'"
|
|
||||||
|
|
||||||
system
|
|
||||||
12
todo.md
12
todo.md
|
|
@ -1,12 +0,0 @@
|
||||||
# todo
|
|
||||||
## ipc
|
|
||||||
add ipc functionality so it's possible while the program runs to:
|
|
||||||
- ~play next and previous track with `cimp --next` and `cimp --prev`~
|
|
||||||
- ~change volume with `cimp --volume xx`~
|
|
||||||
- ~add file to playlist with `cimp --add filename.ext`~
|
|
||||||
- if started without `--add` it should clear the playlist and load and play the given file or playlist.
|
|
||||||
- if given the flag `--playlist` it should output the song array as a playlist.
|
|
||||||
- start stop pause and quit would be good, repeat as well.
|
|
||||||
the c functions are ready in terminkey.h and an example of how it works is found in test_ipc.bas
|
|
||||||
|
|
||||||
## recurcive add all music in folder
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue