Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use JSyn for realtime audio scheduling 💻 🎶 #166

Merged
merged 1 commit into from
Jan 1, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)))