Skip to content

Instantly share code, notes, and snippets.

@mlabbe
Last active August 23, 2026 18:38
Show Gist options
  • Select an option

  • Save mlabbe/089fe7c1ae6840cd520fdd95e1c07019 to your computer and use it in GitHub Desktop.

Select an option

Save mlabbe/089fe7c1ae6840cd520fdd95e1c07019 to your computer and use it in GitHub Desktop.
My first Odin program - a random guitar tab generator
Some sample outputs from successive runs.
e|---------------------------------------------
B|---------------------------------------------
G|---------------------------------------------
D|--8---5---6-------6---3---3---6---3---3------
A|------------------------------------------3--
E|--------------6------------------------------
e|------7---4---1------------------------------
B|--5---------------1---3---1-----------3---3--
G|------------------------------1---3----------
D|---------------------------------------------
A|---------------------------------------------
E|---------------------------------------------
e|-----------------------------------------
B|------------------5----------------------
G|--5-------------------5---5---5---3---6--
D|------5-------5--------------------------
A|----------5------------------------------
E|-----------------------------------------
e|-----------------------------------------
B|-----------------------------------------
G|--------------------------5-----------4--
D|----------------------3-------5---2------
A|--------------1---1----------------------
E|--2---3---1------------------------------
package main
import "core:fmt"
import "core:strings"
import "core:math/rand"
STRING_NOTES :: "eBGDAE"
NUM_STRINGS :: len(STRING_NOTES)
MAX_FRET :: 24
Tab_Cell :: struct {
fret: i8
}
Guitar_Frame :: struct {
// 0 = high E string, 5 = low E
guitar_strings: [NUM_STRINGS]Tab_Cell,
}
Guitar_Timeline :: struct {
frames: [dynamic]Guitar_Frame
}
generate_tab_cell :: proc(builder :^strings.Builder, gstring: Tab_Cell) {
if gstring.fret < 0 {
fmt.sbprint(builder, "----")
} else if gstring.fret < 10 {
fmt.sbprintf(builder, "-%d--", gstring.fret)
} else {
fmt.sbprintf(builder, "-%d-", gstring.fret)
}
}
generate_timeline :: proc(timeline: ^Guitar_Timeline) -> string {
builder := strings.builder_make() or_else panic("builder")
defer strings.builder_destroy(&builder)
string_notes := STRING_NOTES
for row := 0; row < NUM_STRINGS; row += 1 {
// write string note name before getting into it
fmt.sbprintf(&builder, "%c|-", string_notes[row])
for col := 0; col < len(timeline.frames); col += 1 {
gstring := timeline.frames[col].guitar_strings[row]
generate_tab_cell(&builder, gstring)
}
fmt.sbprintln(&builder)
}
return strings.clone(strings.to_string(builder)) or_else panic("alloc")
}
make_guitar_frame_one_note :: proc(string_num: int, fret_num: i8) -> Guitar_Frame {
fmt.assertf(
0 <= string_num && string_num < NUM_STRINGS,
"string number %d is out of range [0, %d)",
string_num,
NUM_STRINGS,
)
frame := Guitar_Frame{
guitar_strings = {
0..<NUM_STRINGS = {fret = -1},
},
}
frame.guitar_strings[string_num].fret = fret_num
return frame
}
// single note at a time grammar
append_guitar_note_run :: proc(timeline: ^Guitar_Timeline, grammar: string) -> bool {
// grammar is:
// Letters: select a string. Lowercase 'e' is high E
// Numbers: fret number for the current chosen string
// '-': separates numbers, removes digit ambiguity, no whitespace
//
// "e-2-0-2-A12-10-0"
guitar_string_index :: proc(c: rune) -> (index: int, ok: bool) {
switch c {
case 'E': return 5, true // Low E
case 'A': return 4, true
case 'D': return 3, true
case 'G': return 2, true
case 'B': return 1, true
case 'e': return 0, true // High E
}
return 0, false
}
current_string := -1
fret: i8 = 0
have_fret := false
for ch in grammar {
switch ch {
case 'E', 'A', 'D', 'G', 'B', 'e':
if have_fret {
return false
}
index, ok := guitar_string_index(ch)
if !ok {
return false
}
current_string = index
case '0'..='9':
if current_string < 0 {
return false
}
digit := int(ch-'0')
have_fret = true
fret = fret*10 + i8(ch-'0')
if fret > MAX_FRET {
return false
}
case '-':
if have_fret {
frame := make_guitar_frame_one_note(current_string, fret)
append(&timeline.frames, frame)
}
fret = 0
have_fret = false
case:
return false
}
}
// commit final number
if have_fret {
frame := make_guitar_frame_one_note(current_string, fret)
append(&timeline.frames, frame)
}
return true
}
generate_random_one_note_grammar :: proc(num_events:int) -> string {
builder := strings.builder_make_len_cap(
0,
num_events * 4,
) or_else panic("builder allocation failed")
defer strings.builder_destroy(&builder)
bounce :: proc(val: int, upper_bound: int) -> int {
p := 2 * upper_bound
rem := (val % p + p) % p
return rem > upper_bound ? p - rem : rem
}
random_sign_flip :: proc() -> int {
return rand.int_range(0, 2)==1 ? 1 : -1
}
current_string := rand.int_range(0, NUM_STRINGS)
current_fret := rand.int_range(0, 12)
last_string := -1
string_notes := STRING_NOTES
for i := 0; i < num_events; i += 1 {
if current_string != last_string {
fmt.sbprintf(&builder, "%c-", string_notes[current_string])
last_string = current_string
// intentionally consume an event, effectively randomizing
// overall length
continue
}
fmt.sbprintf(&builder, "%d-", current_fret)
// determine new note as positional offset from last one
// pedantry: a string shift could have occurred, and the
// intervallic comments below do not necessarily take this
// into account.
switch rand.int_range(0, 9) {
case 0..=2:
// chances are: shift one string
string_skip := rand.int_range(0, 4)
shift_amount := 1
if string_skip == 3 {
shift_amount = 2
}
shift_amount *= random_sign_flip()
current_string = bounce(current_string + shift_amount, NUM_STRINGS-1)
fmt.assertf(0 <= current_string && current_string < NUM_STRINGS, "string number is out of range")
case 3:
// major 2nd
current_fret += 2 * random_sign_flip()
case 4..=5:
// minor 3rd
current_fret += 3 * random_sign_flip()
case 6:
// ascending minor 2nd
current_fret += 1
case 7:
// do nothing
case:
// find an ergonomic p5th interval -- ascend if possible,
// descend if not
if current_string > 0 {
// ascending p5th
if current_string == 2 {
// g -> b, 3 frets
current_fret += 3
} else {
// otherwise, add 2 frets
current_fret += 2
}
current_string -= 1
} else {
// on high e string - do a descending fifth
if current_fret >= 2 {
current_string = 1
current_fret -= 2
} else {
// handle frets 0 and 1
current_string = 2
current_fret += 2
}
}
}
current_fret = bounce(current_fret, MAX_FRET)
}
return strings.clone(strings.to_string(builder)) or_else panic("alloc failed")
}
main :: proc() {
timeline := Guitar_Timeline{
frames = make([dynamic]Guitar_Frame),
}
defer delete(timeline.frames)
grammar_str := generate_random_one_note_grammar(16)
defer delete(grammar_str);
ok := append_guitar_note_run(&timeline, grammar_str)
assert(ok, "generated invalid grammar")
timeline_buf := generate_timeline(&timeline)
defer delete(timeline_buf)
fmt.print(timeline_buf)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment