This is a tool for programmatically composing NES music. Songs are built using a dialect of the Lisp programming language, running in a live interpreter connected to a custom text editor. It is conceptually similar to MML (Music Macro Language), but benefits from Lisp's structured syntax which facilitates a highly ergonomic style of interactive evaluation. The interpreter is based on MAL (Make-a-Lisp) and closely follows Clojure including its destructuring syntax, powerful sequence processing library and macro system. This project also aims to provide a more accessible composition environment for those with impaired vision or who otherwise have difficulty with graphical interfaces.
- Shift+Enter = Eval top-level form
- Alt/Cmd+Enter = Eval all
- Ctrl+Enter = Eval at cursor
The Eval at cursor command is particularly powerful - it evaluates the expression that ends just to the left of the cursor position, allowing you to quickly test the result at each level of nesting.
A part is represented by a sequence of commands, each of which is a hashmap with various keys representing length
(expressed in 1/60 of a second ticks), pitch
(MIDI numbers, including decimal values for vibrato/microtones), volume
and duty
. These sequences are passed to their respective channels.
The note data can be produced however you like, as long as it ends up a sequence of maps with the right keys. So the most basic way would be to use a literal sequence of maps:
[{:volume 9 :length 20 :pitch 60} {:pitch 67}
{:length 50 :pitch 65} {:length 20 :pitch 67}
{:length 10 :pitch 68} {:pitch 67} {:pitch 65}
{:pitch 67} {:length 20 :pitch 60}]
Much of the time, it is enough to encode length/pitch pairs, which can be placed in the sequence using vectors. Thus the above example could be written like this:
[{:volume 9 :length 20 :pitch 60} [20 67] [50 65]
[20 67] [10 68] [10 67] [10 65] [10 67] [20 60]]
There is no limit to the number of ways your music can be written. Check out the examples in the songs folder for inspiration.
To facilitate volume and duty changes, a note can also be given volume
and duty
keys. Volume is in 16 steps, from 0 to 15. Duty is from 0-3 (0-7 for VRC6):
- 0 = 12.5%
- 1 = 25%
- 2 = 50%
- 3 = 75%
A volume or duty change is persistent, i.e. it will affect all subsequent notes until there is another change.
The noise channel plays at 16 possible pitches from 0 (high) to 15 (low). Mode 1 noise (metallic sound) is from 16 to 32.
To create instruments using volume envelopes, you can define a sequence:
(def saw-env
[30 27 23 19 15 11 8 7 7 7 7 7 6 6 6 6 6 6
5 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 1])
Then in the music sequence, select it with the :envelope
key:
[{:envelope saw-env}
[12 33] [12 45] [12 33] [24 38] [12 36] [12 35] [12 36]
[12 33] [12 45] [12 33] [24 38] [12 36] [12 40] [12 28]]
Most songs contain many repeated patterns. To facilitate this without consuming additional data, 2 levels of loops are provided, loop1
and loop2
. You cannot nest a loop1
or loop2
inside another, but you can nest a loop1
inside a loop2
or vice versa. Just call (loop1 <n> <notes>)
where n
is the number of times to loop, and notes
is a sequence of notes.
The play
function takes a map containing any combination of the following keys: square1
, square2
, triangle
, noise
and dpcm
. For example:
(play
{:square1 (concat [{:volume 4 :duty 0}] arps)
:square2 (concat [{:volume 1 :duty 0 :length 9 :pitch 160}]
(detune arps))
:triangle (concat tri2 tri3 tri2 tri4)
:noise (concat (loop1 3 drums1) drums2)})
To save an audio file, pass a filename along with your note sequences to save-wav
:
(save-wav "mytune.wav"
{:square1 [[20 60] [20 67] [50 65] [20 67] [10 68]
[10 67] [10 65] [10 67] [20 60]]})
Saving an NSF file works the same way by calling save-nsf
:
(save-nsf "mytune.nsf"
{:square1 [[20 60] [20 67] [50 65] [20 67] [10 68]
[10 67] [10 65] [10 67] [20 60]]})
These are just the basics - again, check out the example songs for lots of ideas.
There is an Import Sample button on the bottom of the page that allows you to upload .dmc files. Once they are loaded, they can be referred to in the :dpcm
key of the note maps passed to the above functions. The API is like this:
(play
{:dpcm
[{:sample "kick" :length 3}
{:sample "e-sus" :length 12}
{:sample "mute-e" :length 11}
{:sample "mute-e" :length 11}
{:sample "mute-e" :length 11}
{:sample "e-sus" :length 16}
{:rest 32}]})
The sample names must match the file names. Each note must have a sample
key and a length
key. Use :rest
to insert gaps in the playback.
To stop all audio currently playing, simply play a blank sequence:
(play {})
Requires Node.js version 14.18+, 16+.
npm install
npm run dev
npm run build
npm preview