Thursday, February 05, 2015

Sounds awful, but it's all mine - Adventures in programmatic music generation

A few years ago I discovered the RTTTL "music format." RTTTL, as a quick refresher, was a language used to exchange ring tones. Essentially it's a text file that lists out which notes to play; couldn't be simpler. The textual nature of it means that it's trivial to generate songs using any programming language.

Yesterday evening I had about 3 hours on the train to kill. I had planned to work on my Laptop but the "free" WiFi was apparently maxed out by passengers who had the same idea. So I put the laptop away, busted out the Bluetooth Keyboard and started to brainstorm as to how I could use Scheme to generate sweet, sweet music.

I can tell you that I fully missed the mark on the "sweet, sweet" part. But I did manage to use Scheme to generate RTTTL files, which in turn played on my mobile device. I ended up implementing a series of functions to generate Morse Code from arbitrary text, as well as a function to generate a random song. Here, give a listen to some samples:

The Morse Code needs tuning to be truly accurate, and the "random songs" my program generates are more like random noise. But this is hardly a loss. I've always been curious what an algorithm might be that would generate at least a passable tune, and now I've got the toolkit to experiment with this.

I've included my source code below, and you can grab it from github here.

One feature of RTTTL is that your phone, for historic reasons, probably plays it without needing a special application. The flip side of this, though, is that finding a way to play the RTTTL files outside of your phone is non-trivial. I looked all over Google Play for an app that would convert my RTTTL file to a .wav or .mp3 file, but had no luck. Your typical music player and music publication site (I'm looking at you, soundcloud) is going to be clueless as to what an RTTTL file is. Luckily, I was able to find a recipe that works for converting from RTTTL to .wav. Here it is:

  • Install the SMS-Ringtone-RTTTL-MIDI perl module
  • Grab this script I prepared to leverages this module to create a MIDI file
  • Install some MIDI related tools, including timidity++ and fluidsynth

Once the above steps are taken care of, you can convert foo.rtttl to foo.wav by using the following sequence:

 rtttltomidi < foo.rttl > foo.midi
 timidity foo.midi   # listen to your creation
 fluidsynth -F foo.wav /usr/share/sounds/sf2/default.sf2 foo.mid

That last step was inspired by this post, which also goes on to explain how to convert the .wav to .mp3.

Getting these libraries together sounds tricky, but on my new Linux box the whole process was surprisingly painless.

OK, so now it's your turn. Go off and write some code which in turn writes some amazing music!

;; RTTTL - let's generate some sounds. Below we generate both
;; Morse Code and a random "song"

(define (&& . any)
 (apply string-append
        (map (lambda (x)
              (cond ((number? x) (number->string x))
                    ((symbol? x) (symbol->string x))
                    (else x)))
(define (implode sep parts)  
 (let loop ((parts parts) (accum '()))
  (cond ((null? parts) (apply && accum))
         (loop (cdr parts)
               (if (null? accum)
                   (list (car parts))
                   (append accum (list sep (car parts)))))))))

(define (string-head text)
 (substring text 0 1))
(define (string-tail text)
 (substring text 1 (string-length text)))

(define (explode sep text)
 (let loop ((text text) (current "") (accum '()))
  (cond ((equal? text "")
         (if (equal? current "")
             (reverse accum)
             (reverse (cons current accum))))
        ((equal? (string-head text) sep)
         (loop (string-tail text)
               (cons current accum)))
         (loop (string-tail text)
               (string-append current (string-head text))

(define (rtttl title notes)
 (string-append title ":d=4,o=5,b=160:" notes "\n"))
(define (save filename contents)
 (call-with-output-file (string-append "/sdcard/Documents/" filename)
  (lambda (out)
   (display contents out)))) 

(define morse-map
 '((a ".-") (b "-...") (c "-.-.") (d "-..") (e ".")
   (f "..-.") (g "--.") (h "....") (i "..") (j ".---")
   (k ".-.-")  (l ".-..") (m "--") (n "-.") (o "---")
   (p ".-.-") (q "--*-") (r ".-.") (s "...")
   (t "-") (u "..-") (v "...-") (w ".-") (x "-..-")
   (y "-.--") (z "--..")))
(define (morse-char c)
 (let ((needle (string->symbol (string (char-downcase c)))))
  (cond ((eq? c #\space) " ")
        ((assoc needle morse-map) => cadr)
        (else (morse-char #\x)))))

(define (morse-word text)
 (let ((chars (map morse-char (string->list text))))
  (implode "|" chars)))
(define (morse-string text)
 (let ((words (explode " " text)))
  (implode "_" (map morse-word words))))

(define (morse-notes encoded)
 (let loop ((chars (string->list encoded)) (accum '()))
  (cond ((null? chars)
         (implode "," (reverse accum)))
          (cdr chars)
           (case (car chars)
            ((#\.) "c5")
            ((#\-) "a7")
            ((#\|) "p")
            ((#\_) "p,p,p"))
(define (morse-rtttl message)
 (rtttl message (morse-notes (morse-string message))))          
(define (string-reverse text)
 (apply string
        (reverse (string->list text))))
(define (range low high)
 (if (> low high) '() (cons low (range (+ 1 low) high))))
(define (random-elt items)
 (list-ref items (random-integer (length items))))
(define (random-between low high)
 (+ low (random-integer (- high low))))
(define (random-note)
 (let ((note (random-elt '(c c c c d d e f g a p)))
       (len  (random-elt '(1 2 4 8 16)))
       (oct  (random-elt '(5 6))))
  (if (eq? note 'p)
      (&& len note)
      (&& len note oct))))
(define (random-name)
 (&& (random-note) (random-note) (random-note) (random-note)))
(define (random-notes)
  (map (lambda (i)
       (range 0 (random-integer 100)))))

(define (make-buffer)
 (let ((buffer '()))
  (lambda (x)
   (if (equal? x 'get)
       (implode "," (reverse buffer))
       (set! buffer (cons x buffer))))))
(define (random-song)
 (let ((chorus (random-notes))
       (buffer (make-buffer)))
   (for-each (lambda (i)
              (buffer (random-notes))
              (buffer chorus))
             (range 1 (random-between 5 10)))
   (buffer (random-notes))
   (rtttl "Music By Scheme" (buffer 'get))))
(save "hw.rtttl" (morse-rtttl "Hello World"))
(save "sos.rtttl" (morse-rtttl "SOS"))
(save "random.rtttl" (random-song))

No comments:

Post a Comment