Last active
August 23, 2026 18:38
-
-
Save mlabbe/089fe7c1ae6840cd520fdd95e1c07019 to your computer and use it in GitHub Desktop.
My first Odin program - a random guitar tab generator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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------------------------------ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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