Skip to content

Instantly share code, notes, and snippets.

@simsnet
Created June 15, 2022 09:10
Show Gist options
  • Save simsnet/9ac5a4859cfeeb3da09e23241dc31258 to your computer and use it in GitHub Desktop.
Save simsnet/9ac5a4859cfeeb3da09e23241dc31258 to your computer and use it in GitHub Desktop.
The original Flipperlovania cover uploaded for historical purposes
#include <furi.h>
#include <furi_hal.h>
#include <gui/gui.h>
#include <input/input.h>
// "Flipperlovania"
// A remix of "Megalovania" from the video game "UNDERTALE" developed and composed by Toby Fox.
// Remixed by Simsnet#1754 using Notepad++ and FL Studio.
// Original music player code is believed to be created by https://github.com/skotopes, which featured a remix of the song "Marble Machine" composed by Wintergatan.
// TODO float note freq
typedef enum {
// Delay
N = 0,
// Octave 4
A_4 = 466,
B4 = 494,
// Octave 5
C5 = 523,
D5 = 587,
E5 = 659,
F5 = 698,
F_5 = 740,
G5 = 784,
G_5 = 831,
A5 = 880,
B5 = 988,
// Octave 6
C6 = 1047,
D6 = 1175,
E6 = 1319,
} MelodyEventNote;
typedef enum {
L4 = 4,
L8 = 8,
L16 = 16,
L128 = 128,
} MelodyEventLength;
typedef struct {
MelodyEventNote note;
MelodyEventLength length;
} MelodyEventRecord;
typedef struct {
const MelodyEventRecord* record;
int8_t loop_count;
} SongPattern;
const MelodyEventRecord melody_start[] = {
// section 1-1
{D5, L16}, {D5, L16}, {D6, L16}, {N, L16},
// section 1-2
{A5, L16}, {N, L8}, {G_5, L16},
// section 1-3
{N, L16}, {G5, L16}, {N, L16}, {F5, L16},
// section 1-4
{N, L16}, {D5, L16}, {F5, L16}, {G5, L16},
// section 2-1
{C5, L16}, {C5, L16}, {D6, L16}, {N, L16},
// section 2-2
{A5, L16}, {N, L8}, {G_5, L16},
// section 2-3
{N, L16}, {G5, L16}, {N, L16}, {F5, L16},
// section 2-4
{N, L16}, {D5, L16}, {F5, L16}, {G5, L16},
// section 3-1
{B4, L16}, {B4, L16}, {D6, L16}, {N, L16},
// section 3-2
{A5, L16}, {N, L8}, {G_5, L16},
// section 3-3
{N, L16}, {G5, L16}, {N, L16}, {F5, L16},
// section 3-4
{N, L16}, {D5, L16}, {F5, L16}, {G5, L16},
// section 4-1
{A_4, L16}, {A_4, L16}, {D6, L16}, {N, L16},
// section 4-2
{A5, L16}, {N, L8}, {G_5, L16},
// section 4-3
{N, L16}, {G5, L16}, {N, L16}, {F5, L16},
// section 4-4
{N, L16}, {D5, L16}, {F5, L16}, {G5, L16}
};
const MelodyEventRecord melody_loop[] = {
// section 1-1
{D5, L16}, {D5, L16}, {D6, L16}, {N, L16},
// section 1-2
{A5, L16}, {N, L8}, {G_5, L16},
// section 1-3
{N, L16}, {G5, L16}, {N, L16}, {F5, L16},
// section 1-4
{N, L16}, {D5, L16}, {F5, L16}, {G5, L16},
// section 2-1
{C5, L16}, {C5, L16}, {D6, L16}, {N, L16},
// section 2-2
{A5, L16}, {N, L8}, {G_5, L16},
// section 2-3
{N, L16}, {G5, L16}, {N, L16}, {F5, L16},
// section 2-4
{N, L16}, {D5, L16}, {F5, L16}, {G5, L16},
// section 3-1
{B4, L16}, {B4, L16}, {D6, L16}, {N, L16},
// section 3-2
{A5, L16}, {N, L8}, {G_5, L16},
// section 3-3
{N, L16}, {G5, L16}, {N, L16}, {F5, L16},
// section 3-4
{N, L16}, {D5, L16}, {F5, L16}, {G5, L16},
// section 4-1
{A_4, L16}, {A_4, L16}, {D6, L16}, {N, L16},
// section 4-2
{A5, L16}, {N, L8}, {G_5, L16},
// section 4-3
{N, L16}, {G5, L16}, {N, L16}, {F5, L16},
// section 4-4
{N, L16}, {D5, L16}, {F5, L16}, {G5, L16}
};
const MelodyEventRecord melody_chords_1bar[] = {
{E6, L8}, {N, L8}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128},
{B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128},
{B4, L128}, {E5, L128}, {B5, L8}, {N, L4}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128},
{B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128},
{B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {A5, L8}};
const SongPattern song[] = {{melody_start, 1}, {melody_loop, -1}};
typedef enum {
EventTypeTick,
EventTypeKey,
EventTypeNote,
// add your events type
} MusicDemoEventType;
typedef struct {
union {
InputEvent input;
const MelodyEventRecord* note_record;
} value;
MusicDemoEventType type;
} MusicDemoEvent;
typedef struct {
ValueMutex* state_mutex;
osMessageQueueId_t event_queue;
} MusicDemoContext;
#define note_stack_size 4
typedef struct {
// describe state here
const MelodyEventRecord* note_record;
const MelodyEventRecord* note_stack[note_stack_size];
uint8_t volume_id;
uint8_t volume_id_max;
} State;
const float volumes[] = {0, 0.02, 0.05, 0.1, 0.5};
bool is_white_note(const MelodyEventRecord* note_record, uint8_t id) {
if(note_record == NULL) return false;
switch(note_record->note) {
case C5:
if(id == 0) return true;
break;
case C6:
if(id == 0) return true;
break;
case D5:
if(id == 1) return true;
break;
case D6:
if(id == 1) return true;
break;
case E5:
if(id == 2) return true;
break;
case E6:
if(id == 2) return true;
break;
case F5:
if(id == 3) return true;
break;
case F_5:
if(id == 9) return true;
break;
case G5:
if(id == 4) return true;
break;
case G_5:
if(id == 10) return true;
break;
case A_4:
if(id == 12) return true;
break;
case A5:
if(id == 5) return true;
break;
case B4:
if(id == 6) return true;
break;
case B5:
if(id == 6) return true;
break;
default:
break;
}
return false;
}
bool is_black_note(const MelodyEventRecord* note_record, uint8_t id) {
if(note_record == NULL) return false;
switch(note_record->note) {
case F_5:
if(id == 3) return true;
break;
default:
break;
}
return false;
}
const char* get_note_name(const MelodyEventRecord* note_record) {
if(note_record == NULL) return "";
switch(note_record->note) {
case N:
return "---";
break;
case A_4:
return "A#4-";
break;
case B4:
return "B4-";
break;
case C5:
return "C5-";
break;
case D5:
return "D5-";
break;
case E5:
return "E5-";
break;
case F5:
return "F5";
break;
case F_5:
return "F#5";
break;
case G5:
return "G5-";
break;
case G_5:
return "G#5-";
break;
case A5:
return "A5-";
break;
case C6:
return "C6-";
break;
case D6:
return "D6-";
break;
case E6:
return "E6-";
break;
default:
return "UNK";
break;
}
}
const char* get_note_len_name(const MelodyEventRecord* note_record) {
if(note_record == NULL) return "";
switch(note_record->length) {
case L4:
return "4-";
break;
case L8:
return "8-";
break;
case L16:
return "16";
break;
case L128:
return "1+";
break;
default:
return "--";
break;
}
}
static void render_callback(Canvas* canvas, void* ctx) {
State* state = (State*)acquire_mutex((ValueMutex*)ctx, 25);
canvas_clear(canvas);
canvas_set_color(canvas, ColorBlack);
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 0, 12, "Flipperlovania");
uint8_t x_pos = 0;
uint8_t y_pos = 24;
const uint8_t white_w = 10;
const uint8_t white_h = 40;
const int8_t black_x = 6;
const int8_t black_y = -5;
const uint8_t black_w = 8;
const uint8_t black_h = 32;
// white keys
for(size_t i = 0; i < 7; i++) {
if(is_white_note(state->note_record, i)) {
canvas_draw_box(canvas, x_pos + white_w * i, y_pos, white_w + 1, white_h);
} else {
canvas_draw_frame(canvas, x_pos + white_w * i, y_pos, white_w + 1, white_h);
}
}
// black keys
for(size_t i = 0; i < 7; i++) {
if(i != 2 && i != 6) {
canvas_set_color(canvas, ColorWhite);
canvas_draw_box(
canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h);
canvas_set_color(canvas, ColorBlack);
if(is_black_note(state->note_record, i)) {
canvas_draw_box(
canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h);
} else {
canvas_draw_frame(
canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h);
}
}
}
// volume view_port
x_pos = 124;
y_pos = 0;
const uint8_t volume_h = (64 / (state->volume_id_max - 1)) * state->volume_id;
canvas_draw_frame(canvas, x_pos, y_pos, 4, 64);
canvas_draw_box(canvas, x_pos, y_pos + (64 - volume_h), 4, volume_h);
// note stack view_port
x_pos = 73;
y_pos = 0;
canvas_set_color(canvas, ColorBlack);
canvas_set_font(canvas, FontPrimary);
canvas_draw_frame(canvas, x_pos, y_pos, 49, 64);
canvas_draw_line(canvas, x_pos + 28, 0, x_pos + 28, 64);
for(uint8_t i = 0; i < note_stack_size; i++) {
if(i == 0) {
canvas_draw_box(canvas, x_pos, y_pos + 48, 49, 16);
canvas_set_color(canvas, ColorWhite);
} else {
canvas_set_color(canvas, ColorBlack);
}
canvas_draw_str(canvas, x_pos + 4, 64 - 16 * i - 3, get_note_name(state->note_stack[i]));
canvas_draw_str(
canvas, x_pos + 31, 64 - 16 * i - 3, get_note_len_name(state->note_stack[i]));
canvas_draw_line(canvas, x_pos, 64 - 16 * i, x_pos + 48, 64 - 16 * i);
}
release_mutex((ValueMutex*)ctx, state);
}
static void input_callback(InputEvent* input_event, void* ctx) {
osMessageQueueId_t event_queue = ctx;
MusicDemoEvent event;
event.type = EventTypeKey;
event.value.input = *input_event;
osMessageQueuePut(event_queue, &event, 0, 0);
}
void process_note(
const MelodyEventRecord* note_record,
float bar_length_ms,
MusicDemoContext* context) {
MusicDemoEvent event;
// send note event
event.type = EventTypeNote;
event.value.note_record = note_record;
osMessageQueuePut(context->event_queue, &event, 0, 0);
// read volume
State* state = (State*)acquire_mutex(context->state_mutex, 25);
float volume = volumes[state->volume_id];
release_mutex(context->state_mutex, state);
// play note
float note_delay = bar_length_ms / (float)note_record->length;
if(note_record->note != N) {
hal_pwm_set(volume, note_record->note, &SPEAKER_TIM, SPEAKER_CH);
}
delay(note_delay);
hal_pwm_stop(&SPEAKER_TIM, SPEAKER_CH);
}
void music_player_thread(void* p) {
MusicDemoContext* context = (MusicDemoContext*)p;
const float bpm = 130.0f;
// 4/4
const float bar_length_ms = (60.0f * 1000.0f / bpm) * 4;
const uint16_t melody_start_events_count = sizeof(melody_start) / sizeof(melody_start[0]);
const uint16_t melody_loop_events_count = sizeof(melody_loop) / sizeof(melody_loop[0]);
for(size_t i = 0; i < melody_start_events_count; i++) {
process_note(&melody_start[i], bar_length_ms, context);
}
while(1) {
for(size_t i = 0; i < melody_loop_events_count; i++) {
process_note(&melody_loop[i], bar_length_ms, context);
}
}
}
int32_t music_player_app(void* p) {
osMessageQueueId_t event_queue = osMessageQueueNew(8, sizeof(MusicDemoEvent), NULL);
State _state;
_state.note_record = NULL;
for(size_t i = 0; i < note_stack_size; i++) {
_state.note_stack[i] = NULL;
}
_state.volume_id = 1;
_state.volume_id_max = sizeof(volumes) / sizeof(volumes[0]);
ValueMutex state_mutex;
if(!init_mutex(&state_mutex, &_state, sizeof(State))) {
printf("cannot create mutex\r\n");
return 255;
}
ViewPort* view_port = view_port_alloc();
view_port_draw_callback_set(view_port, render_callback, &state_mutex);
view_port_input_callback_set(view_port, input_callback, event_queue);
// Open GUI and register view_port
Gui* gui = furi_record_open("gui");
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
// start player thread
// TODO change to fuirac_start
osThreadAttr_t player_attr = {.name = "music_player_thread", .stack_size = 512};
MusicDemoContext context = {.state_mutex = &state_mutex, .event_queue = event_queue};
osThreadId_t player = osThreadNew(music_player_thread, &context, &player_attr);
if(player == NULL) {
printf("cannot create player thread\r\n");
return 255;
}
MusicDemoEvent event;
while(1) {
osStatus_t event_status = osMessageQueueGet(event_queue, &event, NULL, 100);
State* state = (State*)acquire_mutex_block(&state_mutex);
if(event_status == osOK) {
if(event.type == EventTypeKey) {
// press events
if(event.value.input.type == InputTypeShort &&
event.value.input.key == InputKeyBack) {
release_mutex(&state_mutex, state);
break;
}
if(event.value.input.type == InputTypePress &&
event.value.input.key == InputKeyUp) {
if(state->volume_id < state->volume_id_max - 1) state->volume_id++;
}
if(event.value.input.type == InputTypePress &&
event.value.input.key == InputKeyDown) {
if(state->volume_id > 0) state->volume_id--;
}
if(event.value.input.type == InputTypePress &&
event.value.input.key == InputKeyLeft) {
}
if(event.value.input.type == InputTypePress &&
event.value.input.key == InputKeyRight) {
}
if(event.value.input.key == InputKeyOk) {
}
} else if(event.type == EventTypeNote) {
state->note_record = event.value.note_record;
for(size_t i = note_stack_size - 1; i > 0; i--) {
state->note_stack[i] = state->note_stack[i - 1];
}
state->note_stack[0] = state->note_record;
}
} else {
// event timeout
}
view_port_update(view_port);
release_mutex(&state_mutex, state);
}
osThreadTerminate(player);
hal_pwm_stop(&SPEAKER_TIM, SPEAKER_CH);
view_port_enabled_set(view_port, false);
gui_remove_view_port(gui, view_port);
furi_record_close("gui");
view_port_free(view_port);
osMessageQueueDelete(event_queue);
delete_mutex(&state_mutex);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment