diff --git a/tracker/.gitignore b/tracker/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/tracker/.gitignore @@ -0,0 +1 @@ +build diff --git a/tracker/Makefile b/tracker/Makefile new file mode 100644 index 0000000..8b34f8b --- /dev/null +++ b/tracker/Makefile @@ -0,0 +1,43 @@ +TARGET := tracker + +BUILDDIR := build +.DEFAULT_GOAL := all + +SRCDIRS := src +SRCFILES := $(shell find $(SRCDIRS) -type f -name "*.c") +OBJFILES := $(SRCFILES:%.c=$(BUILDDIR)/%.c.o) +DEPFILES := $(SRCFILES:%.c=$(BUILDDIR)/%.c.d) + +CC := gcc +CFLAGS := -Wall -Wextra +CPPFLAGS := -I src $(shell pkg-config --cflags sdl2) +LDLIBS := $(shell pkg-config --libs sdl2) + +.PHONY: $(TARGET) all clean +all: $(TARGET) + +# PHONY RULES +# ----------- + +$(TARGET): $(BUILDDIR)/$(TARGET) + +clean: + echo CLEAN FILES FOR $(TARGET) + rm -rf $(BUILDDIR) + +# SPECIFIC BUILD RULES +# -------------------- + +$(BUILDDIR)/$(TARGET): $(OBJFILES) + echo 'LD ' $@ + mkdir -p $(@D) + $(CC) $(LDFLAGS) -o $@ $(OBJFILES) $(LDLIBS) + +$(OBJFILES): $(BUILDDIR)/%.c.o: %.c + echo 'CC ' $@ + mkdir -p $(@D) + $(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -o $@ -c $< + +# Header dependencies generated by -MMD. Keep this after the real targets so +# included files can never accidentally become Make's default goal. +-include $(DEPFILES) diff --git a/tracker/src/emu76489.c b/tracker/src/emu76489.c new file mode 100644 index 0000000..0ba3b33 --- /dev/null +++ b/tracker/src/emu76489.c @@ -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 +#include +#include +#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; +} \ No newline at end of file diff --git a/tracker/src/emu76489.h b/tracker/src/emu76489.h new file mode 100644 index 0000000..a6b4ca5 --- /dev/null +++ b/tracker/src/emu76489.h @@ -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 \ No newline at end of file diff --git a/tracker/src/main.c b/tracker/src/main.c new file mode 100644 index 0000000..9e0c17b --- /dev/null +++ b/tracker/src/main.c @@ -0,0 +1,211 @@ +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/tracker/src/main.h b/tracker/src/main.h new file mode 100644 index 0000000..756e0d0 --- /dev/null +++ b/tracker/src/main.h @@ -0,0 +1,5 @@ +#pragma once +#include + +void psg_write(uint8_t value); +void serial_write(char c); \ No newline at end of file diff --git a/tracker/src/notes.c b/tracker/src/notes.c new file mode 100644 index 0000000..fd3f9fb --- /dev/null +++ b/tracker/src/notes.c @@ -0,0 +1,110 @@ +#include +#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 +}; \ No newline at end of file diff --git a/tracker/src/notes.h b/tracker/src/notes.h new file mode 100644 index 0000000..c19be02 --- /dev/null +++ b/tracker/src/notes.h @@ -0,0 +1,6 @@ +#pragma once +#include + +#define NR_NOTE_PERIODS 96 + +extern uint16_t note_periods[]; \ No newline at end of file diff --git a/tracker/src/player.c b/tracker/src/player.c new file mode 100644 index 0000000..4a3abf6 --- /dev/null +++ b/tracker/src/player.c @@ -0,0 +1,158 @@ +#include "player.h" + +#include +#include + +#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; + } + } + } + +} \ No newline at end of file diff --git a/tracker/src/player.h b/tracker/src/player.h new file mode 100644 index 0000000..b7dc56a --- /dev/null +++ b/tracker/src/player.h @@ -0,0 +1,23 @@ +#pragma once +#include + +#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); \ No newline at end of file diff --git a/tracker/src/ui.c b/tracker/src/ui.c new file mode 100644 index 0000000..bf1e4af --- /dev/null +++ b/tracker/src/ui.c @@ -0,0 +1,472 @@ +#include "ui.h" + +#include + +#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; +} \ No newline at end of file diff --git a/tracker/src/ui.h b/tracker/src/ui.h new file mode 100644 index 0000000..88cabb2 --- /dev/null +++ b/tracker/src/ui.h @@ -0,0 +1,13 @@ +#pragma once +#include +#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); \ No newline at end of file