/* aputoy.c - GUI for playing with the GameBoy APU.
**
** This tool allows for configuring all the options for each channel and
** playing back samples. Channels where the length or sweep is active in a way
** that would cause the channel to automatically shut off can activate a lock
** which immediately reactivates the channel when it is shut off, allowing for
** the channel to be replayed easily. Register values can be easily copied to
** the clipboard if you find a sound you like.
*/
#include <errno.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
/* Internal */
#include "apu.h"
/* SDL */
#include <SDL.h>
/* stb */
#if 0
#define STB_TRUETYPE_IMPLEMENTATION
#include "stb_truetype.h"
#endif
/* Common */
#define COMMON_ARENA_IMPLEMENTATION
#include "arena.h"
#include "sdl_helpers.h"
#include "fixed_font.h"
/* Emscripten */
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
#define PERM_ARENA_SIZE 1024*1024*1024
#define WINDOW_WIDTH 1024
#define WINDOW_HEIGHT 768
#define min(A, B) (((A) < (B)) ? (A) : (B))
#define max(A, B) (((A) > (B)) ? (A) : (B))
typedef enum Text_Alignment {
TEXTALIGNMENT_left,
TEXTALIGNMENT_center,
TEXTALIGNMENT_right
} Text_Alignment;
typedef enum Pan {
PAN_left,
PAN_center,
PAN_right
} Pan;
typedef enum Sprite_Type {
SPRITETYPE_power,
SPRITETYPE_square_wave,
SPRITETYPE_waveform,
SPRITETYPE_noise,
SPRITETYPE_up,
SPRITETYPE_down,
SPRITETYPE_play,
SPRITETYPE_locked,
SPRITETYPE_unlocked,
SPRITETYPE_tap,
SPRITETYPE_clipboard,
SPRITETYPE_COUNT
} Sprite_Type;
typedef enum RectCut_Side {
RECTCUTSIDE_left,
RECTCUTSIDE_right,
RECTCUTSIDE_top,
RECTCUTSIDE_bottom
} RectCut_Side;
typedef struct RectCut {
SDL_Rect *rect;
RectCut_Side side;
} RectCut;
typedef struct String {
char *data;
uint32_t size; /* Total size of data */
uint32_t len; /* Size of data used */
} String;
typedef struct Button_State {
bool just_pressed;
bool just_released;
bool down;
} Button_State;
typedef struct Input_State {
char text[5];
uint32_t integer;
bool is_editing;
} Input_State;
typedef struct List_Option {
char *name;
int32_t value;
} List_Option;
typedef struct Sprite_List_Option {
Sprite_Type sprite;
int32_t value;
} Sprite_List_Option;
typedef struct Editor_State {
bool is_running;
Arena *a;
SDL_Renderer *r;
SDL_Window *w;
SDL_Texture *sprites;
SDL_AudioDeviceID dev;
struct {
float size;
int32_t ascent;
int32_t descent;
int32_t line_gap;
SDL_Texture *tex;
#if 0
stbtt_bakedchar *cdata;
#else
Fixed_Font_Kerning *cdata;
#endif
} font;
struct {
SDL_Point pos;
SDL_Point prev_pos;
Button_State left;
} mouse;
struct {
bool has_input;
char text[32];
} text_input;
struct {
int32_t l_volume;
int32_t r_volume;
struct {
bool is_locked;
int32_t wavelength;
int32_t duty_cycle;
int32_t env_dir;
int32_t env_pace;
int32_t env_start_vol;
int32_t sweep_dir;
int32_t sweep_pace;
int32_t sweep_slope;
int32_t length;
} ch1;
struct {
bool is_locked;
int32_t wavelength;
int32_t duty_cycle;
int32_t env_dir;
int32_t env_pace;
int32_t env_start_vol;
int32_t length;
} ch2;
struct {
bool is_locked;
int32_t wavelength;
int32_t length;
int32_t volume;
Input_State samples[16];
} ch3;
struct {
bool is_locked;
int32_t shift;
int32_t divisor;
int32_t width;
int32_t length;
int32_t env_dir;
int32_t env_pace;
int32_t env_start_vol;
} ch4;
APU apu;
} internal;
} Editor_State;
static SDL_Rect sprite_table[SPRITETYPE_COUNT] = {
[SPRITETYPE_power] = {0, 0, 16, 16},
[SPRITETYPE_square_wave] = {16, 0, 64, 16},
[SPRITETYPE_waveform] = {16, 16, 64, 16},
[SPRITETYPE_noise] = {16, 32, 64, 16},
[SPRITETYPE_up] = {80, 0, 16, 16},
[SPRITETYPE_down] = {96, 0, 16, 16},
[SPRITETYPE_play] = {80, 16, 16, 16},
[SPRITETYPE_locked] = {80, 32, 16, 16},
[SPRITETYPE_unlocked] = {96, 32, 16, 16},
[SPRITETYPE_tap] = {0, 16, 16, 16},
[SPRITETYPE_clipboard] = {112, 16, 16, 16},
};
static List_Option duty_cycle_options[4] = {
[0] = {"12.5%", DUTYCYCLE_12_5},
[1] = {"25%", DUTYCYCLE_25},
[2] = {"50%", DUTYCYCLE_50},
[3] = {"75%", DUTYCYCLE_75}
};
static List_Option lfsr_width_options[2] = {
[0] = {"15-bit", LFSRWIDTH_15},
[1] = {"7-bit", LFSRWIDTH_7},
};
static List_Option waveform_volume_options[4] = {
[0] = {"0%", WAVEFORMVOLUME_0},
[1] = {"25%", WAVEFORMVOLUME_25},
[2] = {"50%", WAVEFORMVOLUME_50},
[3] = {"100%", WAVEFORMVOLUME_100},
};
static Sprite_List_Option direction_options[2] = {
[0] = {SPRITETYPE_up, DIRECTION_up},
[1] = {SPRITETYPE_down, DIRECTION_down},
};
SDL_Rect
expand(SDL_Rect *rect, int32_t a)
{
return (SDL_Rect){
rect->x - a,
rect->y - a,
rect->w + 2*a,
rect->h + 2*a,
};
}
SDL_Rect
contract(SDL_Rect *rect, int32_t a)
{
return (SDL_Rect){
rect->x + a,
rect->y + a,
rect->w - 2*a,
rect->h - 2*a
};
}
SDL_Rect
cut_left(SDL_Rect *rect, int32_t a)
{
int32_t old_x = rect->x;
rect->x = min(rect->x + a, rect->x + rect->w);
rect->w = max(0, rect->w - a);
return (SDL_Rect){ old_x, rect->y, a, rect->h };
}
SDL_Rect
cut_right(SDL_Rect *rect, int32_t a)
{
rect->w = max(0, rect->w - a);
return (SDL_Rect){ rect->x + rect->w, rect->y, a, rect->h };
}
SDL_Rect
cut_top(SDL_Rect *rect, int32_t a)
{
int32_t old_y = rect->y;
rect->y = min(rect->y + a, rect->y + rect->h);
rect->h = max(0, rect->h - a);
return (SDL_Rect){ rect->x, old_y, rect->w, a };
}
SDL_Rect
cut_bottom(SDL_Rect *rect, int32_t a)
{
rect->h = max(0, rect->h - a);
return (SDL_Rect){ rect->x, rect->y + rect->h, rect->w, a };
}
#define cut_left_percent(RECT, PCT) cut_left(RECT, (RECT)->w * (PCT))
#define cut_right_percent(RECT, PCT) cut_right(RECT, (RECT)->w * (PCT))
#define cut_top_percent(RECT, PCT) cut_top(RECT, (RECT)->h * (PCT))
#define cut_bottom_percent(RECT, PCT) cut_bottom(RECT, (RECT)->h * (PCT))
RectCut
rectcut(SDL_Rect *rect, RectCut_Side side)
{
return (RectCut){ .rect = rect, .side = side };
}
SDL_Rect
rectcut_cut(RectCut rectcut, int32_t a)
{
switch (rectcut.side)
{
case RECTCUTSIDE_left: return cut_left(rectcut.rect, a);
case RECTCUTSIDE_right: return cut_right(rectcut.rect, a);
case RECTCUTSIDE_top: return cut_top(rectcut.rect, a);
case RECTCUTSIDE_bottom: return cut_bottom(rectcut.rect, a);
default: abort();
}
}
void
draw_line(Editor_State *editor, SDL_Point start, SDL_Point stop, uint8_t r, uint8_t g, uint8_t b)
{
SDL_SetRenderDrawColor(editor->r, r, g, b, SDL_ALPHA_OPAQUE);
SDL_RenderDrawLine(editor->r, start.x, start.y, stop.x, stop.y);
}
void
draw_rect_unfilled(Editor_State *editor, SDL_Rect *rect, uint8_t r, uint8_t g, uint8_t b)
{
SDL_SetRenderDrawColor(editor->r, r, g, b, SDL_ALPHA_OPAQUE);
SDL_RenderDrawRect(editor->r, rect);
}
void
draw_rect_filled(Editor_State *editor, SDL_Rect *rect, uint8_t r, uint8_t g, uint8_t b)
{
SDL_SetRenderDrawColor(editor->r, r, g, b, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(editor->r, rect);
}
SDL_Rect
rectcut_cut_percent(RectCut rectcut, float percent)
{
switch (rectcut.side)
{
case RECTCUTSIDE_left: return cut_left_percent(rectcut.rect, percent);
case RECTCUTSIDE_right: return cut_right_percent(rectcut.rect, percent);
case RECTCUTSIDE_top: return cut_top_percent(rectcut.rect, percent);
case RECTCUTSIDE_bottom: return cut_bottom_percent(rectcut.rect, percent);
default: abort();
}
}
int32_t
text_width(Editor_State *editor, char *text)
{
int32_t result = 0;
while (*text != 0)
{
if ((uint8_t)*text >= 32 && (uint8_t)*text <= 128)
{
#if 0
stbtt_bakedchar *ch = editor->font.cdata + ((uint8_t)*text - 32);
#else
Fixed_Font_Kerning *ch = editor->font.cdata + ((uint8_t)*text - 32);
#endif
result += ch->xadvance;
}
++text;
}
return(result);
}
void
draw_sprite(Editor_State *editor, SDL_Texture *texture, SDL_Rect *src, SDL_Rect *dst)
{
SDL_RenderCopy(editor->r, texture, src, dst);
}
void
draw_text(Editor_State *editor, char *text, int32_t x, int32_t y)
{
SDL_Rect tracking_rect = {
.x = x,
.y = y + editor->font.size,
.w = 0,
.h = 0
};
SDL_Rect dest_rect = {
.x = tracking_rect.x,
.y = tracking_rect.y,
.w = 0,
.h = 0
};
SDL_Rect src_rect = {0};
while (*text != 0)
{
if ((uint8_t)*text >= 32 && (uint8_t)*text <= 128)
{
#if 0
stbtt_bakedchar *ch = editor->font.cdata + ((uint8_t)*text - 32);
#else
Fixed_Font_Kerning *ch = editor->font.cdata + ((uint8_t)*text - 32);
#endif
src_rect.x = ch->x0;
src_rect.y = ch->y0;
src_rect.w = (ch->x1 - ch->x0);
src_rect.h = (ch->y1 - ch->y0);
dest_rect.x = tracking_rect.x + ch->xoff;
dest_rect.y = tracking_rect.y + ch->yoff;
dest_rect.w = src_rect.w;
dest_rect.h = src_rect.h;
SDL_RenderCopy(editor->r, editor->font.tex, &src_rect, &dest_rect);
tracking_rect.x += ch->xadvance;
}
++text;
}
}
void
draw_label(Editor_State *editor, SDL_Rect *rect, char *text, Text_Alignment alignment, float padding_ems)
{
int32_t text_w = text_width(editor, text);
int32_t padding_px = padding_ems * editor->font.size;
/* TODO: Move this into a method for calculating rectangles for a given label
** text. Figure out how to work this into an overall layout system.
*/
#if 0
/* Prevent funkiness with oversmall rectangles by enforcing the minimum size
** to contain the text using the given font.
**
** NOTE: If you set width and height in rect to 0 they will be automatically
** set to the minimum size rect for you.
*/
rect->w = max(text_w + 2*padding_px, rect->w);
rect->h = max(editor->font.size + 2*padding_px, rect->h);
#endif
SDL_Point pos = {
.x = 0,
/* Center on Y by going to the rect center then subtracting off 1/2 the
** font height and 1/2 the descent (which is negative).
*/
.y = rect->y + (rect->h / 2) - (editor->font.size / 2) + (editor->font.descent / 2),
};
switch (alignment)
{
case TEXTALIGNMENT_left:
pos.x = rect->x + padding_px;
break;
case TEXTALIGNMENT_center:
pos.x = rect->x + (rect->w / 2) - (text_w / 2);
break;
case TEXTALIGNMENT_right:
pos.x = rect->x + rect->w - padding_px - text_w;
break;
default: abort();
}
SDL_SetClipRect(SDL_GetWindowSurface(editor->w), rect);
draw_text(editor, text, pos.x, pos.y);
SDL_SetClipRect(SDL_GetWindowSurface(editor->w), rect);
}
bool
button(Editor_State *editor, SDL_Rect *rect)
{
bool is_pressed = false;
/* Draw border and contract the rectangle to produce the inside. */
draw_rect_unfilled(editor, rect, 0, 0, 0);
SDL_Rect inner_rect = contract(rect, 1);
if (SDL_PointInRect(&editor->mouse.pos, &inner_rect))
{
if (editor->mouse.left.down)
{
draw_rect_filled(editor, &inner_rect, 99, 155, 255);
}
else
{
if(editor->mouse.left.just_released)
{
is_pressed = true;
}
draw_rect_filled(editor, &inner_rect, 91, 110, 225);
}
}
else
{
draw_rect_filled(editor, &inner_rect, 63, 63, 116);
}
return(is_pressed);
}
bool
selectable_button(Editor_State *editor, SDL_Rect *rect, bool is_selected)
{
bool is_pressed = false;
/* Draw border and contract the rectangle to produce the inside. */
draw_rect_unfilled(editor, rect, 0, 0, 0);
SDL_Rect inner_rect = contract(rect, 1);
if (SDL_PointInRect(&editor->mouse.pos, &inner_rect))
{
if (editor->mouse.left.down)
{
draw_rect_filled(editor, &inner_rect, 99, 155, 255);
}
else
{
if(editor->mouse.left.just_released)
{
is_pressed = true;
}
draw_rect_filled(editor, &inner_rect, 91, 110, 225);
}
}
else
{
if (is_selected)
{
draw_rect_filled(editor, &inner_rect, 91, 110, 225);
}
else
{
draw_rect_filled(editor, &inner_rect, 63, 63, 116);
}
}
return(is_pressed);
}
bool
text_button(Editor_State *editor, SDL_Rect *rect, char *text, Text_Alignment alignment, float padding_ems)
{
bool is_pressed = button(editor, rect);
draw_label(editor, rect, text, alignment, padding_ems);
return(is_pressed);
}
bool
selectable_text_button(Editor_State *editor, SDL_Rect *rect, char *text, Text_Alignment alignment, float padding_ems, bool is_selected)
{
bool is_pressed = selectable_button(editor, rect, is_selected);
draw_label(editor, rect, text, alignment, padding_ems);
return(is_pressed);
}
bool
sprite_button(Editor_State *editor, SDL_Rect *rect, Sprite_Type sprite)
{
bool is_pressed = button(editor, rect);
/* Center the sprite in the button */
SDL_Rect *sprite_rect = sprite_table + sprite;
SDL_Rect dest_rect = {
rect->x + (rect->w / 2) - (sprite_rect->w / 2),
rect->y + (rect->h / 2) - (sprite_rect->h / 2),
sprite_rect->w,
sprite_rect->h
};
draw_sprite(editor, editor->sprites, sprite_rect, &dest_rect);
return(is_pressed);
}
bool
selectable_sprite_button(Editor_State *editor, SDL_Rect *rect, Sprite_Type sprite, bool is_selected)
{
bool is_pressed = selectable_button(editor, rect, is_selected);
/* Center the sprite in the button */
SDL_Rect *sprite_rect = sprite_table + sprite;
SDL_Rect dest_rect = {
rect->x + (rect->w / 2) - (sprite_rect->w / 2),
rect->y + (rect->h / 2) - (sprite_rect->h / 2),
sprite_rect->w,
sprite_rect->h
};
draw_sprite(editor, editor->sprites, sprite_rect, &dest_rect);
return(is_pressed);
}
void
header(Editor_State *editor, SDL_Rect *rect, char* text)
{
draw_label(editor, rect, text, TEXTALIGNMENT_left, 0.25f);
}
bool
toggle_sprite_button(Editor_State *editor, SDL_Rect *rect, Sprite_Type on_spr, Sprite_Type off_spr, bool *value)
{
bool is_changed = false;
draw_rect_unfilled(editor, rect, 0, 0, 0);
SDL_Rect inner_rect = contract(rect, 1);
SDL_Rect *sprite_rect = NULL;
if (SDL_PointInRect(&editor->mouse.pos, rect))
{
if (*value)
{
draw_rect_filled(editor, &inner_rect, 217, 160, 102);
}
else
{
draw_rect_filled(editor, &inner_rect, 91, 110, 225);
}
if (editor->mouse.left.just_pressed)
{
*value = !*value;
is_changed = true;
}
}
else
{
if (*value)
{
draw_rect_filled(editor, &inner_rect, 223, 113, 38);
}
else
{
draw_rect_filled(editor, &inner_rect, 50, 60, 57);
}
}
if (*value)
{
sprite_rect = sprite_table + on_spr;
}
else
{
sprite_rect = sprite_table + off_spr;
}
SDL_Rect dest_rect = {
rect->x + (rect->w / 2) - (sprite_rect->w / 2),
rect->y + (rect->h / 2) - (sprite_rect->h / 2),
sprite_rect->w,
sprite_rect->h
};
draw_sprite(editor, editor->sprites, sprite_rect, &dest_rect);
return(is_changed);
}
bool
range_selector(Editor_State *editor, SDL_Rect *rect, int32_t *value, int32_t min, int32_t max, char *fmt)
{
static char numtxt[128];
bool is_changed = false;
draw_rect_unfilled(editor, rect, 0, 0, 0);
*rect = contract(rect, 1);
draw_rect_filled(editor, rect, 50, 60, 57);
float v = *value;
int32_t last = *value;
SDL_Rect knob_rect = contract(rect, 3);
rect->w = knob_rect.w;
knob_rect.w = 20;
knob_rect.x += (v - min) * (rect->w - knob_rect.w) / (max - min);
if (SDL_PointInRect(&editor->mouse.pos, rect))
{
if (editor->mouse.left.down)
{
v = ((float)min + ((float)(editor->mouse.pos.x - rect->x) * (max - min + 1)) / (float)rect->w);
}
}
button(editor, &knob_rect);
v = max(min, v);
v = min(max, v);
*value = v;
if (last != *value)
{
is_changed = true;
}
snprintf(numtxt, 128, fmt, *value);
draw_label(editor, rect, numtxt, TEXTALIGNMENT_center, 0);
return(is_changed);
}
bool
pan_selector(Editor_State *editor, SDL_Rect *rect, int32_t *value)
{
bool is_changed = false;
int32_t min = PAN_left;
int32_t max = PAN_right;
{
int32_t l_width = text_width(editor, "L");
cut_left(rect, l_width);
SDL_Rect l_rect = cut_left(rect, l_width);
draw_label(editor, &l_rect, "L", TEXTALIGNMENT_center, 0);
}
{
int32_t r_width = text_width(editor, "R");
cut_right(rect, r_width);
SDL_Rect r_rect = cut_right(rect, r_width);
draw_label(editor, &r_rect, "R", TEXTALIGNMENT_center, 0);
}
draw_line(editor, (SDL_Point){rect->x + 3, rect->y + rect->h / 2}, (SDL_Point){rect->x + rect->w - 3, rect->y + rect->h / 2}, 50, 60, 57);
draw_line(editor, (SDL_Point){rect->x + (rect->w / 2), rect->y + (rect->h / 2) - 11}, (SDL_Point){rect->x + (rect->w / 2), rect->y + (rect->h / 2) - 2}, 50, 60, 57);
float v = *value;
int32_t last = *value;
SDL_Rect knob_rect = contract(rect, 3);
rect->w = knob_rect.w;
knob_rect.w = 10;
knob_rect.x += (v - min) * (rect->w - knob_rect.w) / (max - min);
if (SDL_PointInRect(&editor->mouse.pos, rect))
{
if (editor->mouse.left.down)
{
v = ((float)min + ((float)(editor->mouse.pos.x - rect->x) * (max - min + 1)) / (float)rect->w);
}
}
button(editor, &knob_rect);
v = max(min, v);
v = min(max, v);
*value = v;
if (last != *value)
{
is_changed = true;
}
return(is_changed);
}
bool
options(Editor_State *editor, SDL_Rect *rect, List_Option *options, int32_t n_options, int32_t *value)
{
bool item_changed = false;
int32_t item_btn_w = ((float)rect->w / n_options);
SDL_Rect btn_rect = {rect->x, rect->y, item_btn_w, rect->h};
for (int32_t i = 0; i < n_options; ++i)
{
bool is_selected = (*value == options[i].value);
if (selectable_text_button(editor, &btn_rect, options[i].name, TEXTALIGNMENT_center, 0.25f, is_selected))
{
*value = options[i].value;
item_changed = true;
}
btn_rect.x += btn_rect.w;
}
return(item_changed);
}
bool
sprite_options(Editor_State *editor, SDL_Rect *rect, Sprite_List_Option *options, int32_t n_options, int32_t *value)
{
bool item_changed = false;
int32_t item_btn_w = ((float)rect->w / n_options);
SDL_Rect btn_rect = {rect->x, rect->y, item_btn_w, rect->h};
for (int32_t i = 0; i < n_options; ++i)
{
bool is_selected = (*value == options[i].value);
if (selectable_sprite_button(editor, &btn_rect, options[i].sprite, is_selected))
{
*value = options[i].value;
item_changed = true;
}
btn_rect.x += btn_rect.w;
}
return(item_changed);
}
bool
text_input(Editor_State *editor, SDL_Rect *rect, Input_State *input)
{
bool was_edited = false;
if (SDL_PointInRect(&editor->mouse.pos, rect))
{
/* Trigger editing by clicking in the text field. */
if (editor->mouse.left.just_pressed)
{
input->is_editing = true;
input->text[0] = '\0';
}
}
else
{
/* Take focus by clicking outside the text field. */
if (editor->mouse.left.just_pressed)
{
input->is_editing = false;
}
}
draw_rect_unfilled(editor, rect, 0, 0, 0);
SDL_Rect inner_rect = contract(rect, 1);
if (input->is_editing)
{
if (editor->text_input.has_input)
{
/* Reject the input if its not full of digits. */
bool is_numeric = true;
char *ch = editor->text_input.text;
while (*ch)
{
if (!(*ch >= '0' && *ch <= '9') &&
!(*ch >= 'a' && *ch <= 'f') &&
!(*ch >= 'A' && *ch <= 'F'))
{
SDL_Log("%c", *ch);
is_numeric = false;
break;
}
++ch;
}
if (is_numeric)
{
strncat(input->text, editor->text_input.text, 5);
was_edited = true;
}
}
draw_rect_filled(editor, &inner_rect, 155, 173, 183);
}
else
{
draw_rect_filled(editor, &inner_rect, 132, 126, 135);
}
draw_label(editor, &inner_rect, input->text, TEXTALIGNMENT_right, 0.25f);
return(was_edited);
}
bool
hex_input(Editor_State *editor, SDL_Rect *rect, Input_State *input)
{
bool was_edited = text_input(editor, rect, input);
if (was_edited)
{
/* Parse input as hex */
errno = 0;
input->integer = strtol(input->text, NULL, 16);
if (errno == EINVAL || errno == ERANGE)
{
SDL_Log("error: invalid integer value %s", input->text);
input->text[0] = '\0';
input->integer = 0;
}
}
return(was_edited);
}
bool
hex_input_u8(Editor_State *editor, SDL_Rect *rect, Input_State *input)
{
bool was_edited = hex_input(editor, rect, input);
if (was_edited)
{
if (strlen(input->text) > 2)
{
input->text[2] = '\0';
}
input->integer &= 0xFF;
}
return(was_edited);
}
void
draw_reg_value(Editor_State *editor, SDL_Rect *rect, uint8_t value)
{
static char buf[128];
snprintf(buf, 128, "%02X", value);
draw_rect_unfilled(editor, rect, 0, 0, 0);
SDL_Rect inner_rect = contract(rect, 1);
draw_rect_filled(editor, &inner_rect, 132, 126, 135);
SDL_Rect clipboard_rect = cut_right(&inner_rect, 16 + 6);
if (sprite_button(editor, &clipboard_rect, SPRITETYPE_clipboard))
{
SDL_SetClipboardText(buf);
}
draw_label(editor, &inner_rect, buf, TEXTALIGNMENT_left, 0.25f);
}
void
reg_value(Editor_State *editor, SDL_Rect *rect, char *regname, uint8_t value)
{
SDL_Rect reg_rect = cut_left(rect, text_width(editor, regname) + 6);
draw_label(editor, ®_rect, regname, TEXTALIGNMENT_center, 0.25f);
SDL_Rect val_rect = cut_left(rect, 3 * editor->font.size + 6);
draw_reg_value(editor, &val_rect, value);
}
void
editor_init(Editor_State *editor)
{
/* Load font and create simple texture atlas. */
#if 0
/* TODO: Make this an includable fixed font as binary data. */
FILE* font_file = fopen("../../ext/PixelCarnageMonoNull.ttf", "rb");
if (font_file)
{
uint32_t tex_x = 128;
uint32_t tex_y = 100;
editor->font.size = 16.0f;
editor->font.cdata = push_array(editor->a, stbtt_bakedchar, 96);
editor->font.tex = SDL_CreateTexture(editor->r, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, tex_x, tex_y);
SDL_SetTextureBlendMode(editor->font.tex, SDL_BLENDMODE_BLEND); /* Do alpha blending. */
uint64_t old_pos = arena_get_pos(editor->a);
{
fseek(font_file, 0, SEEK_END);
uint64_t font_file_size = ftell(font_file);
rewind(font_file);
uint8_t *font_data = push_array(editor->a, uint8_t, font_file_size);
fread(font_data, font_file_size, 1, font_file);
fclose(font_file);
stbtt_fontinfo info;
stbtt_InitFont(&info, font_data, 0);
stbtt_GetFontVMetrics(&info, &editor->font.ascent, &editor->font.descent, &editor->font.line_gap);
float scalar = stbtt_ScaleForPixelHeight(&info, editor->font.size);
editor->font.ascent *= scalar;
editor->font.descent *= scalar;
editor->font.line_gap *= scalar;
uint8_t *tmp_bitmap = push_array(editor->a, uint8_t, tex_x*tex_y);
stbtt_BakeFontBitmap(font_data, 0, editor->font.size, tmp_bitmap, tex_x, tex_y, 32, 96, editor->font.cdata);
uint8_t *pix = 0;
int32_t pitch = 0;
SDL_LockTexture(editor->font.tex, NULL, (void*)&pix, &pitch);
for (uint32_t y = 0; y < tex_y; ++y)
{
uint32_t *line = (uint32_t*)(pix + (y * pitch));
for (uint32_t x = 0; x < tex_x; ++x)
{
uint8_t val = tmp_bitmap[x + (y * tex_x)];
*line = 0xFFFFFF00 | val;
++line;
}
}
SDL_UnlockTexture(editor->font.tex);
}
arena_set_pos_back(editor->a, old_pos);
}
else
{
fprintf(stderr, "error: font open failed: %s\n", strerror(errno));
}
#else
uint32_t tex_x = FIXED_FONT_BITMAP_W;
uint32_t tex_y = FIXED_FONT_BITMAP_H;
uint8_t *pix = 0;
int32_t pitch = 0;
editor->font.size = FIXED_FONT_HEIGHT_PIX;
editor->font.ascent = FIXED_FONT_ASCENT;
editor->font.descent = FIXED_FONT_DESCENT;
editor->font.line_gap = FIXED_FONT_LINE_GAP;
editor->font.cdata = fixed_font_kerning;
editor->font.tex = SDL_CreateTexture(editor->r, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, tex_x, tex_y);
SDL_SetTextureBlendMode(editor->font.tex, SDL_BLENDMODE_BLEND); /* Do alpha blending. */
SDL_LockTexture(editor->font.tex, NULL, (void*)&pix, &pitch);
for (uint32_t y = 0; y < tex_y; ++y)
{
uint32_t *line = (uint32_t*)(pix + (y * pitch));
for (uint32_t x = 0; x < tex_x; ++x)
{
uint8_t val = fixed_font_bitmap[x + (y * tex_x)];
*line = 0xFFFFFF00 | val;
++line;
}
}
SDL_UnlockTexture(editor->font.tex);
#endif
/* Load sprites */
#ifdef __EMSCRIPTEN__
editor->sprites = load_png(editor->r, "assets/aputoy-sprites.png");
#else
editor->sprites = load_png(editor->r, "../../misc/aputoy-sprites.png");
#endif
/* Initialize values */
editor->internal.l_volume = 7;
editor->internal.r_volume = 7;
editor->internal.apu.is_on = true;
editor->internal.apu.vol = 0xFF;
editor->internal.apu.pan = 0xFF;
memset(&editor->internal.ch1, 0, sizeof(editor->internal.ch1));
apu_reset(&editor->internal.apu);
for (uint32_t i = 0; i < 16; ++i)
{
editor->internal.ch3.samples[i].text[0] = '0';
editor->internal.ch3.samples[i].text[1] = '0';
}
}
void
editor_shutdown(Editor_State *editor)
{
SDL_DestroyTexture(editor->sprites);
SDL_DestroyTexture(editor->font.tex);
}
void
editor_ch1_update(Editor_State *editor, SDL_Rect ch_rect, int32_t padding)
{
/* Draw border on RHS */
draw_line(editor, (SDL_Point){ch_rect.x + ch_rect.w, ch_rect.y}, (SDL_Point){ch_rect.x + ch_rect.w, ch_rect.y + ch_rect.h}, 0, 0, 0);
/* Top section */
{
/* CH text */
SDL_Rect ch_top_rect = cut_top(&ch_rect, 2*editor->font.size);
int32_t ch_text_width = text_width(editor, "CH1") + 2 * padding;
SDL_Rect ch_label = cut_left(&ch_top_rect, ch_text_width);
draw_label(editor, &ch_label, "CH1", TEXTALIGNMENT_left, 0.25f);
SDL_Rect toggle_rect = cut_right_percent(&ch_top_rect, 0.20f);
toggle_rect = contract(&toggle_rect, padding);
if (toggle_sprite_button(editor, &toggle_rect, SPRITETYPE_power, SPRITETYPE_power, &editor->internal.apu.ch1.is_on))
{
if (editor->internal.ch1.is_locked)
{
editor->internal.ch1.is_locked = false;
}
square_wave_channel_reset(&editor->internal.apu.ch1);
}
SDL_Rect lock_rect = cut_right(&ch_top_rect, toggle_rect.w);
lock_rect = contract(&lock_rect, padding);
lock_rect.x += padding;
toggle_sprite_button(editor, &lock_rect, SPRITETYPE_locked, SPRITETYPE_unlocked, &editor->internal.ch1.is_locked);
/* CH wave sprite */
SDL_Rect sq_wave_src_rect = sprite_table[SPRITETYPE_square_wave];
SDL_Rect sq_wave_rect = cut_left(&ch_top_rect, sq_wave_src_rect.w);
sq_wave_rect.y += (sq_wave_rect.h - sq_wave_src_rect.h) / 2;
sq_wave_rect.h = sq_wave_src_rect.h;
draw_sprite(editor, editor->sprites, &sq_wave_src_rect, &sq_wave_rect);
/* Pan slider */
static int32_t pan_value = PAN_center;
if (pan_selector(editor, &ch_top_rect, &pan_value))
{
/* Update CH1 panning */
uint8_t lpan = (pan_value == PAN_center || pan_value == PAN_left) ? 1 : 0;
uint8_t rpan = (pan_value == PAN_center || pan_value == PAN_right) ? 1 : 0;
editor->internal.apu.pan &= ~0x11;
editor->internal.apu.pan |= ((lpan & 1) << 4) | (rpan & 1);
}
}
/* Separate CH header from body */
cut_top(&ch_rect, editor->font.size);
/* Initial Volume */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Initial Volume");
SDL_Rect volume_rect = cut_top(&ch_rect, 2*editor->font.size + 2 * padding);
volume_rect = contract(&volume_rect, padding);
if (range_selector(editor, &volume_rect, &editor->internal.ch1.env_start_vol, 0, 0xF, "%d"))
{
editor->internal.apu.ch1.envelope.start_volume = editor->internal.ch1.env_start_vol & 0xF;
square_wave_channel_reset(&editor->internal.apu.ch1);
}
}
/* Wavelength */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Wavelength");
SDL_Rect wavelen_ctrl_rect = cut_top(&ch_rect, 2*editor->font.size + 2 * padding);
wavelen_ctrl_rect = contract(&wavelen_ctrl_rect, padding);
if (range_selector(editor, &wavelen_ctrl_rect, (int32_t*)&editor->internal.ch1.wavelength, 0, 0x7FF, "%03X"))
{
editor->internal.apu.ch1.wavelength = (uint16_t)(editor->internal.ch1.wavelength & 0x7FF);
square_wave_channel_reset(&editor->internal.apu.ch1);
}
}
/* Duty Cycle */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Duty Cycle");
SDL_Rect duty_cycle_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
duty_cycle_rect = contract(&duty_cycle_rect, padding);
if (options(editor, &duty_cycle_rect, duty_cycle_options, 4, &editor->internal.ch1.duty_cycle))
{
editor->internal.apu.ch1.duty_cycle = (Duty_Cycle)editor->internal.ch1.duty_cycle;
square_wave_channel_reset(&editor->internal.apu.ch1);
}
}
/* Envelope */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Envelope");
/* Direction */
{
SDL_Rect dir_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
dir_rect = contract(&dir_rect, padding);
if (sprite_options(editor, &dir_rect, direction_options, 2, &editor->internal.ch1.env_dir))
{
editor->internal.apu.ch1.envelope.direction = (Direction)editor->internal.ch1.env_dir;
square_wave_channel_reset(&editor->internal.apu.ch1);
}
}
/* Sweep pace */
{
SDL_Rect pace_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
pace_rect = contract(&pace_rect, padding);
SDL_Rect txt_rect = cut_left_percent(&pace_rect, 0.5f);
draw_label(editor, &txt_rect, "Pace", TEXTALIGNMENT_right, 0.25f);
if (range_selector(editor, &pace_rect, &editor->internal.ch1.env_pace, 0, 7, "%d"))
{
editor->internal.apu.ch1.envelope.sweep_pace = editor->internal.ch1.env_pace & 0x7;
square_wave_channel_reset(&editor->internal.apu.ch1);
}
}
}
/* Length */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Length");
SDL_Rect body_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
body_rect = contract(&body_rect, padding);
SDL_Rect toggle_rect = cut_left_percent(&body_rect, 0.25f);
if (toggle_sprite_button(editor, &toggle_rect, SPRITETYPE_power, SPRITETYPE_power, &editor->internal.apu.ch1.length.is_on))
{
square_wave_channel_reset(&editor->internal.apu.ch1);
}
cut_left(&body_rect, 2 * padding);
if (range_selector(editor, &body_rect, &editor->internal.ch1.length, 0, 63, "%02X"))
{
editor->internal.apu.ch1.length.length = (uint8_t)(editor->internal.ch1.length & 0x3F);
square_wave_channel_reset(&editor->internal.apu.ch1);
}
}
/* Sweep */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Sweep");
/* Direction */
{
SDL_Rect dir_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
dir_rect = contract(&dir_rect, padding);
if (sprite_options(editor, &dir_rect, direction_options, 2, &editor->internal.ch1.sweep_dir))
{
editor->internal.apu.ch1.sweep.direction = (Direction)editor->internal.ch1.sweep_dir;
square_wave_channel_reset(&editor->internal.apu.ch1);
}
}
/* Slope */
{
SDL_Rect slope_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
slope_rect = contract(&slope_rect, padding);
SDL_Rect txt_rect = cut_left_percent(&slope_rect, 0.5f);
draw_label(editor, &txt_rect, "Slope", TEXTALIGNMENT_right, 0.25f);
if (range_selector(editor, &slope_rect, &editor->internal.ch1.sweep_slope, 0, 7, "%d"))
{
editor->internal.apu.ch1.sweep.slope = editor->internal.ch1.sweep_slope & 0x7;
square_wave_channel_reset(&editor->internal.apu.ch1);
}
}
/* Pace */
{
SDL_Rect pace_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
pace_rect = contract(&pace_rect, padding);
SDL_Rect txt_rect = cut_left_percent(&pace_rect, 0.5f);
draw_label(editor, &txt_rect, "Pace", TEXTALIGNMENT_right, 0.25f);
if (range_selector(editor, &pace_rect, &editor->internal.ch1.sweep_pace, 0, 7, "%d"))
{
editor->internal.apu.ch1.sweep.pace = editor->internal.ch1.sweep_pace & 0x7;
square_wave_channel_reset(&editor->internal.apu.ch1);
}
}
}
/* Register values */
{
SDL_Rect reg_val_rect = cut_bottom(&ch_rect, 4 * 2 * editor->font.size + 2 * padding);
SDL_Rect row1 = cut_top(®_val_rect, 2 * editor->font.size + 2 * padding);
SDL_Rect row2 = cut_top(®_val_rect, 2 * editor->font.size + 2 * padding);
SDL_Rect row3 = cut_top(®_val_rect, 2 * editor->font.size + 2 * padding);
/* NR10 */
uint8_t nr10 = (
((editor->internal.apu.ch1.sweep.pace & 7) << 4) |
((editor->internal.apu.ch1.sweep.direction & 1) << 3) |
((editor->internal.apu.ch1.sweep.slope & 7)));
reg_value(editor, &row1, "NR10", nr10);
cut_left(&row1, 16);
/* NR11 */
uint8_t nr11 = (
((editor->internal.apu.ch1.duty_cycle & 3) << 6) |
((editor->internal.apu.ch1.length.length & 0x3F)));
reg_value(editor, &row1, "NR11", nr11);
/* NR12 */
uint8_t nr12 = (
((editor->internal.apu.ch1.envelope.start_volume & 0xF) << 4) |
((editor->internal.apu.ch1.envelope.direction & 1) << 3) |
((editor->internal.apu.ch1.envelope.sweep_pace & 0x7)));
reg_value(editor, &row2, "NR12", nr12);
cut_left(&row2, 16);
/* NR13 */
uint8_t nr13 = editor->internal.apu.ch1.wavelength & 0xFF;
reg_value(editor, &row2, "NR13", nr13);
/* NR14 */
uint8_t nr14 = (
((editor->internal.apu.ch1.length.is_on & 1) << 6) |
((editor->internal.apu.ch1.wavelength >> 8) & 0x7));
reg_value(editor, &row3, "NR14", nr14);
}
}
void
editor_ch2_update(Editor_State *editor, SDL_Rect ch_rect, int32_t padding)
{
/* Draw border on RHS */
draw_line(editor, (SDL_Point){ch_rect.x + ch_rect.w, ch_rect.y}, (SDL_Point){ch_rect.x + ch_rect.w, ch_rect.y + ch_rect.h}, 0, 0, 0);
/* Top section */
{
/* CH text */
SDL_Rect ch_top_rect = cut_top(&ch_rect, 2*editor->font.size);
int32_t ch_text_width = text_width(editor, "CH2") + 2 * padding;
SDL_Rect ch_label = cut_left(&ch_top_rect, ch_text_width);
draw_label(editor, &ch_label, "CH2", TEXTALIGNMENT_left, 0.25f);
SDL_Rect toggle_rect = cut_right_percent(&ch_top_rect, 0.20f);
toggle_rect = contract(&toggle_rect, padding);
if (toggle_sprite_button(editor, &toggle_rect, SPRITETYPE_power, SPRITETYPE_power, &editor->internal.apu.ch2.is_on))
{
if (editor->internal.ch2.is_locked)
{
editor->internal.ch2.is_locked = false;
}
square_wave_channel_reset(&editor->internal.apu.ch2);
}
SDL_Rect lock_rect = cut_right(&ch_top_rect, toggle_rect.w);
lock_rect = contract(&lock_rect, padding);
lock_rect.x += padding;
toggle_sprite_button(editor, &lock_rect, SPRITETYPE_locked, SPRITETYPE_unlocked, &editor->internal.ch2.is_locked);
/* CH wave sprite */
SDL_Rect sq_wave_src_rect = sprite_table[SPRITETYPE_square_wave];
SDL_Rect sq_wave_rect = cut_left(&ch_top_rect, sq_wave_src_rect.w);
sq_wave_rect.y += (sq_wave_rect.h - sq_wave_src_rect.h) / 2;
sq_wave_rect.h = sq_wave_src_rect.h;
draw_sprite(editor, editor->sprites, &sq_wave_src_rect, &sq_wave_rect);
/* Pan slider */
static int32_t pan_value = PAN_center;
if (pan_selector(editor, &ch_top_rect, &pan_value))
{
/* Update CH2 panning */
uint8_t lpan = (pan_value == PAN_center || pan_value == PAN_left) ? 1 : 0;
uint8_t rpan = (pan_value == PAN_center || pan_value == PAN_right) ? 1 : 0;
editor->internal.apu.pan &= ~0x22;
editor->internal.apu.pan |= ((lpan & 1) << 5) | ((rpan & 1) << 1);
}
}
/* Separate CH header from body */
cut_top(&ch_rect, editor->font.size);
/* Initial Volume */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Initial Volume");
SDL_Rect volume_rect = cut_top(&ch_rect, 2*editor->font.size + 2 * padding);
volume_rect = contract(&volume_rect, padding);
if (range_selector(editor, &volume_rect, &editor->internal.ch2.env_start_vol, 0, 0xF, "%d"))
{
editor->internal.apu.ch2.envelope.start_volume = editor->internal.ch2.env_start_vol & 0xF;
square_wave_channel_reset(&editor->internal.apu.ch2);
}
}
/* Wavelength */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Wavelength");
SDL_Rect wavelen_ctrl_rect = cut_top(&ch_rect, 2*editor->font.size + 2 * padding);
wavelen_ctrl_rect = contract(&wavelen_ctrl_rect, padding);
if (range_selector(editor, &wavelen_ctrl_rect, (int32_t*)&editor->internal.ch2.wavelength, 0, 0x7FF, "%03X"))
{
editor->internal.apu.ch2.wavelength = (uint16_t)(editor->internal.ch2.wavelength & 0x7FF);
square_wave_channel_reset(&editor->internal.apu.ch2);
}
}
/* Duty Cycle */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Duty Cycle");
SDL_Rect duty_cycle_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
duty_cycle_rect = contract(&duty_cycle_rect, padding);
if (options(editor, &duty_cycle_rect, duty_cycle_options, 4, &editor->internal.ch2.duty_cycle))
{
editor->internal.apu.ch2.duty_cycle = (Duty_Cycle)editor->internal.ch2.duty_cycle;
square_wave_channel_reset(&editor->internal.apu.ch2);
}
}
/* Envelope */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Envelope");
/* Direction */
{
SDL_Rect dir_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
dir_rect = contract(&dir_rect, padding);
if (sprite_options(editor, &dir_rect, direction_options, 2, &editor->internal.ch2.env_dir))
{
editor->internal.apu.ch2.envelope.direction = (Direction)editor->internal.ch2.env_dir;
square_wave_channel_reset(&editor->internal.apu.ch2);
}
}
/* Sweep pace */
{
SDL_Rect pace_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
pace_rect = contract(&pace_rect, padding);
SDL_Rect txt_rect = cut_left_percent(&pace_rect, 0.5f);
draw_label(editor, &txt_rect, "Pace", TEXTALIGNMENT_right, 0.25f);
if (range_selector(editor, &pace_rect, &editor->internal.ch2.env_pace, 0, 7, "%d"))
{
editor->internal.apu.ch2.envelope.sweep_pace = editor->internal.ch2.env_pace & 0x7;
square_wave_channel_reset(&editor->internal.apu.ch2);
}
}
}
/* Length */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Length");
SDL_Rect body_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
body_rect = contract(&body_rect, padding);
SDL_Rect toggle_rect = cut_left_percent(&body_rect, 0.25f);
if (toggle_sprite_button(editor, &toggle_rect, SPRITETYPE_power, SPRITETYPE_power, &editor->internal.apu.ch2.length.is_on))
{
square_wave_channel_reset(&editor->internal.apu.ch2);
}
cut_left(&body_rect, 2 * padding);
if (range_selector(editor, &body_rect, &editor->internal.ch2.length, 0, 63, "%02X"))
{
editor->internal.apu.ch2.length.length = (uint8_t)(editor->internal.ch2.length & 0x3F);
square_wave_channel_reset(&editor->internal.apu.ch2);
}
}
/* Register values */
{
SDL_Rect reg_val_rect = cut_bottom(&ch_rect, 4 * 2 * editor->font.size + 2 * padding);
SDL_Rect row1 = cut_top(®_val_rect, 2 * editor->font.size + 2 * padding);
SDL_Rect row2 = cut_top(®_val_rect, 2 * editor->font.size + 2 * padding);
/* NR21 */
uint8_t nr21 = (
((editor->internal.apu.ch2.duty_cycle & 3) << 6) |
((editor->internal.apu.ch2.length.length & 0x3F)));
reg_value(editor, &row1, "NR21", nr21);
cut_left(&row1, 16);
/* NR22 */
uint8_t nr22 = (
((editor->internal.apu.ch2.envelope.start_volume & 0xF) << 4) |
((editor->internal.apu.ch2.envelope.direction & 1) << 3) |
((editor->internal.apu.ch2.envelope.sweep_pace & 0x7)));
reg_value(editor, &row1, "NR22", nr22);
/* NR13 */
uint8_t nr23 = editor->internal.apu.ch2.wavelength & 0xFF;
reg_value(editor, &row2, "NR23", nr23);
cut_left(&row2, 16);
/* NR14 */
uint8_t nr24 = (
((editor->internal.apu.ch2.length.is_on & 1) << 6) |
((editor->internal.apu.ch2.wavelength >> 8) & 0x7));
reg_value(editor, &row2, "NR24", nr24);
}
}
void
editor_ch3_update(Editor_State *editor, SDL_Rect ch_rect, int32_t padding)
{
/* Draw border on RHS */
draw_line(editor, (SDL_Point){ch_rect.x + ch_rect.w, ch_rect.y}, (SDL_Point){ch_rect.x + ch_rect.w, ch_rect.y + ch_rect.h}, 0, 0, 0);
/* Top section */
{
/* CH text */
SDL_Rect ch_top_rect = cut_top(&ch_rect, 2*editor->font.size);
int32_t ch_text_width = text_width(editor, "CH3") + 2 * padding;
SDL_Rect ch_label = cut_left(&ch_top_rect, ch_text_width);
draw_label(editor, &ch_label, "CH3", TEXTALIGNMENT_left, 0.25f);
SDL_Rect toggle_rect = cut_right_percent(&ch_top_rect, 0.20f);
toggle_rect = contract(&toggle_rect, padding);
if (toggle_sprite_button(editor, &toggle_rect, SPRITETYPE_power, SPRITETYPE_power, &editor->internal.apu.ch3.is_on))
{
if (editor->internal.ch3.is_locked)
{
editor->internal.ch3.is_locked = false;
}
waveform_channel_reset(&editor->internal.apu.ch3);
}
SDL_Rect lock_rect = cut_right(&ch_top_rect, toggle_rect.w);
lock_rect = contract(&lock_rect, padding);
lock_rect.x += padding;
toggle_sprite_button(editor, &lock_rect, SPRITETYPE_locked, SPRITETYPE_unlocked, &editor->internal.ch3.is_locked);
/* CH wave sprite */
SDL_Rect wave_src_rect = sprite_table[SPRITETYPE_waveform];
SDL_Rect wave_rect = cut_left(&ch_top_rect, wave_src_rect.w);
wave_rect.y += (wave_rect.h - wave_src_rect.h) / 2;
wave_rect.h = wave_src_rect.h;
draw_sprite(editor, editor->sprites, &wave_src_rect, &wave_rect);
/* Pan slider */
static int32_t pan_value = PAN_center;
if (pan_selector(editor, &ch_top_rect, &pan_value))
{
/* Update CH3 panning */
uint8_t lpan = (pan_value == PAN_center || pan_value == PAN_left) ? 1 : 0;
uint8_t rpan = (pan_value == PAN_center || pan_value == PAN_right) ? 1 : 0;
editor->internal.apu.pan &= ~0x44;
editor->internal.apu.pan |= ((lpan & 1) << 6) | ((rpan & 1) << 2);
}
}
/* Separate CH header from body */
cut_top(&ch_rect, editor->font.size);
/* Waveform Volume */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Waveform Volume");
SDL_Rect wave_vol_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
wave_vol_rect = contract(&wave_vol_rect, padding);
if (options(editor, &wave_vol_rect, waveform_volume_options, 4, &editor->internal.ch3.volume))
{
editor->internal.apu.ch3.volume = (Waveform_Volume)editor->internal.ch3.volume;
waveform_channel_reset(&editor->internal.apu.ch3);
}
}
/* Wavelength */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Wavelength");
SDL_Rect wavelen_ctrl_rect = cut_top(&ch_rect, 2*editor->font.size + 2 * padding);
wavelen_ctrl_rect = contract(&wavelen_ctrl_rect, padding);
if (range_selector(editor, &wavelen_ctrl_rect, (int32_t*)&editor->internal.ch3.wavelength, 0, 0x7FF, "%03X"))
{
editor->internal.apu.ch3.wavelength = (uint16_t)(editor->internal.ch3.wavelength & 0x7FF);
waveform_channel_reset(&editor->internal.apu.ch3);
}
}
/* Length */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Length");
SDL_Rect body_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
body_rect = contract(&body_rect, padding);
SDL_Rect toggle_rect = cut_left_percent(&body_rect, 0.25f);
if (toggle_sprite_button(editor, &toggle_rect, SPRITETYPE_power, SPRITETYPE_power, &editor->internal.apu.ch3.length.is_on))
{
waveform_channel_reset(&editor->internal.apu.ch3);
}
cut_left(&body_rect, 2 * padding);
if (range_selector(editor, &body_rect, &editor->internal.ch3.length, 0, 63, "%02X"))
{
editor->internal.apu.ch3.length.length = (uint8_t)(editor->internal.ch3.length & 0x3F);
waveform_channel_reset(&editor->internal.apu.ch3);
}
}
/* Waveform Data */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Waveform Data");
/* Line 1 */
SDL_Rect body_rect;
SDL_Rect input_rect;
int32_t input_width;
for (uint32_t i = 0; i < 16; ++i)
{
if (i % 8 == 0)
{
body_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
body_rect = contract(&body_rect, padding);
input_width = body_rect.w * 0.125f;
}
input_rect = cut_left(&body_rect, input_width);
if (hex_input_u8(editor, &input_rect, editor->internal.ch3.samples + i))
{
editor->internal.apu.ch3.samples[i] = editor->internal.ch3.samples[i].integer;
}
}
}
/* Register values */
{
SDL_Rect reg_val_rect = cut_bottom(&ch_rect, 4 * 2 * editor->font.size + 2 * padding);
SDL_Rect row1 = cut_top(®_val_rect, 2 * editor->font.size + 2 * padding);
SDL_Rect row2 = cut_top(®_val_rect, 2 * editor->font.size + 2 * padding);
SDL_Rect row3 = cut_top(®_val_rect, 2 * editor->font.size + 2 * padding);
/* NR30 */
uint8_t nr30 = (editor->internal.apu.ch3.is_on & 1) << 7;
reg_value(editor, &row1, "NR30", nr30);
cut_left(&row1, 16);
/* NR31 */
uint8_t nr31 = (editor->internal.apu.ch3.length.length & 0xFF);
reg_value(editor, &row1, "NR31", nr31);
/* NR32 */
uint8_t nr32 = (editor->internal.apu.ch3.volume & 3) << 5;
reg_value(editor, &row2, "NR32", nr32);
cut_left(&row2, 16);
/* NR33 */
uint8_t nr33 = editor->internal.apu.ch3.wavelength & 0xFF;
reg_value(editor, &row2, "NR33", nr33);
/* NR34 */
uint8_t nr34 = (
((editor->internal.apu.ch3.length.is_on & 1) << 6) |
((editor->internal.apu.ch3.wavelength >> 8) & 0x7));
reg_value(editor, &row3, "NR34", nr34);
}
}
void
editor_ch4_update(Editor_State *editor, SDL_Rect ch_rect, int32_t padding)
{
/* Draw border on RHS */
draw_line(editor, (SDL_Point){ch_rect.x + ch_rect.w, ch_rect.y}, (SDL_Point){ch_rect.x + ch_rect.w, ch_rect.y + ch_rect.h}, 0, 0, 0);
/* Top section */
{
/* CH text */
SDL_Rect ch_top_rect = cut_top(&ch_rect, 2*editor->font.size);
int32_t ch_text_width = text_width(editor, "CH4") + 2 * padding;
SDL_Rect ch_label = cut_left(&ch_top_rect, ch_text_width);
draw_label(editor, &ch_label, "CH4", TEXTALIGNMENT_left, 0.25f);
SDL_Rect toggle_rect = cut_right_percent(&ch_top_rect, 0.20f);
toggle_rect = contract(&toggle_rect, padding);
if (toggle_sprite_button(editor, &toggle_rect, SPRITETYPE_power, SPRITETYPE_power, &editor->internal.apu.ch4.is_on))
{
if (editor->internal.ch4.is_locked)
{
editor->internal.ch4.is_locked = false;
}
noise_channel_reset(&editor->internal.apu.ch4);
}
SDL_Rect lock_rect = cut_right(&ch_top_rect, toggle_rect.w);
lock_rect = contract(&lock_rect, padding);
lock_rect.x += padding;
toggle_sprite_button(editor, &lock_rect, SPRITETYPE_locked, SPRITETYPE_unlocked, &editor->internal.ch4.is_locked);
/* CH wave sprite */
SDL_Rect wave_src_rect = sprite_table[SPRITETYPE_noise];
SDL_Rect wave_rect = cut_left(&ch_top_rect, wave_src_rect.w);
wave_rect.y += (wave_rect.h - wave_src_rect.h) / 2;
wave_rect.h = wave_src_rect.h;
draw_sprite(editor, editor->sprites, &wave_src_rect, &wave_rect);
/* Pan slider */
static int32_t pan_value = PAN_center;
if (pan_selector(editor, &ch_top_rect, &pan_value))
{
/* Update CH4 panning */
uint8_t lpan = (pan_value == PAN_center || pan_value == PAN_left) ? 1 : 0;
uint8_t rpan = (pan_value == PAN_center || pan_value == PAN_right) ? 1 : 0;
editor->internal.apu.pan &= ~0x88;
editor->internal.apu.pan |= ((lpan & 1) << 7) | ((rpan & 1) << 3);
}
}
/* Separate CH header from body */
cut_top(&ch_rect, editor->font.size);
/* Initial Volume */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Initial Volume");
SDL_Rect volume_rect = cut_top(&ch_rect, 2*editor->font.size + 2 * padding);
volume_rect = contract(&volume_rect, padding);
if (range_selector(editor, &volume_rect, &editor->internal.ch4.env_start_vol, 0, 0xF, "%d"))
{
editor->internal.apu.ch4.envelope.start_volume = editor->internal.ch4.env_start_vol & 0xF;
noise_channel_reset(&editor->internal.apu.ch4);
}
}
/* Shift */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Shift");
SDL_Rect volume_rect = cut_top(&ch_rect, 2*editor->font.size + 2 * padding);
volume_rect = contract(&volume_rect, padding);
if (range_selector(editor, &volume_rect, &editor->internal.ch4.shift, 0, 0xF, "%01X"))
{
editor->internal.apu.ch4.shift = editor->internal.ch4.shift & 0xF;
noise_channel_reset(&editor->internal.apu.ch4);
}
}
/* Divisor */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Divisor");
SDL_Rect volume_rect = cut_top(&ch_rect, 2*editor->font.size + 2 * padding);
volume_rect = contract(&volume_rect, padding);
if (range_selector(editor, &volume_rect, &editor->internal.ch4.divisor, 0, 7, "%d"))
{
editor->internal.apu.ch4.divisor = editor->internal.ch4.divisor & 0x7;
noise_channel_reset(&editor->internal.apu.ch4);
}
}
/* LFSR Width */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "LFSR Width");
SDL_Rect lfsr_w_rect = cut_top(&ch_rect, 2*editor->font.size + 2 * padding);
lfsr_w_rect = contract(&lfsr_w_rect, padding);
if (options(editor, &lfsr_w_rect, lfsr_width_options, 2, &editor->internal.ch4.width))
{
editor->internal.apu.ch4.width = (LFSR_Width)editor->internal.ch4.width;
noise_channel_reset(&editor->internal.apu.ch4);
}
}
/* Envelope */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Envelope");
/* Direction */
{
SDL_Rect dir_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
dir_rect = contract(&dir_rect, padding);
if (sprite_options(editor, &dir_rect, direction_options, 2, &editor->internal.ch4.env_dir))
{
editor->internal.apu.ch4.envelope.direction = (Direction)editor->internal.ch4.env_dir;
noise_channel_reset(&editor->internal.apu.ch4);
}
}
/* Sweep pace */
{
SDL_Rect pace_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
pace_rect = contract(&pace_rect, padding);
SDL_Rect txt_rect = cut_left_percent(&pace_rect, 0.5f);
draw_label(editor, &txt_rect, "Pace", TEXTALIGNMENT_right, 0.25f);
if (range_selector(editor, &pace_rect, &editor->internal.ch4.env_pace, 0, 7, "%d"))
{
editor->internal.apu.ch4.envelope.sweep_pace = editor->internal.ch4.env_pace & 0x7;
noise_channel_reset(&editor->internal.apu.ch4);
}
}
}
/* Length */
{
SDL_Rect header_rect = cut_top(&ch_rect, editor->font.size + 2 * padding);
header(editor, &header_rect, "Length");
SDL_Rect body_rect = cut_top(&ch_rect, 2 * editor->font.size + 2 * padding);
body_rect = contract(&body_rect, padding);
SDL_Rect toggle_rect = cut_left_percent(&body_rect, 0.25f);
if (toggle_sprite_button(editor, &toggle_rect, SPRITETYPE_power, SPRITETYPE_power, &editor->internal.apu.ch4.length.is_on))
{
noise_channel_reset(&editor->internal.apu.ch4);
}
cut_left(&body_rect, 2 * padding);
if (range_selector(editor, &body_rect, &editor->internal.ch4.length, 0, 63, "%02X"))
{
editor->internal.apu.ch4.length.length = (uint8_t)(editor->internal.ch4.length & 0x3F);
noise_channel_reset(&editor->internal.apu.ch4);
}
}
/* Register values */
{
SDL_Rect reg_val_rect = cut_bottom(&ch_rect, 4 * 2 * editor->font.size + 2 * padding);
SDL_Rect row1 = cut_top(®_val_rect, 2 * editor->font.size + 2 * padding);
SDL_Rect row2 = cut_top(®_val_rect, 2 * editor->font.size + 2 * padding);
/* NR41 */
uint8_t nr41 = (editor->internal.apu.ch4.length.length & 0x3F);
reg_value(editor, &row1, "NR41", nr41);
cut_left(&row1, 16);
/* NR42 */
uint8_t nr42 = (
((editor->internal.apu.ch4.envelope.start_volume & 0xF) << 4) |
((editor->internal.apu.ch4.envelope.direction & 1) << 3) |
((editor->internal.apu.ch4.envelope.sweep_pace & 0x7)));
reg_value(editor, &row1, "NR42", nr42);
/* NR43 */
uint8_t nr43 = (
((editor->internal.apu.ch4.shift & 0x7) << 4) |
((editor->internal.apu.ch4.width & 1) << 3) |
((editor->internal.apu.ch4.divisor & 0x7)));
reg_value(editor, &row2, "NR43", nr43);
cut_left(&row2, 16);
/* NR44 */
uint8_t nr44 = (editor->internal.apu.ch4.length.is_on & 1) << 6;
reg_value(editor, &row2, "NR44", nr44);
}
}
void
editor_update(Editor_State *editor)
{
SDL_Rect window_rect = {0};
SDL_GetWindowSize(editor->w, &window_rect.w, &window_rect.h);
int32_t padding = 3;
/* Top bar */
{
SDL_Rect l_vol_bar = cut_top(&window_rect, 2 * editor->font.size + 2 * padding);
SDL_Rect r_vol_bar = cut_top(&window_rect, 2 * editor->font.size + 2 * padding);
int32_t vol_bar_size = 128;
/* Draw bottom line separating top from channels */
draw_line(editor, (SDL_Point){r_vol_bar.x, r_vol_bar.y + r_vol_bar.h}, (SDL_Point){r_vol_bar.x + r_vol_bar.w, r_vol_bar.y + r_vol_bar.h}, 0, 0, 0);
/* L Volume line */
{
SDL_Rect body_rect = contract(&l_vol_bar, 3);
SDL_Rect l_vol_txt_rect = cut_left(&body_rect, text_width(editor, "L Volume") + 2 * padding);
draw_label(editor, &l_vol_txt_rect, "L Volume", TEXTALIGNMENT_center, 0.25f);
SDL_Rect l_vol_rect = cut_left(&body_rect, vol_bar_size);
if (range_selector(editor, &l_vol_rect, &editor->internal.l_volume, 0, 7, "%d"))
{
editor->internal.apu.vol &= ~0x70;
editor->internal.apu.vol |= (editor->internal.l_volume & 0x7) << 4;
}
/* Tap to play */
#if 0
SDL_Rect tap_rect = cut_left(&body_rect, 16 + 4 * padding);
if (sprite_button(editor, &tap_rect, SPRITETYPE_tap))
{
apu_reset(&editor->internal.apu);
}
#else
cut_left(&body_rect, 16 + 4 * padding);
#endif
/* NR50 */
{
SDL_Rect reg_rect = cut_left(&body_rect, text_width(editor, "NR50") + 2 * padding);
draw_label(editor, ®_rect, "NR50", TEXTALIGNMENT_center, 0.25f);
SDL_Rect val_rect = cut_left(&body_rect, 3 * editor->font.size + 2 * padding);
draw_reg_value(editor, &val_rect, editor->internal.apu.vol);
}
}
/* R Volume line */
{
SDL_Rect body_rect = contract(&r_vol_bar, 3);
SDL_Rect r_vol_txt_rect = cut_left(&body_rect, text_width(editor, "R Volume") + 2 * padding);
draw_label(editor, &r_vol_txt_rect, "R Volume", TEXTALIGNMENT_center, 0.25f);
SDL_Rect r_vol_rect = cut_left(&body_rect, vol_bar_size);
if (range_selector(editor, &r_vol_rect, &editor->internal.r_volume, 0, 7, "%d"))
{
editor->internal.apu.vol &= ~0x07;
editor->internal.apu.vol |= (editor->internal.r_volume & 0x7);
}
cut_left(&body_rect, 16 + 4 * padding);
/* NR51 */
{
SDL_Rect reg_rect = cut_left(&body_rect, text_width(editor, "NR51") + 2 * padding);
draw_label(editor, ®_rect, "NR51", TEXTALIGNMENT_center, 0.25f);
SDL_Rect val_rect = cut_left(&body_rect, 3 * editor->font.size + 2 * padding);
draw_reg_value(editor, &val_rect, editor->internal.apu.pan);
}
}
}
/* CH1 */
SDL_Rect ch1_rect = cut_left_percent(&window_rect, 0.25f);
editor_ch1_update(editor, ch1_rect, padding);
/* CH2 */
SDL_Rect ch2_rect = cut_left(&window_rect, ch1_rect.w);
editor_ch2_update(editor, ch2_rect, padding);
/* CH3 */
SDL_Rect ch3_rect = cut_left(&window_rect, ch1_rect.w);
editor_ch3_update(editor, ch3_rect, padding);
/* CH4 */
SDL_Rect ch4_rect = cut_left(&window_rect, ch1_rect.w);
editor_ch4_update(editor, ch4_rect, padding);
}
void
editor_audio_callback(void *userdata, Uint8 *stream, int len)
{
Editor_State *editor = (Editor_State*)userdata;
apu_write(&editor->internal.apu, stream, 44100, len / 2);
if (editor->internal.ch1.is_locked && !editor->internal.apu.ch1.is_on)
{
editor->internal.apu.ch1.is_on = true;
square_wave_channel_reset(&editor->internal.apu.ch1);
}
if (editor->internal.ch2.is_locked && !editor->internal.apu.ch2.is_on)
{
editor->internal.apu.ch2.is_on = true;
square_wave_channel_reset(&editor->internal.apu.ch2);
}
if (editor->internal.ch3.is_locked && !editor->internal.apu.ch3.is_on)
{
editor->internal.apu.ch3.is_on = true;
waveform_channel_reset(&editor->internal.apu.ch3);
}
if (editor->internal.ch4.is_locked && !editor->internal.apu.ch4.is_on)
{
editor->internal.apu.ch4.is_on = true;
noise_channel_reset(&editor->internal.apu.ch4);
}
}
void
update_loop(void *userdata)
{
static SDL_Event event = {0};
Editor_State *editor = (Editor_State*)userdata;
/* Process queued events. */
while (SDL_PollEvent(&event))
{
switch (event.type)
{
case SDL_QUIT:
{
editor->is_running = false;
}
break;
case SDL_MOUSEMOTION:
{
editor->mouse.prev_pos = editor->mouse.pos;
editor->mouse.pos.x = event.motion.x;
editor->mouse.pos.y = event.motion.y;
}
break;
case SDL_MOUSEBUTTONDOWN:
{
if (event.button.button == SDL_BUTTON_LEFT)
{
editor->mouse.left.just_pressed = true;
editor->mouse.left.just_released = false;
editor->mouse.left.down = true;
}
}
break;
case SDL_MOUSEBUTTONUP:
{
if (event.button.button == SDL_BUTTON_LEFT)
{
editor->mouse.left.just_pressed = false;
editor->mouse.left.just_released = true;
editor->mouse.left.down = false;
}
}
break;
case SDL_TEXTINPUT:
{
strncpy(editor->text_input.text, event.text.text, 32);
editor->text_input.has_input = true;
}
break;
default: break;
}
}
/* Redraw everything */
SDL_SetRenderDrawColor(editor->r, 34, 32, 52, SDL_ALPHA_OPAQUE);
SDL_RenderClear(editor->r);
editor_update(editor);
SDL_RenderPresent(editor->r);
/* Reset button states */
editor->mouse.left.just_pressed = false;
editor->mouse.left.just_released = false;
editor->text_input.has_input = false;
}
int
main(void)
{
#if 0
{
SDL_version compiled;
SDL_version linked;
SDL_VERSION(&compiled);
SDL_GetVersion(&linked);
SDL_Log("We compiled against SDL version %u.%u.%u ...\n",
compiled.major, compiled.minor, compiled.patch);
SDL_Log("But we are linking against SDL version %u.%u.%u.\n",
linked.major, linked.minor, linked.patch);
}
#endif
int return_code = EXIT_SUCCESS;
Arena *arena = arena_alloc(PERM_ARENA_SIZE);
if (arena)
{
Editor_State editor = {0};
editor.a = arena;
editor.is_running = true;
if (SDL_Init(SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER) == 0)
{
editor.w = SDL_CreateWindow(
"APU Toy",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
WINDOW_WIDTH, WINDOW_HEIGHT,
SDL_WINDOW_ALLOW_HIGHDPI);
if (editor.w)
{
/* Disable window resizing */
SDL_SetWindowResizable(editor.w, SDL_FALSE);
editor.r = SDL_CreateRenderer(editor.w, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (editor.r)
{
/* Initialize editor */
editor_init(&editor);
/* Initialize audio */
SDL_AudioSpec want, have;
SDL_AudioDeviceID dev;
SDL_memset(&want, 0, sizeof(want));
want.freq = 44100;
want.format = AUDIO_U8;
want.channels = 2;
want.samples = 2048;
want.callback = editor_audio_callback;
want.userdata = &editor;
if ((dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0)) > 0)
{
/* Set up audio device. */
editor.dev = dev;
SDL_PauseAudioDevice(editor.dev, 0);
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop_arg(update_loop, (void*)&editor, 60, 1);
#else
while (editor.is_running)
{
update_loop(&editor);
}
#endif
SDL_PauseAudioDevice(editor.dev, 1);
SDL_CloseAudioDevice(editor.dev);
}
else
{
SDL_Log("failed to initialize audio: %s", SDL_GetError());
return_code = EXIT_FAILURE;
}
editor_shutdown(&editor);
SDL_DestroyRenderer(editor.r);
}
else
{
SDL_Log("failed to create renderer: %s", SDL_GetError());
return_code = EXIT_FAILURE;
}
SDL_DestroyWindow(editor.w);
}
else
{
SDL_Log("unable to create window: %s", SDL_GetError());
return_code = EXIT_FAILURE;
}
SDL_Quit();
}
else
{
SDL_Log("unable to initialize SDL: %s", SDL_GetError());
return_code = EXIT_FAILURE;
}
arena_release(arena);
}
else
{
return_code = EXIT_FAILURE;
}
return(return_code);
}