Create a gist now

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Simple sound synthesis in Clojure
(import '(javax.sound.sampled AudioSystem DataLine$Info SourceDataLine
AudioFormat AudioFormat$Encoding))
(def popular-format
(AudioFormat. AudioFormat$Encoding/PCM_SIGNED
48000 ; sample rate
16 ; bits per sample
2 ; channels
4 ; frame size 2*16bits [bytes]
48000 ; frame rate
false ; little endian
))
(defn open-line [audio-format]
(doto (AudioSystem/getLine (DataLine$Info. SourceDataLine audio-format))
(.open audio-format)
(.start)))
(defn sine [sample-rate freq]
(let [term (/ 1 freq)
samples (* term sample-rate)
factor (/ (* 2 Math/PI) samples)]
(map #(Math/sin (* % factor))
(range samples))))
(defn amplitude [sample-size]
(Math/pow 2 (- sample-size 1.1)))
(defn quantize [amplitude value]
(int (* amplitude value)))
(defn unsigned-byte [x]
(byte (if (> x 127) (- x 256) x)))
(defn little-endian [size x]
(map #(-> (bit-shift-right x (* 8 %))
(bit-and 255)
unsigned-byte)
(range size)))
(defn big-endian [size x]
(reverse (little-endian size x)))
(defn sine-bytes [format freq]
(let [{:keys [sampleSizeInBits frameSize
sampleRate bigEndian]} (bean format)
sample-size (/ sampleSizeInBits 8)
ampl (amplitude sampleSizeInBits)
tear (if bigEndian big-endian little-endian)]
(->> (sine sampleRate freq)
(map (partial quantize ampl))
(map (partial tear sample-size))
(map cycle)
(map (partial take frameSize))
(apply concat)
byte-array)))
(defn recalc-data [{:keys [line] :as state} freq]
(assoc state :data (sine-bytes (.getFormat line) freq)))
(defn play-data [{:keys [line data playing] :as state} agent]
(when (and line data playing)
(.write line data 0 (count data))
(send-off agent play-data agent))
state)
(defn pause [agent]
(send agent assoc :playing false)
(doto (:line @agent)
.stop .flush))
(defn play [agent]
(.start (:line @agent))
(send agent assoc :playing true)
(send-off agent play-data agent))
(defn change-freq [agent freq]
(doto agent
pause
(send recalc-data freq)
play))
(defn line-agent [line freq]
(let [agent (agent {:line line})]
(send agent recalc-data freq)
(play agent)))
(defn tone-freq [x]
(-> (Math/pow 2 (/ x 11)) (* 440) (/ 512)))
(def mountain-king
[[0 1] [2 1] [3 1] [5 1]
[7 1] [3 1] [7 2]
[6 1] [2 1] [6 2]
[5 1] [1 1] [5 2]
[0 1] [2 1] [3 1] [5 1]
[7 1] [3 1] [7 1] [12 1]
[10 1] [7 1] [3 1] [7 1]
[10 2] [-2 2]])
(defn play-melody [agent base-tone base-duration melody]
(doseq [[tone duration] melody]
(change-freq agent (tone-freq (+ base-tone tone)))
(Thread/sleep (* base-duration duration)))
(pause agent))
@lspector

This comment has been minimized.

Show comment
Hide comment
@lspector

lspector Aug 29, 2011

Looks nice! Could you share an example top-level call? I assume it's something like (play-melody _ _ _ mountain-king), but the other args aren't obvious to me.

lspector commented Aug 29, 2011

Looks nice! Could you share an example top-level call? I assume it's something like (play-melody _ _ _ mountain-king), but the other args aren't obvious to me.

@ghost

This comment has been minimized.

Show comment
Hide comment
@ghost

ghost Aug 30, 2011

This gist was accompanying a blog post: http://longstandingbug.com/sound-synthesis.html and I was too lazy at that moment to write docs ;)

To play it you can (play-melody a 70 400 mountain-king) where a is (def a (line-agent (open-line popular-format) 60)), 70 is base tone relative to 440Hz which have value 99 and 400 is duration of "1" in sequence in milliseconds (first value of each vector in mountain-king is tone value relative to base-tone and the second is duration).

This code is not very old but after updating to java 6u26 and then 7 (on Windows) I have some issues to make it work again :| (strange errors involving reflection in Clojure, similar code in Java works flawlessly, I hope you won't have this problem).

Owner

ghost commented Aug 30, 2011

This gist was accompanying a blog post: http://longstandingbug.com/sound-synthesis.html and I was too lazy at that moment to write docs ;)

To play it you can (play-melody a 70 400 mountain-king) where a is (def a (line-agent (open-line popular-format) 60)), 70 is base tone relative to 440Hz which have value 99 and 400 is duration of "1" in sequence in milliseconds (first value of each vector in mountain-king is tone value relative to base-tone and the second is duration).

This code is not very old but after updating to java 6u26 and then 7 (on Windows) I have some issues to make it work again :| (strange errors involving reflection in Clojure, similar code in Java works flawlessly, I hope you won't have this problem).

@lspector

This comment has been minimized.

Show comment
Hide comment
@lspector

lspector Aug 30, 2011

lspector commented Aug 30, 2011

@ghost

This comment has been minimized.

Show comment
Hide comment
@ghost

ghost Aug 30, 2011

Whoops... Actually the clicks means that it probably works and no music means that probably the base tone is to low for your speakers (I played it with subwoofer). Try with base tone like 99 (kind of high one) and then go down with it if you want to know when it melds to silence :)

The clicks are something I wish to address in future but first I need get this to work again on my setup ;)

Question out of curiosity: Which versions of JVM and Clojure you are using? And what OS?

Owner

ghost commented Aug 30, 2011

Whoops... Actually the clicks means that it probably works and no music means that probably the base tone is to low for your speakers (I played it with subwoofer). Try with base tone like 99 (kind of high one) and then go down with it if you want to know when it melds to silence :)

The clicks are something I wish to address in future but first I need get this to work again on my setup ;)

Question out of curiosity: Which versions of JVM and Clojure you are using? And what OS?

@lspector

This comment has been minimized.

Show comment
Hide comment
@lspector

lspector Aug 31, 2011

lspector commented Aug 31, 2011

@skelter

This comment has been minimized.

Show comment
Hide comment
@skelter

skelter Oct 16, 2013

This is actually causing an exception for me because Clojure cannot invoke the open method on the private implementation class AbstractDataLine

skelter commented Oct 16, 2013

This is actually causing an exception for me because Clojure cannot invoke the open method on the private implementation class AbstractDataLine

@antonharald

This comment has been minimized.

Show comment
Hide comment
@antonharald

antonharald Feb 14, 2016

is there some news here? I just tried this code and bumped into the private method error...

java.lang.IllegalArgumentException: Can't call public method of non-public class: public final void com.sun.media.sound.AbstractDataLine.open(javax.sound.sampled.AudioFormat) throws javax.sound.sampled.LineUnavailableException, compiling:(form-init5925709392734003297.clj:1:20)

antonharald commented Feb 14, 2016

is there some news here? I just tried this code and bumped into the private method error...

java.lang.IllegalArgumentException: Can't call public method of non-public class: public final void com.sun.media.sound.AbstractDataLine.open(javax.sound.sampled.AudioFormat) throws javax.sound.sampled.LineUnavailableException, compiling:(form-init5925709392734003297.clj:1:20)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment