Skip to content

Commit

Permalink
Merge pull request #166 from alda-lang/jsyn
Browse files Browse the repository at this point in the history
Use JSyn for realtime audio scheduling 💻 🎶
  • Loading branch information
daveyarwood committed Jan 1, 2016
2 parents 3bba0cd + aa128e5 commit 4d29d75
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 35 deletions.
24 changes: 12 additions & 12 deletions build.boot
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@
[adzerk/boot-test "1.0.4" :scope "test"]

; server
[org.clojure/clojure "1.7.0"]
[instaparse "1.4.1"]
[io.aviso/pretty "0.1.20"]
[com.taoensso/timbre "4.1.1"]
[clj-http "2.0.0"]
[ring "1.4.0"]
[ring/ring-defaults "0.1.5"]
[compojure "1.4.0"]
[djy "0.1.4"]
[str-to-argv "0.1.0"]
[overtone/at-at "1.2.0"]
[jline "2.12.1"]
[org.clojure/clojure "1.7.0"]
[instaparse "1.4.1"]
[io.aviso/pretty "0.1.20"]
[com.taoensso/timbre "4.1.1"]
[clj-http "2.0.0"]
[ring "1.4.0"]
[ring/ring-defaults "0.1.5"]
[compojure "1.4.0"]
[djy "0.1.4"]
[str-to-argv "0.1.0"]
[jline "2.12.1"]
[org.clojars.sidec/jsyn "16.7.3"]

; client
[com.beust/jcommander "1.48"]
Expand Down
6 changes: 4 additions & 2 deletions doc/development-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,11 @@ Because `alda.lisp` is a Clojure DSL, it's possible to use it to build scores wi

The `alda.sound` namespace handles the implementation details of playing the score.

There is an "audio type" abstraction which refers to different ways to generate audio, e.g. MIDI, waveform synthesis, samples, etc. Adding a new audio type is as simple as providing an implementation for each of the multimethods in this namespace, i.e. `set-up-audio-type!`, `refresh-audio-type!`, `tear-down-audio-type!` and `play-event!`.
There is an "audio type" abstraction which refers to different ways to generate audio, e.g. MIDI, waveform synthesis, samples, etc. Adding a new audio type is as simple as providing an implementation for each of the multimethods in this namespace, i.e. `set-up-audio-type!`, `refresh-audio-type!`, `tear-down-audio-type!`, `start-event!` and `stop-event!`.

The `play!` function handles playing an entire Alda score. It does this by using [overtone.at-at](https://github.com/overtone/at-at) to schedule all of the note events to be played via `play-event!`, based on the `:offset` of each event.
The `play!` function handles playing an entire Alda score. It does this by using a [JSyn](http://www.softsynth.com/jsyn) SynthesisEngine to schedule all of the note events to be played in realtime. The time that each event starts and stops is determined by its `:offset` and `:duration`.

What happens, exactly, at the beginning and end of an event, is determined by the `start-event!`/`stop-event!` implementations for each instrument type. For example, for MIDI instruments, `start-event!` sets parameters such as volume and panning and sends a MIDI note-on message at the beginning of the score plus `:offset` milliseconds, and `stop-event!` sends a note-off message `:duration` milliseconds later.

#### alda.lisp.instruments

Expand Down
68 changes: 50 additions & 18 deletions server/src/alda/sound.clj
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
(ns alda.sound
(:require [alda.sound.midi :as midi]
[overtone.at-at :refer (mk-pool now at)]
[taoensso.timbre :as log]
[alda.lisp]
[alda.util :refer (check-for parse-time pdoseq-block parse-position)]))
[alda.util :refer (check-for parse-time pdoseq-block parse-position)])
(:import [com.softsynth.shared.time TimeStamp ScheduledCommand]
[com.jsyn.engine SynthesisEngine]))

(def ^:dynamic *active-audio-types* #{})
(def ^:dynamic *synthesis-engine* (doto (SynthesisEngine.) .start))

(defn set-up?
[x]
Expand Down Expand Up @@ -99,27 +101,48 @@

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defmulti play-event!
"Plays a note/event, using the appropriate method based on the type of the
(defmulti start-event!
"Kicks off a note/event, using the appropriate method based on the type of the
instrument."
(fn [event instrument]
(-> instrument :config :type)))

(defmethod play-event! :default
(defmethod start-event! :default
[_ instrument]
(log/errorf "No implementation of play-event! defined for type %s"
(log/errorf "No implementation of start-event! defined for type %s"
(-> instrument :config :type)))

(defmethod play-event! nil
(defmethod start-event! nil
[event instrument]
:do-nothing)

(defmethod play-event! :midi
(defmethod start-event! :midi
[note instrument]
(midi/play-note! note))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defmulti stop-event!
"Ends a note/event, using the appropriate method based on the type of the
instrument."
(fn [event instrument]
(-> instrument :config :type)))

(defmethod stop-event! :default
[_ instrument]
(log/errorf "No implementation of start-event! defined for type %s"
(-> instrument :config :type)))

(defmethod stop-event! nil
[event instrument]
:do-nothing)

(defmethod stop-event! :midi
[note instrument]
(midi/stop-note! note))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn- score-length
"Calculates the length of a score in ms."
[{:keys [events] :as score}]
Expand Down Expand Up @@ -170,24 +193,33 @@
audio-types (determine-audio-types score)
_ (set-up! audio-types score)
_ (refresh! audio-types score)
pool (mk-pool)
playing? (atom true)
begin (+ (now) (or pre-buffer 0))
begin (+ (.getCurrentTime *synthesis-engine*)
(or pre-buffer 0))
[start end] (start-finish-times *play-opts* markers)
events (shift-events events start end)
duration (- (or end (score-length score))
(or start 0))]
(pdoseq-block [{:keys [offset instrument] :as event} events
(pdoseq-block [{:keys [offset instrument duration] :as event} events
:let [inst (-> instrument instruments)]]
(at (+ begin offset)
#(when @playing?
(if (= (type event) alda.lisp.Function)
((:function event))
(play-event! event inst)))
pool))

(let [start-ts (TimeStamp. (+ begin (/ offset 1000.0)))
stop-ts (TimeStamp. (+ begin (/ offset 1000.0)
(/ duration 1000.0)))
start-cmd (proxy [ScheduledCommand] []
(run []
(when @playing?
(if (= (type event) alda.lisp.Function)
((:function event))
(start-event! event inst)))))
stop-cmd (proxy [ScheduledCommand] []
(run []
(when-not (= (type event) alda.lisp.Function)
(stop-event! event inst))))]
(.scheduleCommand *synthesis-engine* start-ts start-cmd)
(.scheduleCommand *synthesis-engine* stop-ts stop-cmd)))
(when-not async?
; block until the score is done playing
; TODO: find a way to handle this that doesn't involve Thread/sleep
(Thread/sleep (+ duration
(or pre-buffer 0)
(or post-buffer 0))))
Expand Down
10 changes: 7 additions & 3 deletions server/src/alda/sound/midi.clj
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,17 @@
(.close *midi-synth*))

(defn play-note!
[{:keys [midi-note instrument duration volume track-volume panning]}]
[{:keys [midi-note instrument volume track-volume panning]}]
(let [channel-number (-> instrument *midi-channels* :channel)
channel (aget (.getChannels *midi-synth*) channel-number)]
(.controlChange channel 7 (* 127 track-volume))
(.controlChange channel 10 (* 127 panning))
(log/debugf "Playing note %s on channel %s." midi-note channel-number)
(.noteOn channel midi-note (* 127 volume))
(Thread/sleep duration)
(.noteOn channel midi-note (* 127 volume))))

(defn stop-note!
[{:keys [midi-note instrument]}]
(let [channel-number (-> instrument *midi-channels* :channel)
channel (aget (.getChannels *midi-synth*) channel-number)]
(log/debug "MIDI note off:" midi-note)
(.noteOff channel midi-note)))

0 comments on commit 4d29d75

Please sign in to comment.