Skip to content

Latest commit

 

History

History
1415 lines (1118 loc) · 30 KB

performance.org

File metadata and controls

1415 lines (1118 loc) · 30 KB

Experiments

Baseline

(let [max-t (double (* 44100 1.0))
      inc-t (double (/ 1.0 44100))]
  (time (loop [t 0.0]
          (when (< t max-t)
            (recur (+ t inc-t))))))

“Elapsed time: 5100.59828 msecs”

Holy crap!

Only wait, this is for 44100 seconds of audio, not one second like I thought.

Type hints

(let [^double max-t (double (* 44100 1.0))
      ^double inc-t (double (/ 1.0 44100))]
  (time (loop [t 0.0]
          (when (< t max-t)
            (recur (+ t inc-t))))))

Fails because you can’t primitive-hint locals like that. The `(double)` declaration ought to be enough to get it to use primitive math. So WTF is it so slow?

Using doseq instead

(let [max-t (double (* 44100 1.0))
      inc-t (double (/ 1.0 44100))]
  (time (doseq [t (range 0 max-t inc-t)])))

“Elapsed time: 55371.522445 msecs”

Yaa!

Only wait, this is for 44100 seconds of audio, not one second like I thought.

Debugger?

Hmm. I have the debugging enabled in my project. Wonder if that matters? Turning it off and doing this again:

(let [max-t (double (* 44100 1.0))
      inc-t (double (/ 1.0 44100))]
  (time (loop [t 0.0]
          (when (< t max-t)
            (recur (+ t inc-t))))))

“Elapsed time: 5087.389305 msecs”

Nope.

Without the double casts?

(let [max-t (* 44100 1.0)
      inc-t (/ 1.0 44100)]
  (time (loop [t 0.0]
          (when (< t max-t)
            (recur (+ t inc-t))))))

“Elapsed time: 5101.189955 msecs”

With a literal zero for the initial value of t

(let [max-t (* 44100 1.0)
      inc-t (/ 1.0 44100)]
  (time (loop [t 0]
          (when (< t max-t)
            (recur (+ t inc-t))))))

“Elapsed time: 7947.990526 msecs”

With an long zero for the initial value of t

(let [max-t (* 44100 1.0)
      inc-t (/ 1.0 44100)]
  (time (loop [t (long 0)]
          (when (< t max-t)
            (recur (+ t inc-t))))))

“Elapsed time: 7411.203103 msecs”

With an int zero for the initial value of t and a cast on max-t

(let [max-t (double (* 44100 1.0))
      inc-t (/ 1.0 44100)]
  (time (loop [t (int 0)]
          (when (< t max-t)
            (recur (+ t inc-t))))))

“Elapsed time: 7299.571628 msecs”

Same thing, but with the profiler killed and the repl restarted

(let [max-t (double (* 44100 1.0))
      inc-t (/ 1.0 44100)]
  (time (loop [t (int 0)]
          (when (< t max-t)
            (recur (+ t inc-t))))))

“Elapsed time: 7406.9407 msecs”

With only literal values

(time
 (loop [t 0]
   (when (< t 1.0)
     (recur (+ t 2.2675736961451248E-5)))))

“Elapsed time: 3.055637 msecs”

With division in the loop

(time
 (loop [t 0]
   (when (< t 1.0)
     (recur (+ t (/ 1.0 44100))))))

“Elapsed time: 4.770925 msecs”

With division by a local in the loop

(let [inc-t (/ 1.0 44100)]
  (time
   (loop [t 0]
     (when (< t 1.0)
       (recur (+ t inc-t))))))

“Elapsed time: 3.64431 msecs”

With comparison to local in the loop

(let [inc-t (/ 1.0 44100)
      max-t 1.0]
  (time
   (loop [t 0]
     (when (< t max-t)
       (recur (+ t inc-t))))))

“Elapsed time: 4.422404 msecs”

One hour of raw iteration via loop

(let [inc-t (/ 1.0 44100)
      max-t 3600.0]
  (time
   (loop [t 0]
     (when (< t max-t)
       (recur (+ t inc-t))))))

One hour of Math/sin via loop

(let [inc-t (/ 1.0 44100)
      max-t 3600.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (Math/sin t)
       (recur (+ t inc-t))))))

One minute of sampling null sound

(let [inc-t (/ 1.0 44100)
      max-t 60.0
      s (null-sound)]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t)
       (recur (+ t inc-t))))))

One minute of sampling silence

(let [inc-t (/ 1.0 44100)
      max-t 60.0
      s (silence 60)]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t)
       (recur (+ t inc-t))))))

Wow - that’s much faster than the null sound. Something in `sample` must be slow. I suspect the call to vec. Let’s try taking that out.

One minute of sampling null sound without vec

(let [inc-t (/ 1.0 44100)
      max-t 60.0
      s (null-sound)]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t)
       (recur (+ t inc-t))))))

Even slower without the vec! Maybe we should try to make generating the zero-samples faster.

One minute of sampling null sound with memoized zeros

(let [inc-t (/ 1.0 44100)
      max-t 60.0
      s (null-sound)]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t)
       (recur (+ t inc-t))))))

Still slow! Maybe it’s calling channels that’s slow, not the zeros. Let’s try memoizing based on the sound, too.

One minute of sampling null sound with memoized zeros based on sound

(let [inc-t (/ 1.0 44100)
      max-t 60.0
      s (null-sound)]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t)
       (recur (+ t inc-t))))))

Wow - much, much better. OK, so channels is probably slow. Why?

Baseline performance of 1M loop

(let [n 1000000]
  (time
   (loop [n n]
     (when (pos? n)
       (recur (dec n))))))

Baseline performance of calling current impl of channels 1M times

(let [n 1000000
      s (linear 1.0 1 0)]
  (time
   (loop [n n]
     (when (pos? n)
       (channels s)
       (recur (dec n))))))

So, yes: slow

Is satisfies? slow?

(let [n 1000000
      s (null-sound)]
  (time
   (loop [n n]
     (when (pos? n)
       (satisfies? impl/Sound s)
       (recur (dec n))))))

Nope.

OK, so is the new version of channels that reifies ChannelCount still slow?

(let [n 1000000
      s (linear 1.0 1 0)]
  (time
   (loop [n n]
     (when (pos? n)
       (channels s)
       (recur (dec n))))))

Hah. No.

OK, so now is sampling the null sound slow?

(let [inc-t (/ 1.0 44100)
      max-t 60.0
      s (null-sound)]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t)
       (recur (+ t inc-t))))))

Much faster!

How is it to iterate over one minute of a WAV file?

(let [inc-t (/ 1.0 44100)
      max-t 60.0
      s (read-sound "sin.wav")]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t)
       (recur (+ t inc-t))))))

Huh! Not bad. Let’s whip out a profiler and see what’s slow. Signs point to it being vec inside of read-sound’s reification of amplitudes.

After removing the call to vec

(let [inc-t (/ 1.0 44100)
      max-t 60.0
      s (read-sound "sin.wav")]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t)
       (recur (+ t inc-t))))))

Quite a bit better. And over 60x realtime. I call that a victory. Still, the profiler says that we’re spending a lot of time in `second`. Let’s see if replacing that with nth is any better.

After removing the call to second by not using destructuring

(let [inc-t (/ 1.0 44100)
      max-t 60.0
      s (read-sound "sin.wav")]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t)
       (recur (+ t inc-t))))))

Hmm. Not much change. But I found other places where we’re calling first and second

After removing calls to first and second

(let [inc-t (/ 1.0 44100)
      max-t 60.0
      s (read-sound "sin.wav")]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t)
       (recur (+ t inc-t))))))

Wow! Much better again! Up to 120x realtime.

Now the profiler says it’s get. So let’s see if we can refactor that out of there.

After removing calls to get

(let [inc-t (/ 1.0 44100)
      max-t 60.0
      s (read-sound "sin.wav")]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t)
       (recur (+ t inc-t))))))

A little better. I think we might be at the point of diminishing returns. Profiler says the dominating factor is now self-time in sample. Which I’m not sure how to optimize any further easily. Let’s move on.

Using an array rather than repeatedly in read-sound

(let [inc-t (/ 1.0 44100)
      max-t 60.0
      s (read-sound "sin.wav")]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t)
       (recur (+ t inc-t))))))

Slightly worse, but the other way was actually broken due to some laziness issue I never figured out. Let’s call this good for now.

4x oversampling of one minute of a file

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample s t 4 delta-t)
       (recur (+ t inc-t))))))

Wow. Slow, slow, slow. Profiler says it’s probably seq-related, since oversample does a whole bunch of sequence processing. Seems like a good time to try reducers out…

With map instead of mapv in oversample

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample s t 4 delta-t)
       (recur (+ t inc-t))))))

Which is only slightly better. We need something else.

With loops instead of mapping

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample s t 4 delta-t)
       (recur (+ t inc-t))))))

Much better. The profiler tells us that nth is the culprit. Maybe we can get rid of that.

With a doseq instead of nth

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample s t 4 delta-t)
       (recur (+ t inc-t))))))

At this point, I’m not sure I can do much better without restructuring the way the code works.

After the refactoring to explicit channel numbers in sample

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t 0)
       (recur (+ t inc-t))))))

After removing type hints on sample and optimizing <=

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t 0)
       (recur (+ t inc-t))))))

Which is pretty good. At this point we think the performance is gated by the boxing that’s going on because we’re using a protocol. Next step: use an interface instead.

After refactoring to a Java interface rather than a protocol

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t 0)
       (recur (+ t inc-t))))))

No faster. Hmm.

After putting type hints on the read-sound reify

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t 0)
       (recur (+ t inc-t))))))

Now, how does that compare to a not-file sound?

Sampling of silence

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (silence 60.0)
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (sample s t 0)
       (recur (+ t inc-t))))))

OK, so that’s sort of the theoretical minimum. What about oversampling?

Oversampling of silence

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (silence 60.0)
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

And how does that compare to a file sound?

Oversampling of file-based sound

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

Interesting. What about that is slow, exactly?

Calling into .amplitude directly on a file-based sound

(let [inc-t (/ 1.0 44100 4.0)
      delta-t (/ inc-t 4)
      s ^dynne.sound.impl.ISound (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (.amplitude s t 0)
       (recur (+ t inc-t))))))

OK. And I just noticed that there are reflection and performance warnings. Let’s fix those.

Oversampling silence after fixing some performance warnings

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (silence 60.0)
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

Much better! Was about 480 msecs before.

Oversampling file-based sound after fixing some performance warnings

Previous results: 1200 ms

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

So not really any better. But that has to mostly be in the file stuff, because sampling silence is substantially faster.

Calling into .amplitude directly on a file-based sound again

(let [inc-t (/ 1.0 44100 4.0)
      delta-t (/ inc-t 4)
      s ^dynne.sound.impl.ISound (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (.amplitude s t 0)
       (recur (+ t inc-t))))))

Yep. So most of the time we’re spending in oversample is actually calling into .amplitude. The profiler says that most of our time is in self-time of the above function, which doesn’t seem likely, since if I comment out the call to .amplitude, it’s only 30msec. But other than that, it complains that “clojure.lang.Numbers.lt” is the next-biggest thing!

Calling into .amplitude directly on a file-based sound again, but with casting to try to get lt to be faster

(let [inc-t (/ 1.0 44100 4.0)
      delta-t (/ inc-t 4)
      s ^dynne.sound.impl.ISound (read-sound "sin-long.wav")
      max-t 3600.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (.amplitude s t 0)
       (recur (+ t inc-t))))))

Calling .amplitude directly after moving away from atoms to a custom, mutable thingy

(let [inc-t (/ 1.0 44100 4.0)
      delta-t (/ inc-t 4)
      s ^dynne.sound.impl.ISound (read-sound "sin-long.wav")
      max-t 3600.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (.amplitude s t 0)
       (recur (+ t inc-t))))))

That’s actually a fair improvement. Profiler says nothing useful at this point. Might need to try YourKit to see if it’s any better. Still: good progress. Let’s see what oversampling looks like.

Oversampling file-based sound after moving away from atoms

Previous results:

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

Yep: much better. Going to call it a day and commit this.

Utility blocks

oversample a whole sound file

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t (duration s)]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       ;;(sample s t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

Experiments, round 2

Repeat oversampling file-based sound after moving away from atoms

Previous results:

At commit 96de01a

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

Wow - much slower. Let’s try again with an older commit.

Trying against with commit 94efc1d

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

Still really slow. WTF?!

Trying again with commit b134fd981895e5c5f169c18de52108a4bae0faf9

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

Do not understand…

Against a sinusoid with commit b134fd981895e5c5f169c18de52108a4bae0faf9

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (sinusoid 600 440)
      max-t 600.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

Wow. That should not be anywhere near that slow…how was it as fast as it was before?

The weird thing is, the profiler is reporting that ll the time is in the benchmarking code, not in the dynne code itself. Maybe I should try this on a different OS.

Again with 10a0cc1a967f76f7f889ac1ce3e503fe7139ef1f, on Ubuntu

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (sinusoid 600 440)
      max-t 600.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

None, because it ran so long I killed it. Going to reboot and see if that helps.

Again with 10a0cc1a967f76f7f889ac1ce3e503fe7139ef1f, on Windows after a reboot

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (sinusoid 600 440)
      max-t 1.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

Holy crap, we’re actually slower than real-time. Profiler time!

Again with 10a0cc1a967f76f7f889ac1ce3e503fe7139ef1f, on Windows after a reboot

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (sinusoid 600 440)
      max-t 1.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

Still slower than real-time

Again with 10a0cc1a967f76f7f889ac1ce3e503fe7139ef1f, disabling debugging

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (sinusoid 600 440)
      max-t 10.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

Still incredibly slow. Profiler says it’s java.lang.reflect.getParameterTypes, followed by java.lang.Class.forName, seemingly from within sample. Hmm.

After adding more type hints to sample, oversample4, and a few other things

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (sinusoid 600 440)
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

Wah! That certainly helped.

Now back to the previous examples

Previous results: “Elapsed time: 819.078953 msecs”

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 10.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

Back to slower than real time.

After many, many more type hints and reflection removal

Previous results:

;; "Elapsed time: 1278.849554 msecs"
(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (sinusoid 600 440)
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

Wah! That certainly helped.

Same thing against a file-based sound

(let [inc-t (/ 1.0 44100)
      delta-t (/ inc-t 4)
      s (read-sound "sin.wav")
      max-t 60.0]
  (time
   (loop [t 0.0]
     (when (< t max-t)
       (oversample4 s t 0 delta-t)
       (recur (+ t inc-t))))))

Woot!

Experiments, round 3

A simple combination of operations, baseline

(let [dur 60
      path (str  "test" dur ".wav")]
  (when-not (.exists (io/file path))
    (save (->stereo (sinusoid dur 440)) path 44100))
  (-> path
      read-sound
      ;;(sinusoid dur 440)
      ->stereo
      (fade-in 20)
      (fade-out 20)
      (mix (-> (square-wave 60 880)
               (timeshift (/ dur 2))
               (->stereo)))
      ;;(visualize)
      ;;(save (str "test-out" dur ".wav") 44100)
      oversample-all
      time
      ))

Meh. OK at 6.1x realtime. Hope to do better.

A simple combination of operations, baseline, without lein deoptimization

(let [dur 60
      path (str  "test" dur ".wav")]
  (when-not (.exists (io/file path))
    (save (->stereo (sinusoid dur 440)) path 44100))
  (-> path
      read-sound
      ;;(sinusoid dur 440)
      ->stereo
      (fade-in 20)
      (fade-out 20)
      (mix (-> (square-wave 60 880)
               (timeshift (/ dur 2))
               (->stereo)))
      ;;(visualize)
      ;;(save (str "test-out" dur ".wav") 44100)
      oversample-all
      time
      ))

Significantly better: 6.9x realtime. Still not great.

A simple combination of operations, baseline, with inline metadata

(let [dur 60
      path (str  "test" dur ".wav")]
  (when-not (.exists (io/file path))
    (save (->stereo (sinusoid dur 440)) path 44100))
  (-> path
      read-sound
      ;;(sinusoid dur 440)
      ->stereo
      (fade-in 20)
      (fade-out 20)
      (mix (-> (square-wave 60 880)
               (timeshift (/ dur 2))
               (->stereo)))
      ;;(visualize)
      ;;(save (str "test-out" dur ".wav") 44100)
      oversample-all
      time
      ))

I’m pretty sure that the way I was using :inline metadata was totally wrong.

With sample as a macro rather than a function

Previous results:

;; "Elapsed time: 8605.301345 msecs"
(let [dur 60
      path (str  "test" dur ".wav")]
  (when-not (.exists (io/file path))
    (save (->stereo (sinusoid dur 440)) path 44100))
  (-> path
      read-sound
      ;;(sinusoid dur 440)
      ->stereo
      (fade-in 20)
      (fade-out 20)
      (mix (-> (square-wave 60 880)
               (timeshift (/ dur 2))
               (->stereo)))
      ;;(visualize)
      ;;(save (str "test-out" dur ".wav") 44100)
      oversample-all
      time
      ))

Yep, quite a bit better again: 16x realtime. And at this point I’m pretty much out of ideas for how to make it faster without changing the underlying metaphor of functional composition. Maybe something to talk to other people about.

Symbolic composition

Baseline

(let [dur 60
      s1 (sinusoid dur 440)
      s2 (sinusoid dur 1234)]
  (-> s1
      (multiply s2)
      (multiply s2)
      (multiply s2)
      (multiply s2)
      (multiply s2)
      (multiply s2)
      (multiply s2)
      oversample-all
      time
))

Symbolically

(let [dur 60
      s1 (sinusoid dur 440)
      s2 (sinusoid dur 1234)
      i1 (ops/input :s1)
      i2 (ops/input :s2)
      op (ops/compile
          (-> i1
              (ops/multiply i2)
              (ops/multiply i2)
              (ops/multiply i2)
              (ops/multiply i2)
              (ops/multiply i2)
              (ops/multiply i2)
              (ops/multiply i2)))]
  (-> (op {:s1 s1 :s2 s2})
      oversample-all
      time
))

That’s not bad. Maybe 25% better.

Sampled representation

Helper functions

(require '[hiphip.double :as dbl])
(require '[primitive-math :as p])

(defn mono-chunk-seq [chunk-size]
  (repeat [(double-array chunk-size 1.0)]))

(defn stereo-chunk-seq [chunk-size]
  (repeat [(double-array chunk-size 1.0) (double-array chunk-size 1.0)]))

Baseline

How fast can we simply iterate through a sequence of vectors of double arrays, just looking at each element without doing anything to it?

(time
 (let [chunk-size 44100
       num-chunks (* 60 60)]
   (doseq [[chunk] (take num-chunks (mono-chunk-seq chunk-size))]
     (dotimes [n chunk-size]
       (dbl/aget chunk n)))))

Not bad! That’s an hour’s worth of samples at 44100, with a 1.0-second chunk size.

With a simple operation

(time
 (let [chunk-size 10000
       num-chunks (->> chunk-size (/ 44100) (* 60 60 ))]
   (doseq [[chunk-l chunk-r] (take num-chunks (stereo-chunk-seq chunk-size))]
     (dbl/amap [l chunk-l r chunk-r] (p/+ l r)))))

OK, wow, that’s pretty good. Let’s go build something on this.