This tool faithfully emulates the 3 basic sounds of the audio processing unit of the NES. These are:

  • White noise (for snares, hihats) produced by linear feedback shift register PRNG
  • 16-step quantized triangle waves (for basslines, kicks, toms)
  • Variable duty-cycle pulse waves (for leads)

Here is a good introductory video explaining the NES audio channels:

The interpreter is based on MAL (Make-a-Lisp) and closely follows Clojure (including destructuring). The source for the interpreter is available here, and the audio engine is here.

Note: The code that loads when the tool starts is the classic title theme from Legend of Zelda, by Koji Kondo. It is purely there as a demo.

Evaluation key bindings

  • Shift+Enter = Eval top-level form
  • Alt/Cmd+Enter = Eval all
  • Ctrl+Enter = Eval at cursor

Synth/sequence API

Functions returning audio buffers

  • drum-seq
  • tri
  • pulse0, pulse1, pulse2, pulse3

These functions take a sequence (list/vector) of notes. Each note is a map with the following keys:

  • pitch (triangle/pulse only) - a MIDI number representing frequency (middle C is 60)
  • length - note duration in seconds
  • time - the number of seconds at which the note occurs
  • vibrato (optional) - applies a sine wave to the frequency

The vibrato key also takes a map with the following keys:

  • speed - the rate at which the frequency cycles
  • width - the degree to which the frequency changes

Recommended vibrato values for speed/width are 1-10, but there is no limit (a speed of 300 makes a pretty crazy sound!)

Drums have a linear volume decay for natural sounding snares/hihats. The 4 different pulse waves are:

  • pulse0 - 12.5% duty
  • pulse1 - 25% duty
  • pulse2 - 50% duty (square)
  • pulse3 - 75% duty (inverse of pulse1)

The note data can be produced however you like, as long as it ends up a sequence of maps with the right keys.  Here is an example lead from Megaman 2 by Takashi Tateishi (full song):

(defn lead [time]
  (into []
    (for [[beat length note]
          [[0 0.5 61] [1 0.5 61] [1.5 0.5 61] [2 0.5 59]
           [2.5 1 61] [3.5 0.5 69] [4 1 66] [5 1 66] [6 1 64]
           [7 1 63] [8 0.5 63] [9 0.5 64] [9.5 0.5 64] 
           [10.5 0.5 64] [12 0.5 63] [13 0.5 64] [13.5 0.5 64]
           [14.5 0.5 64] [15.5 0.5 63] [16 0.5 61] [17 0.5 61] 
           [17.5 0.5 61] [18 0.5 59] [18.5 1 61] [19.5 0.5 69] 
           [20 1 66] [21 1 66] [22 1 64] [23 1 63] [24.5 0.5 59] 
           [25 0.5 61] [25.5 0.5 59] [26 3 56]]]
     {:time (* tempo (+ beat time)) :length (* tempo length) :pitch note})))
(play (pulse0 (lead 0)))

Mixing, playing and rendering audio files

  • mix - takes a sequence of audio buffers, sums them and returns a new buffer
  • play - plays an audio buffer
  • spit-wav - takes a filename and an audio buffer and downloads it

Tips

The interpreter was designed for education and is not optimized for performance. Generating long sequences of notes (e.g. with `for`) can be particularly slow, however this can be greatly mitigated by saving audio buffers in vars (i.e. with `def`) while composing, as subsequent operations like mixing and playing are extremely fast. This also makes your song easier to read.

Triangle kicks

A classic technique for creating drums on the NES is to use rapidly descending notes with the triangle channel, like this excerpt from Asterix by Alberto Jose González:

(def tempo 0.6) 
(defn triangle-kicks [time root]
  [{:time (* tempo (+ time 0)) :length 0.1 :pitch (+ root 14)}
   {:time (* tempo (+ time 0.07)) :length 0.1 :pitch (+ root 10)}
   {:time (* tempo (+ time 0.09)) :length 0.1 :pitch (+ root 6)}  
   {:time (* tempo (+ time 0.11)) :length 0.1 :pitch (+ root 4)} 
   {:time (* tempo (+ time 0.13)) :length 0.3 :pitch root} 
   {:time (* tempo (+ time 0.5)) :length 0.1 :pitch (+ root 24)} 
   {:time (* tempo (+ time 0.55)) :length 0.3 :pitch (+ root 12)} 
   {:time (* tempo (+ time 1)) :length 0.1 :pitch (+ root 22)} 
   {:time (* tempo (+ time 1.03)) :length 0.1 :pitch (+ root 19)} 
   {:time (* tempo (+ time 1.06)) :length 0.1 :pitch (+ root 16)} 
   {:time (* tempo (+ time 1.06)) :length 0.1 :pitch (+ root 6)}  
   {:time (* tempo (+ time 1.12)) :length 0.3 :pitch (+ root 7)}  
   {:time (* tempo (+ time 1.5)) :length 0.1 :pitch (+ root 24)} 
   {:time (* tempo (+ time 1.53)) :length 0.3 :pitch (+ root 12)}])  
(play (tri (apply concat
  (for [[time note]     
        [[0 36] [2 36] [4 36] [6 36] [8 36] [10 36] [12 41] [14 41]    
        [16 43] [18 43] [20 36] [22 36] [24 44] [26 43] [28 36] [30 36]]]   
    (triangle-kicks time note)))))

Layering these with bursts of white noise creates a satisfying percussive effect.

Building from source

Requires Node.js version 14.18+, 16+.

Download and unzip NES-music-engine-source.zip and run:

npm install

Develop

npm run dev

Create optimized build

npm run build
npm preview
StatusReleased
CategoryTool
PlatformsHTML5
Authorbobbicodes

Download

Download
NES-music-engine-source.zip 11 MB

Development log

Comments

Log in with itch.io to leave a comment.

I like this 

(1 edit)

Wow! This is very elaborate and impressive. I loved the megaman output. I feel like I don't know enough about digitized music (or music in general) to understand your explanation :( How did you replicate the Megaman sound?


The MAL project seems cool too!


Also why did you use your own lisp? This might be a dumb question but I don't understand.

Thanks! Yeah I suppose I just like dove right in there... The first and most important part of copying a song is just to be able to hear it well... I use a program called NSFPlay that lets you mute tracks and slow them way down while preserving the quality, and it even shows a little keyboard with colored dots showing you where all the notes are.

I had actually made the Lisp previously because I had a specific need for it - I wanted a more slimmed down version of Clojure that was native to the browser for educational purposes. But when I heard about the game jam I suddenly got really excited at the thought of "testing" it to see if it could actually work! To be honest, I might not have even bothered otherwise.

(1 edit)

Oh, and this is a good introductory video explaining the NES audio channels:


(Actually, I think I'll link this in the actual project page!)

Thanks for the video. I have just slightly more understanding now. It seems the 5 channels are the available "instruments" so to speak of the NES. Am I correct that they play parallel to each other?


Unfortunately, the parameters such as pitch, vibrato, frequency, and width are still mysterious.


I think that these channel sounds are functions of pressure/time?

Probably the triangle channel is a /\-shaped function and the 0.5 pulse is a square. Music is always a mystery to me.