2023-02-24 22:34:35 -05:00
|
|
|
#ifdef HAVE_CONFIG_H
|
|
|
|
#include <config.h>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#include <fstream>
|
|
|
|
#include <iostream>
|
|
|
|
#include <cstdlib>
|
|
|
|
|
|
|
|
#include <alsa/asoundlib.h>
|
|
|
|
#include <xmp.h>
|
|
|
|
#include <ncurses.h>
|
|
|
|
|
2023-02-25 00:13:51 -05:00
|
|
|
#define VERSION "0.0.2.1"
|
2023-02-24 22:34:35 -05:00
|
|
|
#define SAMPLERATE 48000
|
|
|
|
#define BUFFER_SIZE 250000
|
|
|
|
|
|
|
|
static char *note_name[] = { "C ", "C#", "D ", "D#", "E ", "F ", "F#", "G ", "G#", "A ", "A#", "B " };
|
|
|
|
|
2023-02-25 00:03:32 -05:00
|
|
|
void updateTrack(char* name, char* type);
|
|
|
|
void renderRows(WINDOW* win, xmp_module_info *mi, xmp_frame_info *fi);
|
2023-02-25 04:23:11 -05:00
|
|
|
char getEffectType(int i);
|
2023-02-25 00:03:32 -05:00
|
|
|
|
2023-02-24 22:34:35 -05:00
|
|
|
static char *device = "default";
|
2023-02-25 04:23:11 -05:00
|
|
|
int chanOffset = 0; int detail = 1; int vol = 100;
|
2023-02-25 00:03:32 -05:00
|
|
|
bool looper = false, is_stopped = false;
|
2023-02-24 22:34:35 -05:00
|
|
|
int main(int argc, char *argv[]) {
|
2023-02-25 00:03:32 -05:00
|
|
|
printf("Trakker %s (with libxmp %s)\n", VERSION, xmp_version);
|
|
|
|
int time, key, err, row, pos;
|
|
|
|
bool fupdate = false;
|
2023-02-24 22:34:35 -05:00
|
|
|
snd_pcm_t *handle;
|
|
|
|
snd_pcm_sframes_t frames;
|
|
|
|
if ((err = snd_pcm_open(&handle, device, SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
|
|
|
|
printf("Playback open error: %s\n", snd_strerror(err));
|
|
|
|
exit(EXIT_FAILURE);
|
|
|
|
}
|
|
|
|
if ((err = snd_pcm_set_params(handle, SND_PCM_FORMAT_S16, SND_PCM_ACCESS_RW_INTERLEAVED, 2, SAMPLERATE, 1, BUFFER_SIZE)) < 0) {
|
|
|
|
printf("Playback open error: %s\n", snd_strerror(err));
|
|
|
|
exit(EXIT_FAILURE);
|
|
|
|
}
|
|
|
|
printf("Using Audio Driver: %s\n", "ALSA");
|
|
|
|
|
|
|
|
xmp_context c;
|
|
|
|
c = xmp_create_context();
|
|
|
|
if (xmp_load_module(c, argv[1]) != 0) {
|
|
|
|
fprintf(stderr, "Failed to load Module: %s\n", argv[1]);
|
|
|
|
exit(EXIT_FAILURE);
|
|
|
|
}
|
|
|
|
|
|
|
|
struct xmp_module_info mi;
|
|
|
|
struct xmp_frame_info fi;
|
|
|
|
|
|
|
|
printf("Loaded module: %s\n", argv[1]);
|
|
|
|
|
|
|
|
WINDOW *win;
|
|
|
|
initscr();
|
|
|
|
|
|
|
|
if (LINES < 5 || COLS < 5) {
|
|
|
|
endwin();
|
|
|
|
fprintf(stderr, "ncurses failed: Display is too small.\n");
|
|
|
|
exit(EXIT_FAILURE);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(has_colors() == FALSE) {
|
|
|
|
endwin();
|
|
|
|
fprintf(stderr, "ncurses failed: No color support.\n");
|
|
|
|
exit(EXIT_FAILURE);
|
|
|
|
}
|
|
|
|
start_color();
|
|
|
|
|
|
|
|
if (can_change_color() == TRUE) {
|
|
|
|
init_color(COLOR_BLACK, 0, 0, 0);
|
2023-02-25 00:03:32 -05:00
|
|
|
} else { printf("Color changing not supported!\n"); }
|
2023-02-24 22:34:35 -05:00
|
|
|
|
|
|
|
init_pair(1, COLOR_WHITE, COLOR_BLUE); // PLAYHEAD
|
2023-02-25 00:03:32 -05:00
|
|
|
init_pair(2, COLOR_WHITE, COLOR_RED); // PAUSED
|
2023-02-24 22:34:35 -05:00
|
|
|
|
|
|
|
cbreak();
|
|
|
|
noecho();
|
|
|
|
curs_set(0);
|
|
|
|
|
|
|
|
win = newwin(LINES-2, COLS-2, 1, 1);
|
|
|
|
keypad(win, TRUE);
|
|
|
|
wmove(win, 0, 0);
|
|
|
|
wprintw(win, "");
|
|
|
|
printf("Loaded ncurses display.\n");
|
|
|
|
|
|
|
|
refresh();
|
|
|
|
|
|
|
|
xmp_get_module_info(c, &mi);
|
|
|
|
row = pos = -1;
|
2023-02-25 00:03:32 -05:00
|
|
|
chanOffset = 0;
|
2023-02-24 22:34:35 -05:00
|
|
|
xmp_start_player(c, SAMPLERATE, 0);
|
|
|
|
|
2023-02-25 00:03:32 -05:00
|
|
|
updateTrack(mi.mod->name, mi.mod->type);
|
2023-02-24 22:34:35 -05:00
|
|
|
while (true) {
|
|
|
|
xmp_get_frame_info(c, &fi);
|
|
|
|
if (xmp_play_frame(c) != 0 && !is_stopped) break;
|
2023-02-25 00:03:32 -05:00
|
|
|
if (!looper && fi.loop_count > 0) break;
|
|
|
|
|
2023-02-24 22:34:35 -05:00
|
|
|
mvprintw(
|
|
|
|
0, COLS-11,
|
|
|
|
"%02u:%02u/%02u:%02u",
|
|
|
|
((fi.time / 1000) / 60) % 60,
|
|
|
|
(fi.time / 1000) % 60,
|
|
|
|
((fi.total_time / 1000) / 60) % 60,
|
|
|
|
(fi.total_time / 1000) % 60
|
|
|
|
);
|
|
|
|
|
2023-02-25 00:03:32 -05:00
|
|
|
keys:
|
|
|
|
wtimeout(win, is_stopped?-1:0);
|
2023-02-24 22:34:35 -05:00
|
|
|
if ((key = wgetch(win)) != 0) {
|
2023-02-25 00:03:32 -05:00
|
|
|
vol = xmp_get_player(c, XMP_PLAYER_VOLUME);
|
2023-02-24 22:34:35 -05:00
|
|
|
switch (key) {
|
|
|
|
case ' ': // Pause/Play
|
|
|
|
time = fi.time;
|
2023-02-25 00:03:32 -05:00
|
|
|
is_stopped = !is_stopped;
|
2023-02-24 22:34:35 -05:00
|
|
|
break;
|
|
|
|
case KEY_LEFT: // Move Channels Left
|
2023-02-25 00:03:32 -05:00
|
|
|
if (chanOffset > 0) chanOffset--;
|
2023-02-24 22:34:35 -05:00
|
|
|
break;
|
|
|
|
case KEY_RIGHT: // Move Channels Right
|
2023-02-25 00:03:32 -05:00
|
|
|
if (chanOffset < mi.mod->chn-1) chanOffset++;
|
2023-02-24 22:34:35 -05:00
|
|
|
break;
|
|
|
|
case KEY_UP: // Seek Up
|
2023-02-25 00:03:32 -05:00
|
|
|
if (is_stopped) fi.row--;
|
|
|
|
else xmp_set_position(c, fi.pos-1);
|
2023-02-24 22:34:35 -05:00
|
|
|
break;
|
|
|
|
case KEY_DOWN: // Seek Down
|
2023-02-25 00:03:32 -05:00
|
|
|
if (is_stopped) fi.row++;
|
|
|
|
else xmp_set_position(c, fi.pos+1);
|
2023-02-24 22:34:35 -05:00
|
|
|
break;
|
|
|
|
case '+':
|
|
|
|
if (vol < 100) xmp_set_player(c, XMP_PLAYER_VOLUME, vol+=5);
|
|
|
|
break;
|
|
|
|
case '-':
|
|
|
|
if (vol > 0) xmp_set_player(c, XMP_PLAYER_VOLUME, vol-=5);
|
|
|
|
break;
|
|
|
|
case 'l':
|
|
|
|
looper = !looper;
|
|
|
|
break;
|
2023-02-25 04:23:11 -05:00
|
|
|
case '0': detail = 0; break;
|
|
|
|
case '1': detail = 1; break;
|
|
|
|
case '2': detail = 2; break;
|
|
|
|
case '3': detail = 3; break;
|
|
|
|
case '4': detail = 4; break;
|
2023-02-24 22:34:35 -05:00
|
|
|
};
|
2023-02-25 00:03:32 -05:00
|
|
|
renderRows(win, &mi, &fi);
|
2023-02-24 22:34:35 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_stopped) {
|
|
|
|
frames = snd_pcm_bytes_to_frames(handle, fi.buffer_size);
|
|
|
|
if (snd_pcm_writei(handle, fi.buffer, frames) < 0) {
|
|
|
|
snd_pcm_prepare(handle);
|
|
|
|
}
|
|
|
|
|
2023-02-25 00:03:32 -05:00
|
|
|
if (fi.pos != pos) {
|
|
|
|
pos = fi.pos;
|
|
|
|
row = -1;
|
|
|
|
}
|
|
|
|
if (fi.row != row) {
|
|
|
|
renderRows(win, &mi, &fi);
|
|
|
|
row = fi.row;
|
|
|
|
fupdate = false;
|
2023-02-24 22:34:35 -05:00
|
|
|
}
|
2023-02-25 00:03:32 -05:00
|
|
|
} else goto keys;
|
2023-02-24 22:34:35 -05:00
|
|
|
}
|
|
|
|
printf("Closing ncurses...\n");
|
|
|
|
clrtoeol();
|
|
|
|
refresh();
|
|
|
|
endwin();
|
|
|
|
printf("Releasing libxmp...\n");
|
|
|
|
xmp_end_player(c);
|
|
|
|
xmp_release_module(c);
|
|
|
|
xmp_free_context(c);
|
|
|
|
|
|
|
|
printf("Shutting down audio driver...\n");
|
|
|
|
err = snd_pcm_drain(handle);
|
|
|
|
if (err < 0)
|
|
|
|
printf("snd_pcm_drain failed: %s\n", snd_strerror(err));
|
|
|
|
snd_pcm_close(handle);
|
|
|
|
return 0;
|
|
|
|
}
|
2023-02-25 00:03:32 -05:00
|
|
|
|
|
|
|
void updateTrack(char* name, char* type) {
|
|
|
|
mvprintw(
|
|
|
|
0, 0,
|
|
|
|
"%s (%s)",
|
|
|
|
name,
|
|
|
|
type
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
void renderRows(WINDOW* win, xmp_module_info *mi, xmp_frame_info *fi) {
|
|
|
|
werase(win);
|
2023-02-25 00:35:52 -05:00
|
|
|
move(LINES-1, 0);
|
|
|
|
wclrtoeol(stdscr);
|
2023-02-25 00:03:32 -05:00
|
|
|
mvprintw(
|
|
|
|
LINES-1, 0,
|
2023-02-25 00:35:52 -05:00
|
|
|
"[%c] PAT:%02x:%02x/%02x BPM:%02u SPD:%02u CHAN:%02u/%02u VOL:%03u d%i %s",
|
2023-02-25 00:03:32 -05:00
|
|
|
is_stopped?'x':'>',
|
|
|
|
fi->pos,
|
|
|
|
fi->pattern,
|
|
|
|
mi->mod->pat,
|
|
|
|
fi->bpm,
|
|
|
|
fi->speed,
|
2023-02-25 04:23:11 -05:00
|
|
|
chanOffset+1,
|
|
|
|
mi->mod->chn,
|
2023-02-25 00:35:52 -05:00
|
|
|
vol,
|
|
|
|
detail+1,
|
|
|
|
looper?"LOOP ":""
|
2023-02-25 00:03:32 -05:00
|
|
|
);
|
|
|
|
int view_chanOffset = chanOffset;
|
|
|
|
int view_detail = detail;
|
2023-02-25 04:23:11 -05:00
|
|
|
int dlvl;
|
|
|
|
if (detail == 4) dlvl = 19;
|
|
|
|
else if (detail == 3) dlvl = 15;
|
|
|
|
else if (detail == 2) dlvl = 11;
|
|
|
|
else if (detail == 1) dlvl = 7;
|
|
|
|
else dlvl = 2;
|
2023-02-25 00:03:32 -05:00
|
|
|
for (int y = 0; y < LINES - 2; y++) {
|
|
|
|
int trow = (fi->row - ((LINES - 2) / 2))+y;
|
2023-02-25 04:23:11 -05:00
|
|
|
if (trow > fi->num_rows-1 || trow < 0) { continue;}
|
2023-02-25 00:03:32 -05:00
|
|
|
if (trow == fi->row) {
|
|
|
|
wattron(win, COLOR_PAIR(is_stopped?2:1));
|
|
|
|
wattron(win, A_BOLD);
|
|
|
|
} else {
|
|
|
|
wattroff(win, COLOR_PAIR(is_stopped?2:1));
|
|
|
|
wattroff(win, A_BOLD);
|
|
|
|
}
|
|
|
|
wmove(win, y, 0);
|
|
|
|
wprintw(win, "%02X", trow);
|
|
|
|
int coff = ((COLS - 2) % dlvl);
|
|
|
|
int maxcol = -1;
|
|
|
|
for (int i = chanOffset; i < mi->mod->chn; i++) {
|
|
|
|
if (coff+(((i-chanOffset)+1)*dlvl)+dlvl > COLS) {
|
|
|
|
maxcol = i;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
wmove(win, y, coff+((i-chanOffset)*dlvl)+2);
|
|
|
|
int track = mi->mod->xxp[fi->pattern]->index[i];
|
|
|
|
struct xmp_event event = mi->mod->xxt[track]->event[trow];
|
|
|
|
if (i > 0 && i == chanOffset) wprintw(win, "<");
|
|
|
|
else wprintw(win, "|");
|
|
|
|
if (detail >= 1) {
|
2023-02-25 04:23:11 -05:00
|
|
|
if (event.note > 0x80) {
|
|
|
|
wprintw(win, "=== ");
|
|
|
|
} else if (event.note > 0) {
|
|
|
|
int note = event.note - 1;
|
|
|
|
wprintw(win, "%s%d ", note_name[note % 12], note / 12);
|
|
|
|
} else {
|
|
|
|
wprintw(win, "--- ");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.ins > 0) {
|
|
|
|
wprintw(win, "%02X", event.ins);
|
2023-02-25 00:03:32 -05:00
|
|
|
} else {
|
2023-02-25 04:23:11 -05:00
|
|
|
wprintw(win, "--");
|
2023-02-25 00:03:32 -05:00
|
|
|
}
|
2023-02-25 04:23:11 -05:00
|
|
|
|
2023-02-25 00:03:32 -05:00
|
|
|
if (detail >= 2) {
|
2023-02-25 04:23:11 -05:00
|
|
|
if (event.vol != 0) {
|
|
|
|
wprintw(win, " v%02i", event.vol-1);
|
2023-02-25 00:03:32 -05:00
|
|
|
} else {
|
|
|
|
wprintw(win, " ---");
|
|
|
|
}
|
2023-02-25 04:23:11 -05:00
|
|
|
|
2023-02-25 00:03:32 -05:00
|
|
|
if (detail >= 3) {
|
2023-02-25 04:23:11 -05:00
|
|
|
char f1;
|
|
|
|
if ((f1 = getEffectType(event.fxt)) != 0)
|
|
|
|
wprintw(win, " %c%02X", f1, event.fxp);
|
|
|
|
else
|
2023-02-25 00:03:32 -05:00
|
|
|
wprintw(win, " ---");
|
2023-02-25 04:23:11 -05:00
|
|
|
|
|
|
|
if (detail >= 4) {
|
|
|
|
char f2;
|
|
|
|
if ((f2 = getEffectType(event.fxt)) != 0)
|
|
|
|
wprintw(win, " %c%02X", f2, event.f2p);
|
|
|
|
else
|
|
|
|
wprintw(win, " ---");
|
2023-02-25 00:03:32 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-02-25 04:23:11 -05:00
|
|
|
} else {
|
|
|
|
if (event.note > 0x80) {
|
|
|
|
wprintw(win, "-");
|
|
|
|
} else if (event.note > 0) {
|
|
|
|
wprintw(win, "#");
|
|
|
|
} else {
|
|
|
|
wprintw(win, " ");
|
|
|
|
}
|
2023-02-25 00:03:32 -05:00
|
|
|
}
|
|
|
|
view_chanOffset = chanOffset;
|
|
|
|
view_detail = detail;
|
|
|
|
}
|
|
|
|
if (maxcol < mi->mod->chn && maxcol > 0) wprintw(win, ">");
|
|
|
|
else wprintw(win, "|");
|
|
|
|
}
|
|
|
|
refresh();
|
|
|
|
wrefresh(win);
|
2023-02-25 04:23:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
char getEffectType(int i) {
|
|
|
|
// The effect type characters are so strange to me.
|
|
|
|
// They make absolutely no sense in why it's setup this way.
|
|
|
|
// Maybe I'm mega stupid right now but this is all of the IT
|
|
|
|
// formats effects, so maybe this will cover everything enough.
|
|
|
|
// Broken, but not crashy-broken anymore.
|
|
|
|
switch(i) {
|
|
|
|
case 1: return 'F';
|
|
|
|
case 2: return 'E';
|
|
|
|
case 3: return 'G';
|
|
|
|
case 4: return 'H';
|
|
|
|
case 5: return 'L';
|
|
|
|
case 6: return 'K';
|
|
|
|
case 7: return 'R';
|
|
|
|
case 8: return 'X';
|
|
|
|
case 9: return 'O';
|
|
|
|
case 10: return 'D';
|
|
|
|
case 11: return 'B';
|
|
|
|
case 16: return 'V';
|
|
|
|
case 17: return 'W';
|
|
|
|
case 27: return 'Q';
|
|
|
|
case 29: return 'I';
|
|
|
|
case 128: return 'M';
|
|
|
|
case 129: return 'N';
|
|
|
|
case 132: return 'Z';
|
|
|
|
case 135: return 'T';
|
|
|
|
case 137: return 'P';
|
|
|
|
case 138: return 'Y';
|
|
|
|
case 142: return 'C';
|
|
|
|
case 163: return 'A';
|
|
|
|
case 172: return 'U';
|
|
|
|
case 180: return 'J';
|
|
|
|
}
|
2023-02-25 00:03:32 -05:00
|
|
|
}
|