├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENCE.txt ├── README.md ├── build.sh ├── package.json ├── project.clj ├── resources └── public │ └── index.html ├── src └── cljs_bach │ └── synthesis.cljs └── system.properties /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | /resources/public/js 12 | /out 13 | /.repl 14 | *.log 15 | /.env 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | script: lein cljsbuild once dev 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.3.0 5 | ----- 6 | 7 | * Document `sample`. 8 | * Fixed ADSR duration (thanks @esp1!). 9 | * Fixed raw buffer range (thanks @0x2493). 10 | 11 | 0.2.0 12 | ----- 13 | 14 | * Memoize `buffer`, so that e.g. `reverb` and `white-noise` don't repeatedly calculate their bits. 15 | 16 | 0.1.0 17 | ----- 18 | 19 | * First release, with all the major parts of the Web Audio API in place. 20 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Open Source Initiative OSI - The MIT License:Licensing 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2015, Chris Ford (christophertford at gmail) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CLJS Bach 2 | ========= 3 | 4 | A Clojurescript wrapper for the Web Audio API, extracted from [Klangmeister](http://ctford.github.io/klangmeister/). 5 | 6 | [![Build Status](https://travis-ci.org/ctford/cljs-bach.png)](https://travis-ci.org/ctford/cljs-bach) 7 | 8 | Importing it into your project 9 | ------------------------------ 10 | 11 | CLJS Bach is a Clojurescript library. To include it in your Clojurescript project, you need to 12 | include the following in your `project.clj` or `build.boot`. 13 | 14 | [![Clojars Project](http://clojars.org/cljs-bach/latest-version.svg)](http://clojars.org/cljs-bach) 15 | 16 | Once you've done that, you can use it like any other library. 17 | 18 | Usage 19 | ----- 20 | 21 | Firstly, create an audio context. You only need one, and if you keep creating them the browser will run out and error on you. 22 | 23 | (defonce context (audio-context)) 24 | 25 | See [Klangmeister](http://ctford.github.io/klangmeister/) for examples of how to build synthesisers. Here's a simple 26 | one. Note the use of `connect->` to join together simple parts together. 27 | 28 | (defn ping [freq] 29 | (connect-> 30 | (square freq) ; Try a sawtooth wave. 31 | (percussive 0.01 0.4) ; Try varying the attack and decay. 32 | (gain 0.1))) ; Try a bigger gain. 33 | 34 | Once you have a synthesiser, connect it to `destination` and use `run-with` to give it an audio context, a time to run at 35 | and a duration. 36 | 37 | ; Play the ping synthesiser now, at 440 hertz. 38 | (-> (ping 440) 39 | (connect-> destination) 40 | (run-with context (current-time context) 1.0))) 41 | 42 | If you forget to connect a synthesiser to `destination`, then you'll here no sound, because nothing will be sent to the speakers. 43 | 44 | Vanilla javascript usage 45 | ------------------------ 46 | 47 | See `resources/public/index.html`. To try it out, run `lein figwheel` and then navigate to `http://localhost:3449/`. 48 | 49 | API 50 | --- 51 | 52 | ### Machinery 53 | 54 | * `(audio-context)` - returns the browser's Audio Context. 55 | * `(current-time context)` - returns the current time according to the supplied audio context. 56 | * `(run-with node context at duration)` - runs the synthesiser in the supplied context. 57 | * `destination` - a node representing the browser's speakers. 58 | 59 | ### Oscillators 60 | 61 | * `(sawtooth frequency)` - a sawtooth wave oscillating at `frequency` hertz. 62 | * `(sine frequency)` - a sine wave oscillating at `frequency` hertz. 63 | * `(square frequency)` - a square wave oscillating at `frequency` hertz. 64 | * `(triangle frequency)` - a triangle wave oscillating at `frequency` hertz. 65 | * `white-noise` - a node emitting random noise. 66 | 67 | ### Modifiers 68 | 69 | * `(adsr attack decay sustain release)` - an envelope for shaping a note. 70 | * `(gain level)` - a node that multiplies its input by `level`. 71 | * `(high-pass cutoff)` - filter out frequencies below `cutoff`. 72 | * `(low-pass cutoff)` - filter out frequencies above `cutoff`. 73 | * `(percussive attack decay)` - a simple envelope for shaping a note. 74 | 75 | ### Effects 76 | 77 | * `(stereo-panner pan)` - pan the signal left (-1) or right (1). 78 | * `(delay-line seconds)` - delay the signal by `seconds`. 79 | * `reverb` - apply reverb to the signal. 80 | 81 | ### Combinators 82 | 83 | * `(connect-> node1 node2)` - connect `node1`'s output to `node2`'s input. 84 | * `(add node1 node2)` - add together the outputs of `node1` and `node2`. 85 | 86 | ### Samples 87 | 88 | * `(sample uri)` - load a sample from a URI, which can then be used as a source node in the same way that oscillators can. The asynchronicity can make this a little glitchy. 89 | 90 | Getting a REPL 91 | -------------- 92 | 93 | CLJS Bach relies on the Web Audio API, so it will only work if your javascript environment supports that. Fortunately, all 94 | major browsers except Internet Explorer do (and it will on next major release). 95 | 96 | If you have this project checked out locally, you can use the [Figwheel](https://github.com/bhauman/lein-figwheel) REPL. Firstly, 97 | start Figwheel. 98 | 99 | lein figwheel 100 | 101 | Figwheel will tell you where's running, so you can open it in your browser. 102 | 103 | Figwheel: Starting server at http://localhost:3449 104 | .... 105 | Prompt will show when Figwheel connects to your application 106 | 107 | Once you've done that, the REPL will come to life. 108 | 109 | To quit, type: :cljs/quit 110 | cljs.user=> 111 | 112 | From then on, you can type commands into the REPL, and they'll be executed by your browser. 113 | 114 | Design 115 | ------ 116 | 117 | CLJS Bach is purely functional. To make synthesiser composition easier, synthesisers are actually functions that take 118 | an audio context, a time and a duration and return the synthesis graph. 119 | 120 | (defn some-synth [...] 121 | (fn [context at duration] 122 | ; Create a graph of synthesis nodes. 123 | )) 124 | 125 | That's why you need `run-with` to make sound actually happen. 126 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | lein clean && lein cljsbuild once prod 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bach.js", 3 | "version": "0.3.0-pre1", 4 | "description": "A modular synthesis library. Nothing works yet.", 5 | "main": "resources/public/js/compiled/bach.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/ctford/cljs-bach.git" 15 | }, 16 | "keywords": [ 17 | "synthesis", 18 | "web audio" 19 | ], 20 | "author": "Chris Ford", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/ctford/cljs-bach/issues" 24 | }, 25 | "homepage": "https://github.com/ctford/cljs-bach#readme" 26 | } 27 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject cljs-bach "0.4.0-SNAPSHOT" 2 | :description "A Clojurescript wrapper for the Web Audio API." 3 | :license {:name "MIT" } 4 | :dependencies [[org.clojure/clojure "1.7.0"] 5 | [org.clojure/clojurescript "1.7.228"] 6 | [cljs-ajax "0.5.5"]] 7 | 8 | :min-lein-version "2.5.0" 9 | 10 | :plugins [[lein-cljsbuild "1.1.2"] 11 | [lein-figwheel "0.5.0-2"]] 12 | 13 | :clean-targets ^{:protect false} ["resources/public/js/compiled" 14 | "target" 15 | "out"] 16 | 17 | :source-paths ["src"] 18 | :resource-paths ["resources" "target/cljsbuild"] 19 | 20 | :cljsbuild {:builds [{:id "dev" 21 | :source-paths ["src"] 22 | :figwheel true 23 | :compiler {:main "cljs_bach.synthesis" 24 | :optimizations :none 25 | :pretty-print true 26 | :output-to "resources/public/js/compiled/bach.js" 27 | :output-dir "resources/public/js/compiled" 28 | :asset-path "js/compiled"}} 29 | 30 | {:id "prod" 31 | :source-paths ["src"] 32 | :compiler {:main "cljs_bach.synthesis" 33 | :static-fns true 34 | :optimizations :advanced 35 | :pretty-print false 36 | :optimize-constants true 37 | :output-to "resources/public/js/compiled/bach.js" 38 | :asset-path "js/compiled"}}]}) 39 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLJS Bach 6 | 7 | 8 | 9 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/cljs_bach/synthesis.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-bach.synthesis 2 | (:require 3 | [ajax.core :as ajax] 4 | [ajax.protocols :as protocol])) 5 | 6 | (defn ^:export audio-context 7 | "Construct an audio context in a way that works even if it's prefixed." 8 | [] 9 | (if js/window.AudioContext. ; Some browsers e.g. Safari don't use the 10 | (js/window.AudioContext.) ; unprefixed version yet. 11 | (js/window.webkitAudioContext.))) 12 | 13 | (defn ^:export current-time 14 | "Return the current time as recorded by the audio context." 15 | [context] 16 | (.-currentTime context)) 17 | 18 | ; Definitions 19 | 20 | (defn subgraph 21 | ([input output] {:input input :output output}) 22 | ([singleton] (subgraph singleton singleton))) 23 | 24 | (defn source 25 | "A graph of synthesis nodes without an input, 26 | so another graph can't connect to it." 27 | [node] 28 | (subgraph nil node)) 29 | 30 | (defn sink 31 | "A graph of synthesis nodes without an output, 32 | so it can't connect to another graph." 33 | [node] 34 | (subgraph node nil)) 35 | 36 | ; Plumbing 37 | 38 | (defn ^:export run-with 39 | "Convert a synth (actually a reader fn) into a concrete 40 | subgraph by supplying context and timing." 41 | [synth context at duration] 42 | (synth context at duration)) 43 | 44 | (defn ^:export destination 45 | "The destination of the audio context i.e. the speakers." 46 | [context at duration] 47 | (sink (.-destination context))) 48 | 49 | (defn plug [param input context at duration] 50 | "Plug an input into an audio parameter, accepting both 51 | numbers and synths." 52 | (if (number? input) 53 | (.setValueAtTime param input at) 54 | (-> input (run-with context at duration) :output (.connect param)))) 55 | 56 | (defn ^:export gain 57 | "Multiply the signal by level." 58 | [level] 59 | (fn [context at duration] 60 | (subgraph 61 | (doto (.createGain context) 62 | (-> .-gain (plug level context at duration)))))) 63 | 64 | (def ^:export pass-through 65 | "Pass the signal through unaltered." 66 | (gain 1.0)) 67 | 68 | 69 | ; Envelopes 70 | 71 | (defn envelope 72 | "Build an envelope out of [segment-duration final-level] coordinates." 73 | [& corners] 74 | (fn [context at duration] 75 | (let [audio-node (.createGain context)] 76 | (-> audio-node .-gain (.setValueAtTime 0 at)) 77 | (loop [x at, coordinates corners] 78 | (when-let [[[dx y] & remaining] coordinates] 79 | (-> audio-node .-gain (.linearRampToValueAtTime y (+ x dx))) 80 | (recur (+ dx x) remaining))) 81 | (subgraph audio-node)))) 82 | 83 | (defn adshr 84 | "An ADSR envelope that also lets you specify the hold duration." 85 | [attack decay sustain hold release] 86 | (envelope [attack 1.0] [decay sustain] [hold sustain] [release 0])) 87 | 88 | (defn ^:export adsr 89 | "A four-stage envelope." 90 | [attack decay sustain release] 91 | (fn [context at duration] 92 | (let [remainder (- duration attack decay release) 93 | hold (max 0.0 remainder) 94 | node (adshr attack decay sustain hold release)] 95 | (-> node (run-with context at duration))))) 96 | 97 | (defn ^:export percussive 98 | "A simple envelope." 99 | [attack decay] 100 | (envelope [attack 1.0] [decay 0.0])) 101 | 102 | 103 | ; Combinators 104 | 105 | (defn apply-to-graph 106 | "Like apply, but for the node graphs synths produce." 107 | [f & synths] 108 | (fn [context at duration] 109 | (->> synths 110 | (map #(run-with % context at duration)) 111 | (apply f)))) 112 | 113 | (defn join-in-series 114 | [graph1 graph2] 115 | (.connect (:output graph1) (:input graph2)) 116 | (subgraph (:input graph1) (:output graph2))) 117 | 118 | (defn ^:export connect 119 | "Use the output of one synth as the input to another." 120 | [upstream-synth downstream-synth] 121 | (apply-to-graph join-in-series upstream-synth downstream-synth)) 122 | 123 | (defn connect-> 124 | "Connect synths in series." 125 | [& nodes] 126 | (reduce connect nodes)) 127 | 128 | (defn join-in-parallel 129 | [upstream downstream & graphs] 130 | (doseq [graph graphs] 131 | (.connect (:output graph) (:input downstream)) 132 | (when (:input graph) 133 | (.connect (:output upstream) (:input graph)))) 134 | (subgraph (:input upstream) (:output downstream))) 135 | 136 | (defn ^:export add 137 | "Add together synths by connecting them all to the same 138 | upstream and downstream gains." 139 | [& synths] 140 | (apply 141 | apply-to-graph 142 | join-in-parallel 143 | pass-through pass-through synths)) 144 | 145 | 146 | ; Noise 147 | 148 | (defn raw-buffer 149 | [generate-bit! context duration] 150 | (let [sample-rate 44100 151 | frame-count (* sample-rate duration) 152 | buffer (.createBuffer context 1 frame-count sample-rate) 153 | data (.getChannelData buffer 0)] 154 | (doseq [i (range frame-count)] 155 | (aset data i (generate-bit! i))) 156 | buffer)) 157 | 158 | (def buffer (memoize raw-buffer)) 159 | 160 | (defn noise 161 | "Make noise according to the supplied strategy for creating bits." 162 | [generate-bit!] 163 | (fn [context at duration] 164 | (source 165 | (doto (.createBufferSource context) 166 | (-> .-buffer (set! (buffer generate-bit! context (+ duration 1.0)))) 167 | (.start at))))) 168 | 169 | (def ^:export white-noise 170 | "Random noise." 171 | (let [white (fn [_] (-> (js/Math.random) (* 2.0) (- 1.0)))] 172 | (noise white))) 173 | 174 | (defn ^:export constant 175 | "Make a constant value by creating noise with a fixed value." 176 | [x] 177 | (noise (constantly x))) 178 | 179 | ; Oscillators 180 | 181 | (defn oscillator 182 | "A periodic wave." 183 | [type freq] 184 | (fn [context at duration] 185 | (source 186 | (doto (.createOscillator context) 187 | (-> .-frequency .-value (set! 0)) 188 | (-> .-frequency (plug freq context at duration)) 189 | (-> .-type (set! type)) 190 | (.start at) 191 | (.stop (+ at duration 1.0)))))) ; Give a bit extra for the release 192 | 193 | (def ^:export sine (partial oscillator "sine")) 194 | (def ^:export sawtooth (partial oscillator "sawtooth")) 195 | (def ^:export square (partial oscillator "square")) 196 | (def ^:export triangle (partial oscillator "triangle")) 197 | 198 | 199 | ; Filters 200 | 201 | (defn biquad-filter 202 | "Attenuate frequencies beyond the cutoff, and intensify 203 | the cutoff frequency based on the value of q." 204 | ([type freq] 205 | (biquad-filter type freq 1.0)) 206 | ([type freq q] 207 | (fn [context at duration] 208 | (subgraph 209 | (doto (.createBiquadFilter context) 210 | (-> .-frequency .-value (set! 0)) 211 | (-> .-frequency (plug freq context at duration)) 212 | (-> .-Q (plug q context at duration)) 213 | (-> .-type (set! type))))))) 214 | 215 | (def ^:export low-pass (partial biquad-filter "lowpass")) 216 | (def ^:export high-pass (partial biquad-filter "highpass")) 217 | 218 | 219 | ; Effects 220 | 221 | (defn ^:export stereo-panner 222 | "Pan the signal left (-1) or right (1)." 223 | [pan] 224 | (fn [context at duration] 225 | (subgraph 226 | (doto (.createStereoPanner context) 227 | (-> .-pan (plug pan context at duration)))))) 228 | 229 | (defn ^:export delay-line 230 | "Delay the signal." 231 | [seconds] 232 | (fn [context at duration] 233 | (subgraph 234 | (let [maximum 5] 235 | (doto (.createDelay context maximum) 236 | (-> .-delayTime (plug seconds context at duration))))))) 237 | 238 | (defn convolver 239 | "Linear convolution." 240 | [generate-bit!] 241 | (fn [context at duration] 242 | (subgraph 243 | (doto (.createConvolver context) 244 | (-> .-buffer (set! (buffer generate-bit! context (+ duration 1.0)))))))) 245 | 246 | (def ^:export reverb 247 | "Crude reverb." 248 | (let [duration 5 249 | decay 3 250 | sample-rate 44100 251 | length (* sample-rate (+ duration 1.0)) 252 | logarithmic-decay (fn [i] 253 | (* (-> i (js/Math.random) (* 2.0) (- 1.0)) 254 | (Math/pow (- 1 (/ i length)) decay)))] 255 | (convolver logarithmic-decay))) 256 | 257 | (defn ^:export enhance 258 | "Mix the original signal with one with the effect applied." 259 | [effect level] 260 | (add pass-through (connect-> effect (gain level)))) 261 | 262 | (defn get-mp3 [uri callback] 263 | (ajax/GET uri {:response-format {:type :arraybuffer 264 | :read protocol/-body 265 | :description "audio" 266 | :content-type "audio/mpeg"} 267 | :handler callback})) 268 | 269 | (defn raw-sample 270 | "Play a sample addressed via a URI. Until fetching and decoding is complete, it will play silence." 271 | [uri] 272 | (let [psuedo-promise (js-obj)] ; A mutable object to close over and share between calls. 273 | (get-mp3 uri #(set! (.-data psuedo-promise) %)) ; GET, then deliver the data by updating the mutable object. 274 | (fn [context at duration] 275 | (source 276 | (let [node (doto (.createBufferSource context) 277 | (.start at) 278 | (.stop (+ at duration))) 279 | set-buffer (fn [buffer] 280 | (set! (.-buffer psuedo-promise) buffer) ; Save it for later. 281 | (-> node .-buffer (set! buffer)))] ; Set it on the audio node. 282 | (when-let [data (.-data psuedo-promise)] ; Has the ajax call returned? 283 | (if-let [buffer (.-buffer psuedo-promise)] ; Has the buffer been decoded? 284 | (set-buffer buffer) ; Already decoded, so set it. 285 | (.decodeAudioData context data set-buffer))) ; Decode it and then set it. 286 | node))))) 287 | 288 | (def ^:export sample (memoize raw-sample)) 289 | -------------------------------------------------------------------------------- /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=1.8 2 | --------------------------------------------------------------------------------