WASM4 Notes
Bhathiya Perera
Hint: Something like Nokia ringtone maker for WASM4
# =========================================================================================
# ________________________________
# / o oooo ooo oooo o o o /\
# / oo ooo oo oooo o o o / /
# / _________________________ / /
# / // / // /// // /// // /// / / / /
# /___ //////////////////////////_/ /
# \____\________________________\_\/
#
# **notes**: Application by -- Bhathiya Perera --
# For WASM4 Fantasy Console
#
# =========================================================================================
# ASCII ART by Forrest Cook -- reference https://asciiart.website/index.php?art=music/pianos
# Note Frequencies -- reference https://pages.mtu.edu/~suits/notefreqs.html
# Nokia composer web app -- reference https://zserge.com/nokia-composer/h/
# =========================================================================================
# Nokia Tune:
# 3e3 3d3 2f#2 2g#2 3c#3 3b2 2d2 2e2 3b2 3a2 2c#2 2e2 1a2 1R
# =========================================================================================
import w4
import libs.numbers as num
import libs.perlin
import libs.random
import libs.c as mini_clib
# Total notes allowed
MAX_NOTES :Const[int] = 256
FONT_WIDTH :Const[int] = 8
NOTE_WRAP_AROUND:Const[int] = 55
# ======= Notes ===========
NOTE_C: Const[u8] = 1u8
NOTE_C_S: Const[u8] = 2u8
NOTE_D: Const[u8] = 3u8
NOTE_D_S: Const[u8] = 4u8
NOTE_E: Const[u8] = 5u8
NOTE_F: Const[u8] = 6u8
NOTE_F_S: Const[u8] = 7u8
NOTE_G: Const[u8] = 8u8
NOTE_G_S: Const[u8] = 9u8
NOTE_A: Const[u8] = 10u8
NOTE_A_S: Const[u8] = 11u8
NOTE_B: Const[u8] = 12u8
NOTE_REST:Const[u8] = 13u8
# Play until we find this
NOTE_NULL:Const[u8] = 0u8
# ======= Notes Time ======
# 1/pow(2, TIME)
TIME_64: Const[u8] = 6u8
TIME_32: Const[u8] = 5u8
TIME_16: Const[u8] = 4u8
TIME_8: Const[u8] = 3u8
TIME_4: Const[u8] = 2u8
TIME_HALF:Const[u8] = 1u8
TIME_FULL:Const[u8] = 0u8
# ====Note Octaves ========
OCTAVE_1 :Const[u8] = 0u8
OCTAVE_2 :Const[u8] = 1u8
OCTAVE_3 :Const[u8] = 2u8
# == Editing Modes ========
MODE_TIME:Const[u8] = 0u8
MODE_NOTE:Const[u8] = 1u8
MODE_OCT :Const[u8] = 2u8
MODE_DEL :Const[u8] = 3u8
# max, min mode
TOTAL_MODES :Const[u8] = 4u8
class State:
width: int
height: int
frame_count: u64
gamepad_prev: u8
text_buf: Array[u8]
# Tuple [u8 - time, u8 - note, u8 - octave, u8 - extra]
note_buf: Array[Tuple[u8,u8,u8,u8]]
note_freq: Array[int]
note_time: Array[int]
# Cursor position
cursor: int
prev_cursor: int
v_start: int
v_end: int
note_count: int
mode: u8
playing: bool
play_on: u64
def clear_buf(target: Array[u8]) -> None:
target[0] = 0u8
target[1] = 0u8
target[2] = 0u8
target[3] = 0u8
target[4] = 0u8
target[5] = 0u8
target[6] = 0u8
target[7] = 0u8
target[8] = 0u8
target[9] = 0u8
target[10] = 0u8
def i2s(x: int, target: Array[u8]) -> bool:
# Convert given integer to a string
# Support up to 10 character output (size 11 buffer is required)
charset: Ptr[Const[u8]] = binarydata("0123456789- \0")
clear_buf(target)
pos: int = 0
y: int = x
if x > 9999999999 or x < -999999999:
target[0] = cast("u8", charat(cast("str", charset), 10))
return False
if x < 0:
y *= -1
target[pos] = cast("u8", charat(cast("str", charset), 10))
else:
target[pos] = cast("u8", charat(cast("str", charset), 11))
pos += 1
if y == 0:
target[pos] = cast("u8", charat(cast("str", charset), 0))
pos += 1
while y != 0:
character: int = y % 10
target[pos] = cast("u8", charat(cast("str", charset), character))
pos += 1
y /= 10
# Reverse from 1 to pos
rpos: int = 1
rmax: int = (pos / 2) + 1
while rpos < rmax:
temp: u8 = target[rpos]
target[rpos] = target[pos - rpos]
target[pos - rpos] = temp
rpos += 1
return True
def del_note(s: State) -> None:
if s.note_count == 0:
return
x: int = s.cursor + 1
last: int = s.note_count - 1
while x <= last:
s.note_buf[x - 1][0] = s.note_buf[x][0]
s.note_buf[x - 1][1] = s.note_buf[x][1]
s.note_buf[x - 1][2] = s.note_buf[x][2]
s.note_buf[x - 1][3] = s.note_buf[x][3]
x += 1
s.note_buf[last][0] = NOTE_NULL
s.note_buf[last][1] = NOTE_NULL
s.note_buf[last][2] = NOTE_NULL
s.note_buf[last][3] = NOTE_NULL
s.note_count -= 1
if s.cursor >= s.note_count:
s.cursor = s.note_count - 1
if s.cursor < 0:
s.cursor = 0
def add_left(s: State) -> None:
# We are full
if s.note_count == MAX_NOTES:
return
if s.note_count == 0:
s.note_buf[0][0] = TIME_4
s.note_buf[0][1] = NOTE_C
s.note_buf[0][2] = OCTAVE_2
s.note_buf[0][3] = 0u8
s.note_count = 1
s.cursor = 0
return
# Add at cursor location + 1
# Move everything from right most to + 1 position
x: int = s.note_count - 1
while x >= s.cursor:
s.note_buf[x + 1][0] = s.note_buf[x][0]
s.note_buf[x + 1][1] = s.note_buf[x][1]
s.note_buf[x + 1][2] = s.note_buf[x][2]
s.note_buf[x + 1][3] = s.note_buf[x][3]
x -= 1
s.note_buf[s.cursor][0] = TIME_4
# Alternatively add note c and note rest
if s.note_count % 2 != 0:
s.note_buf[s.cursor][1] = NOTE_C
else:
s.note_buf[s.cursor][1] = NOTE_REST
s.note_buf[s.cursor][2] = OCTAVE_2
s.note_buf[s.cursor][3] = 0u8
s.note_count += 1
def add_right(s: State) -> None:
# We are full
if s.note_count == MAX_NOTES:
return
if s.note_count == 0:
s.note_buf[0][0] = TIME_4
s.note_buf[0][1] = NOTE_C
s.note_buf[0][2] = OCTAVE_2
s.note_buf[0][3] = 0u8
s.note_count = 1
s.cursor = 0
return
# Add at cursor location + 1
# Move everything from right most to + 1 position
x: int = s.note_count - 1
while x >= s.cursor + 1:
s.note_buf[x + 1][0] = s.note_buf[x][0]
s.note_buf[x + 1][1] = s.note_buf[x][1]
s.note_buf[x + 1][2] = s.note_buf[x][2]
s.note_buf[x + 1][3] = s.note_buf[x][3]
x -= 1
s.note_buf[s.cursor + 1][0] = TIME_4
# Alternatively add note c and note rest
if s.note_count % 2 != 0:
s.note_buf[s.cursor + 1][1] = NOTE_C
else:
s.note_buf[s.cursor + 1][1] = NOTE_REST
s.note_buf[s.cursor + 1][2] = OCTAVE_2
s.note_buf[s.cursor + 1][3] = 0u8
s.note_count += 1
s.cursor += 1
def up_note(s: State) -> None:
if s.note_count == 0:
return
if s.mode == MODE_TIME:
s.note_buf[s.cursor][0] += 1u8
if s.note_buf[s.cursor][0] > TIME_64:
s.note_buf[s.cursor][0] = TIME_FULL
if s.mode == MODE_NOTE:
s.note_buf[s.cursor][1] += 1u8
if s.note_buf[s.cursor][1] > NOTE_REST:
s.note_buf[s.cursor][1] = NOTE_C
if s.mode == MODE_OCT:
s.note_buf[s.cursor][2] += 1u8
if s.note_buf[s.cursor][2] > OCTAVE_3:
s.note_buf[s.cursor][2] = OCTAVE_1
def down_note(s: State) -> None:
if s.note_count == 0:
return
if s.mode == MODE_TIME:
if s.note_buf[s.cursor][0] == TIME_FULL:
s.note_buf[s.cursor][0] = TIME_64
else:
s.note_buf[s.cursor][0] -= 1u8
if s.mode == MODE_NOTE:
if s.note_buf[s.cursor][1] == NOTE_C:
s.note_buf[s.cursor][1] = NOTE_REST
else:
s.note_buf[s.cursor][1] -= 1u8
if s.mode == MODE_OCT:
if s.note_buf[s.cursor][2] == OCTAVE_1:
s.note_buf[s.cursor][2] = OCTAVE_3
else:
s.note_buf[s.cursor][2] -= 1u8
def handle_input(s: State) -> None:
just_pressed: u8 = w4.gamepad1() & (w4.gamepad1() ^ s.gamepad_prev)
if just_pressed & w4.BUTTON_2 != 0u8:
if s.playing:
# Stop
s.playing = False
s.cursor = s.prev_cursor
s.mode = MODE_NOTE
elif w4.gamepad1() & w4.BUTTON_1 != 0u8:
# Play
if s.note_count == 0:
return
s.prev_cursor = s.cursor
s.cursor = 0
s.playing = True
elif s.mode == MODE_DEL:
# Delete
del_note(s)
elif s.mode == MODE_OCT:
# Add left
add_left(s)
else:
add_right(s)
# Only if we are not playing
if not s.playing:
if just_pressed & w4.BUTTON_1 != 0u8:
s.mode += 1u8
s.mode %= TOTAL_MODES
if just_pressed & w4.BUTTON_UP != 0u8:
up_note(s)
if just_pressed & w4.BUTTON_DOWN != 0u8:
down_note(s)
if just_pressed & w4.BUTTON_LEFT != 0u8:
s.cursor -= 1
# Wrap around
if s.cursor < 0:
s.cursor = s.note_count - 1
if just_pressed & w4.BUTTON_RIGHT != 0u8:
s.cursor += 1
# Wrap around
if s.cursor > s.note_count - 1:
s.cursor = 0
s.gamepad_prev = w4.gamepad1()
def draw_board(s: State) -> None:
w4.set_draw_colors(0x0014u16)
w4.text_u8(binarydata("mode:\0"), 2, 2)
i2s(s.note_count, s.text_buf)
w4.text_u8(cast("Ptr[Const[u8]]", s.text_buf), 120, 2)
w4.vline(118, 0, 10u32)
w4.vline(79, 0, 10u32)
w4.set_draw_colors(0x0012u16)
if s.note_count == 0:
i2s(0, s.text_buf)
else:
i2s(s.cursor + 1, s.text_buf)
w4.text_u8(cast("Ptr[Const[u8]]", s.text_buf), 80, 2)
w4.hline(0, 10, 160u32)
w4.hline(0, s.height - 10, 160u32)
if s.playing:
w4.text_u8(binarydata("play\0"), 44, 2)
w4.text_u8(binarydata("stop\0"), 55, s.height - 10 + 2)
elif s.mode == MODE_TIME:
w4.text_u8(binarydata("time\0"), 44, 2)
w4.text_u8(binarydata("add\x85\0"), 55, s.height - 10 + 2)
elif s.mode == MODE_NOTE:
w4.text_u8(binarydata("note\0"), 44, 2)
w4.text_u8(binarydata("add\x85\0"), 55, s.height - 10 + 2)
elif s.mode == MODE_OCT:
w4.text_u8(binarydata("octa\0"), 44, 2)
w4.text_u8(binarydata("add\x84\0"), 55, s.height - 10 + 2)
elif s.mode == MODE_DEL:
w4.set_draw_colors(0x0013u16)
w4.text_u8(binarydata("del \0"), 44, 2)
w4.text_u8(binarydata("del \0"), 55, s.height - 10 + 2)
w4.set_draw_colors(0x0014u16)
if not s.playing:
w4.text_u8(binarydata("\x80\0"), 2, s.height - 10 + 2)
w4.text_u8(binarydata("\x81\0"), 45, s.height - 10 + 2)
if not s.playing:
w4.text_u8(binarydata("\x80+\x81\0"), 88, s.height - 10 + 2)
w4.set_draw_colors(0x0012u16)
w4.text_u8(binarydata("mode\0"), 12, s.height - 10 + 2)
if not s.playing:
w4.text_u8(binarydata("play\0"), 114, s.height - 10 + 2)
def draw_note(s: State, grid_pos: int, note_pos: int, cursor: bool) -> None:
note_data: Array[u8] = cast("Array[u8]", binarydata("cdefgabR#0"))
# Do not draw empty notes
if s.note_buf[note_pos][1] == NOTE_NULL:
return
x: int = 4 + (grid_pos % 4) * 40
y: int = 12 + (grid_pos / 4) * 10
x_delta: int = 0
clear_buf(s.text_buf)
# Put the time indicator to the buffer
s.text_buf[0] = note_data[9] + s.note_buf[note_pos][0]
if cursor:
w4.set_draw_colors(0x0042u16)
else:
w4.set_draw_colors(0x0012u16)
w4.text_u8(cast("Ptr[Const[u8]]", s.text_buf), x + x_delta, y)
x_delta += FONT_WIDTH
# Put note indicator
note: u8 = s.note_buf[note_pos][1]
if note == NOTE_C or note == NOTE_C_S:
s.text_buf[0] = note_data[0]
if note == NOTE_D or note == NOTE_D_S:
s.text_buf[0] = note_data[1]
if note == NOTE_E:
s.text_buf[0] = note_data[2]
if note == NOTE_F or note == NOTE_F_S:
s.text_buf[0] = note_data[3]
if note == NOTE_G or note == NOTE_G_S:
s.text_buf[0] = note_data[4]
if note == NOTE_A or note == NOTE_A_S:
s.text_buf[0] = note_data[5]
if note == NOTE_B:
s.text_buf[0] = note_data[6]
if note == NOTE_REST:
s.text_buf[0] = note_data[7]
elif note == NOTE_C_S or note == NOTE_D_S or note == NOTE_F_S or note == NOTE_G_S or note == NOTE_A_S:
s.text_buf[1] = note_data[8]
if cursor:
w4.set_draw_colors(0x0043u16)
else:
w4.set_draw_colors(0x0013u16)
w4.text_u8(cast("Ptr[Const[u8]]", s.text_buf), x + x_delta, y)
x_delta += FONT_WIDTH
if s.text_buf[1] != 0u8:
# handle '#' sign
x_delta += FONT_WIDTH
s.text_buf[1] = 0u8
# Put octave indicator
if note != NOTE_REST:
s.text_buf[0] = note_data[9] + 1u8 + s.note_buf[note_pos][2]
if cursor:
w4.set_draw_colors(0x0041u16)
else:
w4.set_draw_colors(0x0014u16)
w4.text_u8(cast("Ptr[Const[u8]]", s.text_buf), x + x_delta, y)
def draw_notes(s: State) -> None:
# Nothing to draw
if s.note_count == 0:
return
if s.cursor < s.v_start:
# moved left
s.v_start = s.cursor
s.v_end = s.v_start + NOTE_WRAP_AROUND
elif s.cursor > s.v_end:
# moved right
s.v_end = s.cursor
s.v_start = s.v_end - NOTE_WRAP_AROUND
else:
s.v_end = s.v_start + NOTE_WRAP_AROUND
if s.v_start < 0:
s.v_start = 0
if s.v_end > s.note_count - 1:
s.v_end = s.note_count - 1
x: int = s.v_start
grid_pos: int = 0
while x < s.note_count and x <= s.v_end:
current: bool = x == s.cursor
# Show current playing in playing mode
if s.playing:
prev: int = x - 1
if prev < 0:
prev = s.note_count - 1
current = prev == s.cursor
draw_note(s, grid_pos, x, current)
x += 1
grid_pos += 1
def play_notes(s: State) -> None:
if not s.playing:
return
# Play the next note on nth frame
if s.play_on > s.frame_count:
return
time: u8 = s.note_buf[s.cursor][0]
note: u8 = s.note_buf[s.cursor][1]
octv: u8 = s.note_buf[s.cursor][2]
actual_note: u8 = octv * 12u8 + note
note_time: u32 = cast("u32", s.note_time[time])
s.play_on = s.frame_count + cast("u64", note_time)
if note != NOTE_REST:
note_freq: u32 = cast("u32", s.note_freq[actual_note])
w4.tone(note_freq, note_time, 50u32, 0u32)
s.cursor += 1
if s.cursor > s.note_count - 1:
s.cursor = 0
def game_step(d: AnyPtr) -> None:
s: State = cast("State", d)
s.frame_count += 1u64
handle_input(s)
draw_board(s)
draw_notes(s)
play_notes(s)
def init_state() -> State:
s: State = State()
s.width = 160
s.height = 160
s.frame_count = 0u64
s.play_on = 0u64
s.gamepad_prev = 0u8
s.cursor = 0
s.note_count = 0
s.v_start = 0
s.v_end = 0
s.mode = MODE_NOTE
s.playing = False
s.note_freq = array("int",0,262,277,294,311,330,349,370,392,415,440,466,495,523,554,587,622,659,698,740,784,830,880,932,988,1047,1109,1175,1245,1319,1397,1480,1568,1661,1760,1865,1976)
s.note_time = array("int",60,30,15,8,4,2,1)
# 11 character buffer for writing stuff
s.text_buf = cast("Array[u8]", mini_clib.calloc(cast("mini_clib.Size", 1), cast("mini_clib.Size", 11)))
# Set total number of notes to max notes
arrsetlen(s.note_buf, MAX_NOTES)
# Mark it all as null
x: int = 0
while x < MAX_NOTES:
s.note_buf[x][0] = NOTE_NULL
s.note_buf[x][1] = NOTE_NULL
s.note_buf[x][2] = NOTE_NULL
s.note_buf[x][3] = NOTE_NULL
x += 1
return s
def del_state(current: AnyPtr) -> None:
del current
def main() -> int:
# based on palette
# https://lospec.com/palette-list/sweet-28
# https://lospec.com/palette-list/bread-berry
w4.set_palette(0xfff678u32, 0x1fa4bfu32, 0xda4fbcu32, 0x1b1b5cu32)
s: State = init_state()
w4.set_game_state(cast("AnyPtr", s))
return 0