Start with z80 tracker

This commit is contained in:
2026-08-28 13:32:51 +02:00
parent e48d43e368
commit 76e820926c
12 changed files with 1405 additions and 0 deletions

303
tracker/src/emu76489.c Normal file
View File

@@ -0,0 +1,303 @@
/****************************************************************************
emu76489.c -- SN76489 emulator by Mitsutaka Okazaki 2001-2016
2001 08-13 : Version 1.00
2001 10-03 : Version 1.01 -- Added SNG_set_quality().
2004 05-23 : Version 1.10 -- Implemented GG stereo mode by RuRuRu
2004 06-07 : Version 1.20 -- Improved the noise emulation.
2015 12-13 : Version 1.21 -- Changed own integer types to C99 stdint.h types.
2016 09-06 : Version 1.22 -- Support per-channel output.
References:
SN76489 data sheet
sn76489.c -- from MAME
sn76489.txt -- from http://www.smspower.org/
*****************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "emu76489.h"
static uint32_t voltbl[16] = {
0xff, 0xcb, 0xa1, 0x80, 0x65, 0x50, 0x40, 0x33, 0x28, 0x20, 0x19, 0x14, 0x10, 0x0c, 0x0a, 0x00
};
#define GETA_BITS 24
static void
internal_refresh (SNG * sng)
{
if (sng->quality)
{
sng->base_incr = 1 << GETA_BITS;
sng->realstep = (uint32_t) ((1 << 31) / sng->rate);
sng->sngstep = (uint32_t) ((1 << 31) / (sng->clk / 16));
sng->sngtime = 0;
}
else
{
sng->base_incr = (uint32_t) ((double) sng->clk * (1 << GETA_BITS) / (16 * sng->rate));
}
}
void
SNG_set_rate (SNG * sng, uint32_t r)
{
sng->rate = r ? r : 44100;
internal_refresh (sng);
}
void
SNG_set_quality (SNG * sng, uint32_t q)
{
sng->quality = q;
internal_refresh (sng);
}
SNG *
SNG_new (uint32_t c, uint32_t r)
{
SNG *sng;
sng = (SNG *) malloc (sizeof (SNG));
if (sng == NULL)
return NULL;
sng->clk = c;
sng->rate = r ? r : 44100;
SNG_set_quality (sng, 0);
return sng;
}
void
SNG_reset (SNG * sng)
{
int i;
sng->base_count = 0;
for (i = 0; i < 3; i++)
{
sng->count[i] = 0;
sng->freq[i] = 0;
sng->edge[i] = 0;
sng->volume[i] = 0x0f;
sng->mute[i] = 0;
}
sng->adr = 0;
sng->noise_seed = 0x8000;
sng->noise_count = 0;
sng->noise_freq = 0;
sng->noise_volume = 0x0f;
sng->noise_mode = 0;
sng->noise_fref = 0;
sng->out = 0;
sng->stereo = 0xFF;
sng->ch_out[0] = sng->ch_out[1] = sng->ch_out[2] = sng->ch_out[3] = 0;
}
void
SNG_delete (SNG * sng)
{
free (sng);
}
void
SNG_writeIO (SNG * sng, uint32_t val)
{
if (val & 0x80)
{
sng->adr = (val & 0x70) >> 4;
switch (sng->adr)
{
case 0:
case 2:
case 4:
sng->freq[sng->adr >> 1] = (sng->freq[sng->adr >> 1] & 0x3F0) | (val & 0x0F);
break;
case 1:
case 3:
case 5:
sng->volume[(sng->adr - 1) >> 1] = val & 0xF;
break;
case 6:
sng->noise_mode = (val & 4) >> 2;
if ((val & 0x03) == 0x03) {
sng->noise_freq = sng->freq[2];
sng->noise_fref = 1;
} else {
sng->noise_freq = 32 << (val & 0x03);
sng->noise_fref = 0;
}
if (sng->noise_freq == 0)
sng->noise_freq = 1;
sng->noise_seed = 0x8000;
break;
case 7:
sng->noise_volume = val & 0x0f;
break;
}
}
else
{
sng->freq[sng->adr >> 1] = ((val & 0x3F) << 4) | (sng->freq[sng->adr >> 1] & 0x0F);
}
}
static inline int parity(int val) {
val^=val>>8;
val^=val>>4;
val^=val>>2;
val^=val>>1;
return val&1;
};
static inline void
update_output (SNG * sng)
{
int i;
uint32_t incr;
sng->base_count += sng->base_incr;
incr = (sng->base_count >> GETA_BITS);
sng->base_count &= (1 << GETA_BITS) - 1;
/* Noise */
sng->noise_count += incr;
if (sng->noise_count & 0x100)
{
if (sng->noise_mode) /* White */
sng->noise_seed = (sng->noise_seed>>1) | (parity(sng->noise_seed&0x0009)<<15);
else /* Periodic */
sng->noise_seed = (sng->noise_seed>>1) | ((sng->noise_seed&1)<<15);
if(sng->noise_fref)
sng->noise_count -= sng->freq[2];
else
sng->noise_count -= sng->noise_freq;
}
if (sng->noise_seed & 1)
{
sng->ch_out[3] += voltbl[sng->noise_volume] << 4;
}
sng->ch_out[3] >>= 1;
/* Tone */
for (i = 0; i < 3; i++)
{
sng->count[i] += incr;
if (sng->count[i] & 0x400)
{
if (sng->freq[i] > 1)
{
sng->edge[i] = !sng->edge[i];
sng->count[i] -= sng->freq[i];
}
else
{
sng->edge[i] = 1;
}
}
if (sng->edge[i] && !sng->mute[i])
{
sng->ch_out[i] += voltbl[sng->volume[i]] << 4;
}
sng->ch_out[i] >>= 1;
}
}
static inline int16_t
mix_output (SNG * sng)
{
sng->out = sng->ch_out[0] + sng->ch_out[1] + sng->ch_out[2] + sng->ch_out[3];
return (int16_t) sng->out;
}
int16_t
SNG_calc (SNG * sng)
{
if (!sng->quality) {
update_output(sng);
return mix_output(sng);
}
/* Simple rate converter */
while (sng->realstep > sng->sngtime)
{
sng->sngtime += sng->sngstep;
update_output(sng);
}
sng->sngtime = sng->sngtime - sng->realstep;
return mix_output(sng);
}
static inline void
mix_output_stereo (SNG * sng, int32_t out[2])
{
int i;
out[0] = out[1] = 0;
if((sng->stereo>>4)&0x08) {
out[0] += sng->ch_out[3];
}
if(sng->stereo & 0x08) {
out[1] += sng->ch_out[3];
}
for (i = 0; i < 3; i++)
{
if ((sng->stereo >> (i+4)) & 0x01) {
out[0] += sng->ch_out[i];
}
if ((sng->stereo >> i) & 0x01) {
out[1] += sng->ch_out[i];
}
}
}
void
SNG_calc_stereo (SNG *sng, int32_t out[2])
{
if (!sng->quality) {
update_output(sng);
mix_output_stereo(sng,out);
return;
}
while (sng->realstep > sng->sngtime)
{
sng->sngtime += sng->sngstep;
update_output(sng);
}
sng->sngtime = sng->sngtime - sng->realstep;
mix_output_stereo(sng,out);
return;
}
void
SNG_writeGGIO(SNG *sng, uint32_t val)
{
sng->stereo = val;
}

60
tracker/src/emu76489.h Normal file
View File

@@ -0,0 +1,60 @@
#ifndef _EMU76489_H_
#define _EMU76489_H_
#include "stdint.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct __SNG {
int32_t out ;
uint32_t clk, rate, base_incr, quality ;
uint32_t count[3] ;
uint32_t volume[3] ;
uint32_t freq[3] ;
uint32_t edge[3] ;
uint32_t mute[3] ;
uint32_t noise_seed ;
uint32_t noise_count ;
uint32_t noise_freq ;
uint32_t noise_volume ;
uint32_t noise_mode ;
uint32_t noise_fref ;
uint32_t base_count ;
/* rate converter */
uint32_t realstep ;
uint32_t sngtime ;
uint32_t sngstep ;
uint32_t adr ;
uint32_t stereo;
int16_t ch_out[4];
} SNG ;
SNG *SNG_new(uint32_t clk, uint32_t rate) ;
void SNG_delete(SNG *) ;
void SNG_reset(SNG *) ;
void SNG_set_rate(SNG *,uint32_t rate) ;
void SNG_set_quality(SNG *,uint32_t q) ;
void SNG_writeIO(SNG *SNG, uint32_t val) ;
uint8_t SNG_readReg(SNG *, uint32_t reg) ;
uint8_t SNG_readIO(SNG *SNG) ;
int16_t SNG_calc(SNG *) ;
void SNG_setVolumeMode(SNG *SNG, int type) ;
void SNG_calc_stereo(SNG *, int32_t out[2]) ;
void SNG_writeGGIO(SNG *SNG, uint32_t val) ;
#ifdef __cplusplus
}
#endif
#endif

211
tracker/src/main.c Normal file
View File

@@ -0,0 +1,211 @@
#include <stdio.h>
#include <stdint.h>
#include <errno.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <SDL2/SDL.h>
#include "emu76489.h"
#include "player.h"
#define SAMPLE_RATE 48000
#define MUSIC_HZ 50
typedef struct {
SNG *psg;
uint32_t samples_until_tick;
} AudioState;
SNG * g_psg = NULL;
typedef struct {
SDL_atomic_t running;
int old_stdin_flags;
int terminal_is_raw;
struct termios old_terminal;
} InputState;
void psg_write(uint8_t value){
SNG_writeIO(g_psg, value);
}
static int input_thread(void *userdata){
InputState *state = userdata;
while(SDL_AtomicGet(&state->running)){
uint8_t input;
ssize_t bytes_read = read(STDIN_FILENO, &input, sizeof(input));
if(bytes_read == 1){
input_interrupt(input);
}else if(bytes_read == 0 || errno == EAGAIN || errno == EWOULDBLOCK){
SDL_Delay(10);
}else if(errno != EINTR){
break;
}
}
return 0;
}
static int start_terminal_input(InputState *state){
SDL_zero(*state);
SDL_AtomicSet(&state->running, 1);
state->old_stdin_flags = fcntl(STDIN_FILENO, F_GETFL);
if(state->old_stdin_flags == -1 ||
fcntl(STDIN_FILENO, F_SETFL, state->old_stdin_flags | O_NONBLOCK) == -1){
return -1;
}
if(isatty(STDIN_FILENO)){
if(tcgetattr(STDIN_FILENO, &state->old_terminal) == -1){
fcntl(STDIN_FILENO, F_SETFL, state->old_stdin_flags);
return -1;
}
struct termios raw_terminal = state->old_terminal;
raw_terminal.c_lflag &= (tcflag_t) ~(ICANON | ECHO);
raw_terminal.c_cc[VMIN] = 0;
raw_terminal.c_cc[VTIME] = 0;
if(tcsetattr(STDIN_FILENO, TCSANOW, &raw_terminal) == -1){
fcntl(STDIN_FILENO, F_SETFL, state->old_stdin_flags);
return -1;
}
state->terminal_is_raw = 1;
}
return 0;
}
static void stop_terminal_input(InputState *state, SDL_Thread *thread){
SDL_AtomicSet(&state->running, 0);
SDL_WaitThread(thread, NULL);
if(state->terminal_is_raw){
tcsetattr(STDIN_FILENO, TCSANOW, &state->old_terminal);
}
fcntl(STDIN_FILENO, F_SETFL, state->old_stdin_flags);
}
void serial_write(char c){
write(STDOUT_FILENO, &c, 1);
}
static void audio_callback(void *userdata, Uint8 *stream, int len){
AudioState *state = userdata;
int16_t *output = (int16_t *)stream;
int sample_count = len / sizeof(int16_t);
for (int i = 0; i < sample_count; i++){
/*
* SAMPLE_RATE = 48000
* MUSIC_HZ = 50
*
* 48000 / 50 = 960
*
* Therefore music_tick() happens once every
* 960 generated audio samples.
*/
if(state->samples_until_tick == 0){
music_tick_interrupt();
state->samples_until_tick = SAMPLE_RATE / MUSIC_HZ;
}
/*
* Ask emu76489 to generate one PCM sample.
*/
output[i] = SNG_calc(state->psg);
state->samples_until_tick--;
}
}
int main(int argc, char ** argv){
(void) argc;
(void) argv;
if(SDL_Init(SDL_INIT_AUDIO) != 0){
fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
return 1;
}
g_psg = SNG_new(PSG_CLOCK, SAMPLE_RATE);
if(!g_psg){
fprintf(stderr, "Could not create SN76489 emulator\n");
SDL_Quit();
return 1;
}
SNG_reset(g_psg);
AudioState state = {
.psg = g_psg,
.samples_until_tick = 0
};
SDL_AudioSpec wanted;
SDL_zero(wanted);
wanted.freq = SAMPLE_RATE;
wanted.format = AUDIO_S16SYS;
wanted.channels = 1; /* mono */
wanted.samples = 1024;
wanted.callback = audio_callback;
wanted.userdata = &state;
SDL_AudioDeviceID device = SDL_OpenAudioDevice(
NULL,
0,
&wanted,
NULL,
0
);
if(device == 0){
fprintf(stderr, "SDL_OpenAudioDevice failed: %s\n", SDL_GetError());
SNG_delete(g_psg);
SDL_Quit();
return 1;
}
SDL_PauseAudioDevice(device, 0);
InputState input_state;
if(start_terminal_input(&input_state) != 0){
fprintf(stderr, "Could not configure terminal input\n");
SDL_CloseAudioDevice(device);
SNG_delete(g_psg);
SDL_Quit();
return 1;
}
SDL_Thread *keyboard_thread = SDL_CreateThread(
input_thread,
"terminal input",
&input_state
);
if(!keyboard_thread){
fprintf(stderr, "Could not create keyboard input thread: %s\n", SDL_GetError());
if(input_state.terminal_is_raw){
tcsetattr(STDIN_FILENO, TCSANOW, &input_state.old_terminal);
}
fcntl(STDIN_FILENO, F_SETFL, input_state.old_stdin_flags);
SDL_CloseAudioDevice(device);
SNG_delete(g_psg);
SDL_Quit();
return 1;
}
SDL_Delay(1000);
player_main();
stop_terminal_input(&input_state, keyboard_thread);
SDL_PauseAudioDevice(device, 1);
SDL_CloseAudioDevice(device);
SNG_delete(g_psg);
SDL_Quit();
return 0;
}

5
tracker/src/main.h Normal file
View File

@@ -0,0 +1,5 @@
#pragma once
#include <stdint.h>
void psg_write(uint8_t value);
void serial_write(char c);

110
tracker/src/notes.c Normal file
View File

@@ -0,0 +1,110 @@
#include <stdint.h>
#include "player.h"
#define NOTE_FREQUENCY(f) ((uint16_t)(PSG_CLOCK/(32*f)))
uint16_t note_periods[] = {
NOTE_FREQUENCY(16.35), // C-0
NOTE_FREQUENCY(17.32), // C#0
NOTE_FREQUENCY(18.35), // D-0
NOTE_FREQUENCY(19.45), // D#0
NOTE_FREQUENCY(20.60), // E-0
NOTE_FREQUENCY(21.83), // F-0
NOTE_FREQUENCY(23.12), // F#0
NOTE_FREQUENCY(24.50), // G-0
NOTE_FREQUENCY(25.96), // G#0
NOTE_FREQUENCY(27.50), // A-0
NOTE_FREQUENCY(29.14), // A#0
NOTE_FREQUENCY(30.87), // B-0
NOTE_FREQUENCY(32.70), // C-1
NOTE_FREQUENCY(34.65), // C#1
NOTE_FREQUENCY(36.71), // D-1
NOTE_FREQUENCY(38.89), // D#1
NOTE_FREQUENCY(41.20), // E-1
NOTE_FREQUENCY(43.65), // F-1
NOTE_FREQUENCY(46.25), // F#1
NOTE_FREQUENCY(49.00), // G-1
NOTE_FREQUENCY(51.91), // G#1
NOTE_FREQUENCY(55.00), // A-1
NOTE_FREQUENCY(58.27), // A#1
NOTE_FREQUENCY(61.74), // B-1
NOTE_FREQUENCY(65.41), // C-2
NOTE_FREQUENCY(69.30), // C#2
NOTE_FREQUENCY(73.42), // D-2
NOTE_FREQUENCY(77.78), // D#2
NOTE_FREQUENCY(82.41), // E-2
NOTE_FREQUENCY(87.31), // F-2
NOTE_FREQUENCY(92.50), // F#2
NOTE_FREQUENCY(98.00), // G-2
NOTE_FREQUENCY(103.83), // G#2
NOTE_FREQUENCY(110.00), // A-2
NOTE_FREQUENCY(116.54), // A#2
NOTE_FREQUENCY(123.47), // B-2
NOTE_FREQUENCY(130.81), // C-3
NOTE_FREQUENCY(138.59), // C#3
NOTE_FREQUENCY(146.83), // D-3
NOTE_FREQUENCY(155.56), // D#3
NOTE_FREQUENCY(164.81), // E-3
NOTE_FREQUENCY(174.61), // F-3
NOTE_FREQUENCY(185.00), // F#3
NOTE_FREQUENCY(196.00), // G-3
NOTE_FREQUENCY(207.65), // G#3
NOTE_FREQUENCY(220.00), // A-3
NOTE_FREQUENCY(233.08), // A#3
NOTE_FREQUENCY(246.94), // B-3
NOTE_FREQUENCY(261.63), // C-4
NOTE_FREQUENCY(277.18), // C#4
NOTE_FREQUENCY(293.66), // D-4
NOTE_FREQUENCY(311.13), // D#4
NOTE_FREQUENCY(329.63), // E-4
NOTE_FREQUENCY(349.23), // F-4
NOTE_FREQUENCY(369.99), // F#4
NOTE_FREQUENCY(392.00), // G-4
NOTE_FREQUENCY(415.30), // G#4
NOTE_FREQUENCY(440.00), // A-4
NOTE_FREQUENCY(466.16), // A#4
NOTE_FREQUENCY(493.88), // B-4
NOTE_FREQUENCY(523.25), // C-5
NOTE_FREQUENCY(554.37), // C#5
NOTE_FREQUENCY(587.33), // D-5
NOTE_FREQUENCY(622.25), // D#5
NOTE_FREQUENCY(659.26), // E-5
NOTE_FREQUENCY(698.46), // F-5
NOTE_FREQUENCY(739.99), // F#5
NOTE_FREQUENCY(783.99), // G-5
NOTE_FREQUENCY(830.61), // G#5
NOTE_FREQUENCY(880.00), // A-5
NOTE_FREQUENCY(932.33), // A#5
NOTE_FREQUENCY(987.77), // B-5
NOTE_FREQUENCY(1046.50), // C-6
NOTE_FREQUENCY(1108.73), // C#6
NOTE_FREQUENCY(1174.66), // D-6
NOTE_FREQUENCY(1244.51), // D#6
NOTE_FREQUENCY(1318.51), // E-6
NOTE_FREQUENCY(1396.91), // F-6
NOTE_FREQUENCY(1479.98), // F#6
NOTE_FREQUENCY(1567.98), // G-6
NOTE_FREQUENCY(1661.22), // G#6
NOTE_FREQUENCY(1760.00), // A-6
NOTE_FREQUENCY(1864.66), // A#6
NOTE_FREQUENCY(1975.53), // B-6
NOTE_FREQUENCY(2093.00), // C-7
NOTE_FREQUENCY(2217.46), // C#7
NOTE_FREQUENCY(2349.32), // D-7
NOTE_FREQUENCY(2489.02), // D#7
NOTE_FREQUENCY(2637.02), // E-7
NOTE_FREQUENCY(2793.83), // F-7
NOTE_FREQUENCY(2959.96), // F#7
NOTE_FREQUENCY(3135.96), // G-7
NOTE_FREQUENCY(3322.44), // G#7
NOTE_FREQUENCY(3520.00), // A-7
NOTE_FREQUENCY(3729.31), // A#7
NOTE_FREQUENCY(3951.07), // B-7
};

6
tracker/src/notes.h Normal file
View File

@@ -0,0 +1,6 @@
#pragma once
#include <stdint.h>
#define NR_NOTE_PERIODS 96
extern uint16_t note_periods[];

158
tracker/src/player.c Normal file
View File

@@ -0,0 +1,158 @@
#include "player.h"
#include <stdint.h>
#include <stdio.h>
#include "main.h"
#include "notes.h"
#include "ui.h"
typedef struct{
struct {
uint16_t tone;
uint8_t vol;
} channel[4];
} psg_state_t;
static uint8_t music_tick_interrupt_happened = 0;
static uint8_t input_interrupt_happened = 0;
static char input;
static uint8_t playing = 0;
static uint8_t tick = 0;
static uint8_t row = 0;
static pattern_t * pattern = NULL;
static psg_state_t psg_state = {
.channel = {
{ .tone = 0, .vol = 15 },
{ .tone = 0, .vol = 15 },
{ .tone = 0, .vol = 15 },
{ .tone = 0, .vol = 15 },
}
};
static void sn_set_tone(uint8_t channel, uint16_t period){
period &= 0x03ff;
uint8_t latch = 0x80 | ((channel & 3) << 5) | (period & 0x0f);
uint8_t data = (period >> 4) & 0x3f;
psg_write(latch);
psg_write(data);
}
static void sn_set_volume(uint8_t channel, uint8_t volume){
uint8_t command = 0x90 | ((channel & 3) << 5) | (volume & 0x0f);
psg_write(command);
}
static void flush_psg(){
for(uint8_t chan=0; chan<3; chan++){
sn_set_tone(chan, psg_state.channel[chan].tone);
sn_set_volume(chan, psg_state.channel[chan].vol);
}
sn_set_volume(3, psg_state.channel[3].vol);
}
static void process_row(){
row_t * current = &pattern->rows[row];
for(uint8_t chan=0; chan<3; chan++){
if(current->channel[chan].note < 12 && current->channel[chan].note >= NR_NOTE_PERIODS+12){
// Only turn off note if it isnt 0
if(current->channel[chan].note != 0)
psg_state.channel[chan].vol = 15;
}else{
psg_state.channel[chan].tone = note_periods[current->channel[chan].note-12];
psg_state.channel[chan].vol = 15-current->channel[chan].volume;
}
}
}
static void next_pattern(){
playing = 0;
}
static void perform_music_tick(){
if(!playing || !pattern)
return;
flush_psg();
tick++;
if(tick>=pattern->speed){
tick = 0;
row ++;
if(row>=PATTERN_ROWS){
row = 0;
next_pattern();
}
process_row();
}
}
// 50Hz music tick interrupt
void music_tick_interrupt(){
music_tick_interrupt_happened = 1;
}
// Input interrupt
void input_interrupt(char _input){
input = _input;
input_interrupt_happened = 1;
}
void player_main(){
flush_psg();
ui_init();
pattern_t p = {
.speed = 12,
.rows = {
{.channel = {{60, 15}, {0, 0}, {0, 0}, {0, 0}}},
{.channel = {{67, 15}, {0, 0}, {0, 0}, {0, 0}}},
{.channel = {{69, 15}, {0, 0}, {0, 0}, {0, 0}}},
{.channel = {{67, 15}, {0, 0}, {0, 0}, {0, 0}}},
{.channel = {{60, 15}, {0, 0}, {0, 0}, {0, 0}}},
}
};
pattern = &p;
ui_set_pattern(&p);
for(;;){
if(music_tick_interrupt_happened){
music_tick_interrupt_happened = 0;
perform_music_tick();
}
if(input_interrupt_happened){
input_interrupt_happened = 0;
ui_input(input);
}
uint8_t action = do_ui();
if(action != ACTION_NONE){
if(action == ACTION_PLAY_ONCE){
tick = 0;
row = 0;
process_row();
flush_psg();
playing = 1;
}
if(action == ACTION_STOP){
playing = 0;
tick = 0;
row = 0;
psg_state = (psg_state_t){
.channel = {
{ .tone = 0, .vol = 15 },
{ .tone = 0, .vol = 15 },
{ .tone = 0, .vol = 15 },
{ .tone = 0, .vol = 15 },
}
};
flush_psg();
}
if(action == ACTION_QUIT){
break;
}
}
}
}

23
tracker/src/player.h Normal file
View File

@@ -0,0 +1,23 @@
#pragma once
#include <stdint.h>
#define PSG_CLOCK 1000000
#define PATTERN_ROWS 16
typedef struct{
uint8_t note;
uint8_t volume;
} cell_t;
typedef struct{
cell_t channel[4];
} row_t;
typedef struct{
row_t rows[PATTERN_ROWS];
uint8_t speed;
} pattern_t;
void player_main();
void music_tick_interrupt();
void input_interrupt(char input);

472
tracker/src/ui.c Normal file
View File

@@ -0,0 +1,472 @@
#include "ui.h"
#include <stddef.h>
#include "main.h"
#include "player.h"
#define ROWS 25
#define COLS 80
#define TRACK_ROWS 16
#define CHANNELS 4
static uint8_t dirty = 1;
static uint8_t clear = 1;
static uint8_t cursor_row = 0;
static uint8_t cursor_channel = 0;
static uint8_t input_state = 0;
static uint8_t input_octave = 4;
static uint8_t do_action = ACTION_NONE;
static pattern_t * pattern = NULL;
static void serial_puts(char * s){
while(*s!='\0'){
serial_write(*s);
s++;
}
}
static void cursor_set(uint8_t x, uint8_t y){
// Buffer for "\x1b[yyy;xxxH\0" -> max 12 bytes needed
char buf[12];
char *p = buf;
// 1. Write ANSI prefix
*p++ = 0x1B; // ESC
*p++ = '[';
// 2. Convert Y (Row) to ASCII digits
if (y >= 100) {
*p++ = '0' + (y / 100);
y %= 100;
*p++ = '0' + (y / 10);
} else if (y >= 10) {
*p++ = '0' + (y / 10);
}
*p++ = '0' + (y % 10);
// 3. Write separator
*p++ = ';';
// 4. Convert X (Col) to ASCII digits
if (x >= 100) {
*p++ = '0' + (x / 100);
x %= 100;
*p++ = '0' + (x / 10);
} else if (x >= 10) {
*p++ = '0' + (x / 10);
}
*p++ = '0' + (x % 10);
// 5. Write ANSI suffix and null-terminator
*p++ = 'H';
*p = '\0';
// 6. Send via your serial routine
serial_puts(buf);
}
static void cls(){
serial_puts("\033[2J");
cursor_set(1, 1);
}
static void reverse_on(void){
serial_puts("\x1b[7m");
}
static void reverse_off(void){
serial_puts("\x1b[0m");
}
static void cursor_hide(void){
serial_puts("\x1b[?25l");
}
static void cursor_show(void){
serial_puts("\x1b[?25h");
}
static void print_hex_digit(uint8_t n){
n &= 0x0f;
if (n < 10)
serial_write('0' + n);
else
serial_write('A' + (n - 10));
}
static void print_hex_byte(uint8_t n){
print_hex_digit(n >> 4);
print_hex_digit(n);
}
static void print_note(uint8_t note){
static const char note_names[12][2] = {
{'C', '-'},
{'C', '#'},
{'D', '-'},
{'D', '#'},
{'E', '-'},
{'F', '-'},
{'F', '#'},
{'G', '-'},
{'G', '#'},
{'A', '-'},
{'A', '#'},
{'B', '-'}
};
uint8_t n;
uint8_t octave;
if (note == 0) {
serial_puts("---");
return;
}
if (note == 0xff) {
serial_puts("OFF");
return;
}
note -= 12;
n = note % 12;
octave = (note / 12);
serial_write(note_names[n][0]);
serial_write(note_names[n][1]);
if (octave < 10)
serial_write('0' + octave);
else
serial_write('?');
}
static void draw_cell(uint8_t row, uint8_t channel){
cell_t *cell = &pattern->rows[row].channel[channel];
if (row == cursor_row && channel == cursor_channel)
reverse_on();
print_note(cell->note);
serial_write(' ');
// if (cell->instrument == 0) {
serial_puts("--");
// }
// else {
// print_hex_byte(cell->instrument);
// }
if (row == cursor_row && channel == cursor_channel)
reverse_off();
}
static void draw_row(uint8_t row){
uint8_t channel;
print_hex_byte(row);
serial_puts(" ");
for (channel = 0; channel < CHANNELS; channel++) {
draw_cell(row, channel);
if (channel != CHANNELS - 1)
serial_puts(" ");
}
serial_puts("\r\n");
}
static void draw(void){
uint8_t row;
if (clear) {
cls();
clear = 0;
}else {
cursor_set(1, 1);
}
cursor_hide();
serial_puts("Z80 TRACKER\r\n");
serial_puts("O [");
print_hex_digit(input_octave);
serial_puts("]\r\n");
serial_puts(" CH1 CH2 CH3 NOISE\r\n");
serial_puts(" ------- ------- ------- -------\r\n");
for (row = 0; row < TRACK_ROWS; row++) {
draw_row(row);
}
serial_puts("\r\n");
serial_puts("Arrows: move A-K: notes X: clear Space: off\r\n");
serial_puts("P: play O: stop Q: quit Numbers: oct\r\n");
/*
* Keep the real terminal cursor out of the tracker area.
*/
cursor_set(1, 25);
}
static void move_up(void){
if (cursor_row > 0)
cursor_row--;
dirty = 1;
}
static void move_down(void){
if (cursor_row < TRACK_ROWS - 1)
cursor_row++;
dirty = 1;
}
static void move_left(void){
if (cursor_channel > 0)
cursor_channel--;
dirty = 1;
}
static void move_right(void){
if (cursor_channel < CHANNELS - 1)
cursor_channel++;
dirty = 1;
}
/*
* Very simple piano mapping:
*
* W E T Y U
* C#D# F#G#A#
*
* A S D F G H J K
* C D E F G A B C
*
* This currently enters octave 4.
*/
static uint8_t key_to_note(char key){
uint8_t semitone;
switch (key) {
case 'a': case 'A': semitone = 0; break; /* C */
case 'w': case 'W': semitone = 1; break; /* C# */
case 's': case 'S': semitone = 2; break; /* D */
case 'e': case 'E': semitone = 3; break; /* D# */
case 'd': case 'D': semitone = 4; break; /* E */
case 'f': case 'F': semitone = 5; break; /* F */
case 't': case 'T': semitone = 6; break; /* F# */
case 'g': case 'G': semitone = 7; break; /* G */
case 'y': case 'Y': semitone = 8; break; /* G# */
case 'h': case 'H': semitone = 9; break; /* A */
case 'u': case 'U': semitone = 10; break; /* A# */
case 'j': case 'J': semitone = 11; break; /* B */
case 'k': case 'K': semitone = 12; break; /* C */
default:
return 0;
}
return 12+(input_octave*12) + semitone;
}
static void enter_note(uint8_t note){
cell_t *cell = &pattern->rows[cursor_row].channel[cursor_channel];
cell->note = note;
cell->volume = 15;
/*
* Just give every entered note instrument 1 for now.
*/
//if (cell->instrument == 0)
// cell->instrument = 1;
/*
* Tracker-style behavior: advance after entering a note.
*/
if (cursor_row < TRACK_ROWS - 1)
cursor_row++;
dirty = 1;
}
void ui_input(char input){
uint8_t note;
/*
* State 0:
* normal keyboard input.
*/
if (input_state == 0) {
if ((uint8_t)input == 0x1b) {
input_state = 1;
return;
}
/*
* Clear cell.
*/
if (input == 'x' || input == 'X') {
pattern->rows[cursor_row].channel[cursor_channel].note = 0;
dirty = 1;
return;
}
/*
* Space = note off.
*/
if (input == ' ') {
pattern->rows[cursor_row].channel[cursor_channel].note = 0xff;
if (cursor_row < TRACK_ROWS - 1)
cursor_row++;
dirty = 1;
return;
}
/*
* P = play once
*/
if (input == 'p' || input=='P') {
do_action = ACTION_PLAY_ONCE;
return;
}
/*
* O = stop
*/
if (input == 'o' || input=='O') {
do_action = ACTION_STOP;
return;
}
/*
* Q = quit
*/
if (input == 'q' || input=='Q') {
do_action = ACTION_QUIT;
return;
}
/*
* 0-7 = octaves
*/
if (input >= '0' && input<='7') {
input_octave = input-'0';
dirty = 1;
return;
}
/*
* Piano keyboard.
*/
note = key_to_note(input);
if (note != 0) {
enter_note(note);
}
return;
}
/*
* State 1:
*
* We received ESC.
*
* Arrow keys normally send '[' next.
*/
if (input_state == 1) {
if (input == '[') {
input_state = 2;
}
else {
/*
* Unknown escape sequence.
*/
input_state = 0;
}
return;
}
/*
* State 2:
*
* ESC [
*
* Last byte tells us which arrow.
*/
if (input_state == 2) {
switch (input) {
case 'A':
move_up();
break;
case 'B':
move_down();
break;
case 'C':
move_right();
break;
case 'D':
move_left();
break;
}
input_state = 0;
}
}
uint8_t do_ui(){
if(dirty){
dirty = 0;
draw();
}
if(do_action != ACTION_NONE){
uint8_t action = do_action;
do_action = ACTION_NONE;
return action;
}
return ACTION_NONE;
}
void ui_set_pattern(pattern_t * _pattern){
pattern = _pattern;
}
void ui_init(){
uint8_t row;
uint8_t channel;
dirty = 1;
clear = 1;
cursor_row = 0;
cursor_channel = 0;
input_state = 0;
}

13
tracker/src/ui.h Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include <stdint.h>
#include "player.h"
#define ACTION_NONE 0
#define ACTION_PLAY_ONCE 1
#define ACTION_STOP 2
#define ACTION_QUIT 3
void ui_input(char input);
uint8_t do_ui();
void ui_init();
void ui_set_pattern(pattern_t * _pattern);