<commit_before>(ns klangmeister.sound.synthesis)

; Plumbing

(defn run-with
  "Convert a node (actually a reader fn) into a concrete audio node by supplying context and timing."
  [node context at duration]
  (node context at duration))

(defn destination [context at duration]
  (.-destination context))

(defn plug [param input context at duration]
  "Plug an input into an audio parameter, accepting both numbers and nodes."
  (if (number? input)
    (.setValueAtTime param input at)
    (-> input (run-with context at duration) (.connect param))))

(defn gain [level]
  (fn [context at duration]
    (doto (.createGain context)
      (-> .-gain (plug level context at duration)))))


; Envelopes

(defn envelope
  "Build an envelope out of [segment-duration final-level] coordinates."
  [& corners]
  (fn [context at duration]
    (let [audio-node (.createGain context)]
      (-> audio-node .-gain (.setValueAtTime 0 at))
      (reduce
        (fn [x [dx y]]
          (-> audio-node .-gain (.linearRampToValueAtTime y (+ x dx)))
          (+ dx x))
        at
        corners)
      audio-node)))

(defn adshr [attack decay sustain hold release]
  (envelope [attack 1.0] [decay sustain] [hold sustain] [release 0]))

(defn adsr [attack decay sustain release]
  (fn [context at duration]
    (let [remainder (- duration attack decay sustain)
          hold (max 0.0 remainder)
          node (adshr attack decay sustain hold release)]
      (-> node (run-with context at duration)))))

(defn percussive [attack decay]
  (envelope [attack 1.0] [decay 0.0]))


; Combinators

(defn connect
  "Use the output of one node as the input to another."
  [node1 node2]
  (fn [context at duration]
    (let [sink (-> node2 (run-with context at duration))]
      (-> node1 (run-with context at duration) (.connect sink))
      sink)))

(defn connect->
  "Connect nodes in series."
  [& nodes]
  (reduce connect nodes))

(defn add
  "Add together nodes by connecting them all to the same gain."
  [& nodes]
  (fn [context at duration]
    (let [sink (-> (gain 1.0) (run-with context at duration))]
      (doseq [node nodes]
        (-> node (run-with context at duration) (.connect sink)))
      sink)))


; Noise

(defn noise
  "Make noise according to the supplied strategy for creating bits."
  [generate-bit!]
  (fn [context at duration]
    (let [sample-rate 44100
          frame-count (* sample-rate (+ duration 1.0)) ; Give a bit of extra for the release.
          buffer (.createBuffer context 1 frame-count sample-rate)
          data (.getChannelData buffer 0)]
      (doseq [i (range sample-rate)]
        (aset data i (generate-bit!)))
      (doto (.createBufferSource context)
        (-> .-buffer (set! buffer))
        (.start at)))))

(def white-noise
  (let [white #(-> (js/Math.random) (* 2.0) (- 1.0))]
    (noise white)))

(defn constant
  "Make a constant value by creating noise with a fixed value."
  [x]
  (noise (constantly x)))

; Oscillators

(defn oscillator
  [type freq]
  (fn [context at duration]
    (doto (.createOscillator context)
      (-> .-frequency .-value (set! 0))
      (-> .-frequency (plug freq context at duration))
      (-> .-type (set! type))
      (.start at)
      (.stop (+ at duration 1.0))))) ; Give a bit extra for the release

(def sine (partial oscillator "sine"))
(def sawtooth (partial oscillator "sawtooth"))
(def square (partial oscillator "square"))
(def triangle (partial oscillator "triangle"))


; Filters

(defn biquad-filter
  ([type freq q]
   (fn [context at duration]
     (doto (-> (biquad-filter type freq) (run-with context at duration))
       (-> .-Q (plug q context at duration)))))
  ([type freq]
   (fn [context at duration]
     (doto (.createBiquadFilter context)
       (-> .-frequency .-value (set! 0))
       (-> .-frequency (plug freq context at duration))
       (-> .-type (set! type))))))

(def low-pass (partial biquad-filter "lowpass"))
(def high-pass (partial biquad-filter "highpass"))


; Effects

(defn stereo-panner [pan]
  (fn [context at duration]
    (doto (.createStereoPanner context)
      (-> .-pan (plug pan context at duration)))))

(defn delay-line
  [time]
  (fn [context at duration]
    (let [maximum 5]
      (doto (.createDelay context maximum)
        (-> .-delayTime (plug time context at duration))))))

; FIXME: Unify with noise.
(defn convolver
  [generate-bit!]
  (fn [context at duration]
    (let [sample-rate 44100
          frame-count (* sample-rate (+ duration 1.0)) ; Give a bit of extra for the release.
          buffer (.createBuffer context 1 frame-count sample-rate)
          data (.getChannelData buffer 0)]
      (doseq [i (range sample-rate)]
        (aset data i (generate-bit! i)))
      (doto (.createConvolver context)
        (-> .-buffer (set! buffer))))))

(def reverb
  (let [duration 5
        decay 3
        sample-rate 44100
        length (* sample-rate (+ duration 1.0))
        logarithmic-decay (fn [i]
                            (* (-> i (js/Math.random) (* 2.0) (- 1.0))
                               (Math/pow (- 1 (/ i length)) decay)))]
    (convolver logarithmic-decay)))<commit_msg>A synth is now a reader fn that returns a map with both input and output, which allows better composition of non-singleton synths.<commit_after>(ns klangmeister.sound.synthesis)

; A synth is a function that returns a section.
(defn section
  ([input output] {:input input :output output})
  ([singleton] (section singleton singleton)))

; Plumbing

(defn run-with
  "Convert a synth (actually a reader fn) into a concrete section by supplying context and timing."
  [synth context at duration]
  (synth context at duration))

(defn destination [context at duration]
  (section (.-destination context)))

(defn plug [param input context at duration]
  "Plug an input into an audio parameter, accepting both numbers and synths."
  (if (number? input)
    (.setValueAtTime param input at)
    (-> input (run-with context at duration) :output (.connect param))))

(defn gain [level]
  (fn [context at duration]
    (section
      (doto (.createGain context)
        (-> .-gain (plug level context at duration))))))


; Envelopes

(defn envelope
  "Build an envelope out of [segment-duration final-level] coordinates."
  [& corners]
  (fn [context at duration]
    (let [audio-node (.createGain context)]
      (-> audio-node .-gain (.setValueAtTime 0 at))
      (reduce
        (fn [x [dx y]]
          (-> audio-node .-gain (.linearRampToValueAtTime y (+ x dx)))
          (+ dx x))
        at
        corners)
      (section audio-node))))

(defn adshr [attack decay sustain hold release]
  (envelope [attack 1.0] [decay sustain] [hold sustain] [release 0]))

(defn adsr [attack decay sustain release]
  (fn [context at duration]
    (let [remainder (- duration attack decay sustain)
          hold (max 0.0 remainder)
          node (adshr attack decay sustain hold release)]
      (-> node (run-with context at duration)))))

(defn percussive [attack decay]
  (envelope [attack 1.0] [decay 0.0]))


; Combinators

(defn connect
  "Use the output of one synth as the input to another."
  [upstream-synth downstream-synth]
  (fn [context at duration]
    (let [{downstream-input :input downstream-output :output} (-> downstream-synth (run-with context at duration))
          {upstream-input :input upstream-output :output} (-> upstream-synth (run-with context at duration))]
      (-> upstream-output (.connect downstream-input))
      (section upstream-input downstream-output))))

(defn connect->
  "Connect synths in series."
  [& nodes]
  (reduce connect nodes))

(defn add
  "Add together synths by connecting them all to the same gain."
  [& synths]
  (fn [context at duration]
    (let [{downstream-input :input downstream-output :output} (-> (gain 1.0) (run-with context at duration))]
      (doseq [synth synths]
        (let [{:keys [input output]} (-> synth (run-with context at duration))]
          (-> output (.connect downstream-input))))
      (section downstream-input downstream-output))))


; Noise

(defn noise
  "Make noise according to the supplied strategy for creating bits."
  [generate-bit!]
  (fn [context at duration]
    (let [sample-rate 44100
          frame-count (* sample-rate (+ duration 1.0)) ; Give a bit of extra for the release.
          buffer (.createBuffer context 1 frame-count sample-rate)
          data (.getChannelData buffer 0)]
      (doseq [i (range sample-rate)]
        (aset data i (generate-bit!)))
      (section
        (doto (.createBufferSource context)
          (-> .-buffer (set! buffer))
          (.start at))))))

(def white-noise
  (let [white #(-> (js/Math.random) (* 2.0) (- 1.0))]
    (noise white)))

(defn constant
  "Make a constant value by creating noise with a fixed value."
  [x]
  (noise (constantly x)))

; Oscillators

(defn oscillator
  [type freq]
  (fn [context at duration]
    (section
      (doto (.createOscillator context)
        (-> .-frequency .-value (set! 0))
        (-> .-frequency (plug freq context at duration))
        (-> .-type (set! type))
        (.start at)
        (.stop (+ at duration 1.0)))))) ; Give a bit extra for the release

(def sine (partial oscillator "sine"))
(def sawtooth (partial oscillator "sawtooth"))
(def square (partial oscillator "square"))
(def triangle (partial oscillator "triangle"))


; Filters

(defn biquad-filter
  ([type freq q]
   (fn [context at duration]
     (section
       (doto (-> (biquad-filter type freq) (run-with context at duration) :output)
         (-> .-Q (plug q context at duration))))))
  ([type freq]
   (fn [context at duration]
     (section
       (doto (.createBiquadFilter context)
         (-> .-frequency .-value (set! 0))
         (-> .-frequency (plug freq context at duration))
         (-> .-type (set! type)))))))

(def low-pass (partial biquad-filter "lowpass"))
(def high-pass (partial biquad-filter "highpass"))


; Effects

(defn stereo-panner [pan]
  (fn [context at duration]
    (section
      (doto (.createStereoPanner context)
        (-> .-pan (plug pan context at duration))))))

(defn delay-line
  [time]
  (fn [context at duration]
    (let [maximum 5]
      (section
        (doto (.createDelay context maximum)
          (-> .-delayTime (plug time context at duration)))))))

; FIXME: Unify with noise.
(defn convolver
  [generate-bit!]
  (fn [context at duration]
    (let [sample-rate 44100
          frame-count (* sample-rate (+ duration 1.0)) ; Give a bit of extra for the release.
          buffer (.createBuffer context 1 frame-count sample-rate)
          data (.getChannelData buffer 0)]
      (doseq [i (range sample-rate)]
        (aset data i (generate-bit! i)))
      (section
        (doto (.createConvolver context)
          (-> .-buffer (set! buffer)))))))

(def reverb
  (let [duration 5
        decay 3
        sample-rate 44100
        length (* sample-rate (+ duration 1.0))
        logarithmic-decay (fn [i]
                            (* (-> i (js/Math.random) (* 2.0) (- 1.0))
                               (Math/pow (- 1 (/ i length)) decay)))]
    (convolver logarithmic-decay)))
{'fraction_non_alphanumeric': 0.13585686027692861, 'fraction_numerical': 0.00989030749865132, 'mean_word_length': 2.7089029676558853, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 53, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '509541', 'n_tokens_mistral': 3619, 'n_tokens_neox': 3395, 'n_words': 1233}