fengctor / octune

A DSL for creating 8-bit style music
BSD 3-Clause "New" or "Revised" License
22 stars 1 forks source link

Octune

Octune logo

Logo made by @Weelam šŸ™

Octune is a domain-specific language (DSL) for creating 8-bit music, compiling to WAV. The name is a pun on "oct-" for the 8-bit music produced and "tune" for, well, tune šŸ™‚.

Usage

Octune is used simply by feeding the executable the Octune song files (.otn) used for constructing the song. Using the module name, say M, containing main, it will produce M.wav in the current directory. For example, if mainModule.otn declares module Main containing main and possibly uses variables from other1.otn ... otherk.otn, path/to/octune mainModule.otn other1.otn ... otherk.otn produces Main.wav if the files are successfully compiled.

Use the -o/--output option to choose the name of the song produced; -o SongName will output the song to SongName.wav.

The --check option will perform the checks done before sample generation without generating samples and outputting them to a file. This is useful for periodically verifying your Octune files are well-formed.

Language Description

Syntax

The syntax for an Octune file is given by the following grammar in EBNF:

<file> ::= "module" <module_path> {<declaration>}+

<module_path> ::= <module_component>
               |  <module_component> , ".", <module_path>

<module_component> ::= capital letter followed by 0 or more letters

<declaration> ::= <variable> "=" <song>
               |  <variable> "=" <line_expression>

<variable> ::= lowercase letter followed by 0 or more alphanumeric characters

<song> ::= "{" <bpm> ":" <line_expression> "}"

<bpm> ::= positive integer

<line_expression> ::= <note>
                   |  <variable_usage>
                   |  <sequence>
                   |  <merge>
                   |  <repeat>
                   |  <waveform_choice>
                   |  <volume_modify>

<note> ::= {<note_modifier>}, <beats>, <sound>

<note_modifier> ::= "'"
                 |  "''"

<beats> ::= <non_negative_decimal>
         |  <musical_length>, <dots>

<non_negative_decimal> ::= non-negative decimal number

<musical_length> ::= "t" | "s" | "e" | "q" | "h"

<dots> ::= ""
        |  ".", <dots>

<sound> ::= "_"
         |  <pitch>
         |  <percussion>

<pitch> ::= <letter>, [<accidental>], <octave>

<letter> ::= "A" | "B" | ... | "G"

<accidental> ::= "b" | "#"

<octave> ::= "0" | "1" | ... | "8"

<percussion> ::= "%" | "%%"

<variable_usage> ::= <variable>
                  |  <module_component>, ".", <variable_usage>

<sequence> ::= "[" {<line_expr_or_beat_assert>}+ "]"

<merge> ::= "[+" {<line_expression>}+ "+]"

<repeat> ::= "[*" <non_negative_int> ":" {<line_expr_or_beat_assert>}+ "*]"

<non_negative_int> ::= non-negative integer

<waveform_choice> ::= "[^" <waveform> ":" {<line_expr_or_beat_assert>}+ "^]"

<waveform> ::= "SQUARE"
            |  "SAWTOOTH"

<volume_modify> ::= "[!" <non_negative_decimal> ":" {<line_expr_or_beat_assert>}+ "!]"

<line_expr_or_beat_assert> ::= <line_expression>
                            |  <beat_assertion>

<beat_assertion> ::= "|", [<non_negative_decimal>, ">"]

Furthermore, line comments are preceded by -- and block comments are surrounded by {- and -}.

Semantics

Line Expressions

Octune songs are fundamentally created by composing (šŸ˜‰) entities called "line expressions", and then declaring the desired bpm for the line expression to be played at. The basic building block of a line expression is a "note", which can be combined with other line expressions to create line expressions of their own.

Notes

A note is primarily denoted by its length (in terms of beats) and its sound with no space between them.

The length may be specified by a rational number in decimal form (like 1, 2, 3.5, ...) or by musical note length (t = thirty-second note, s = sixteenth note, e = eigth note, q = quarter note, h = half note). Musical note lengths may further by followed by dots (.) to denote dotted notes, so that q. is 1.5 beats, q.. is 1.75 beats, h. is 3 beats, and etc.

The sound can be one of the following:

Notes may also be prefixed with 0 or more modifiers, which slightly change how the note is played:

Examples:

Sequences

Sequences are denoted by a whitespace-separated list of line expressions surrounded by [ and ]. They represent playing those line expressions one at a time.

Examples:

Merges

Merges are denoted by a whitespace-separated list of line expressions surrounded by [+ and +]. They represent playing those line expressions simultaneously. Note that the length of a merge is the length of its longest argument.

Examples:

Repeats

Sequences are denoted by a non-negative integer n, followed by :, followed by a whitespace-separated list of line expressions, together surrounded by [* and *]. They represent the sequence of the given line expressions (played one at a time), but with the sequence repeated n times.

Examples:

Waveform Choice

Waveform choices are denoted by a waveform w, followed by :, followed by a whitespace-separated list of line expressions, together surrounded by [^ and ^]. They represent playing those line expressions one at a time, but generating waves with waveform w. There are currently square and sawtooth waveforms, denoted by SQUARE and SAWTOOTH respectively. Note that outer waveform choice blocks will override any inner ones, ie: [^ SAWTOOTH : [^ SQUARE : expression ^] ^] will generate expression using sawtooth waves.

Examples:

Volume Modifiers

Volume modifiers are denoted by a non-negative rational decimal n, followed by :, followed by a whitespace-separated list of line expressions, together surrounded by [! and !]. They represent playing those line expressions one at a time, but with the wave samples multiplied by n, allowing you to increase or decrease their volume.

Examples:

Variables

Variables can hold line expressions, and when they do they may be used by their names as line expressions themselves. Valid variable names are those that start with a lowercase letter, followed by any alphanumeric character. They represent the line expression that they were declared with.

Variables are declared at the top level by variableName = <some line expression>, and only variables that have been declared can be used. Order of declarations is not important, but variables may not cyclically reference each other.

Examples:

Songs

A song is the entrypoint for music generation, separate from line expressions. It is denoted by a non-negative integer bpm (the song's speed in beats per minute), followed by :, followed by a single line expression, together surrounded by { and }. They represent the given line expression being played at bpm beats per minute.

Music generation starts at the main variable, which must exist and be declared with a song.

Examples:

Octune Music Examples

The samples/ directory in this repository contains Octune code for a few well-known songs. Try compiling them and giving them a listen! You can also find some of them on the Octune YouTube channel.

Documentation TODO