Compare commits

..

No commits in common. "master" and "0.1" have entirely different histories.

5 changed files with 448 additions and 1047 deletions

1106
cimp.bas

File diff suppressed because it is too large Load diff

View file

@ -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 (0100) |
| `--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.
```
```

View file

@ -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

View file

@ -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
View file

@ -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