/* 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 #include #include #include /* Internal */ #include "apu.h" /* SDL */ #include /* 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 #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); }