Clojure and Blue/Csound Example

I have started to work on a new composition and wanted to move from using Python to Clojure as the scripting language for my music.  I was experimenting yesterday and was pleased to be able to write a score generation function that was fairly flexible, allowing for easily using hardcoded values as well as any sequence for filling in p-fields of generated Csound Score.

From my work session I came up with some fairly condensed code I was happy with:

(require '[clojure.string :refer  [join]])

(defn pch-add  [bpch interval]
      (let  [scale-degrees 12
                       new-val  (+  (* scale-degrees  (first bpch))
                                                      (second bpch) interval)]
                [(quot new-val scale-degrees)
                          (rem new-val scale-degrees)]))

(defn pch->sco  [[a b]] 
      (format "%d.%02d" a b ))

(defn pch-interval-seq  [pch & intervals] 
    (reduce  (fn  [a b]  (conj a  (pch-add  (last a) b)))  [pch] intervals))

(defn pch-interval-sco  [pch & intervals] 
    (map pch->sco  (apply pch-interval-seq pch intervals)))

(defn score-arg  [a]
      (if  (number? a)
                (repeat a)
                a))

(defn gen-score  [& fields]
      (let  [pfields  (map score-arg fields)]
                (join "\n"
                                  (apply map (fn [& args] (join " " args)) (repeat "i") pfields))))

;; EXAMPLE CODE

(def score 
      (gen-score 1 0 1 
                         (pch-interval-sco  [6 0] 12 8 6 2 6)
                         (pch-interval-sco  [6 0] 12 8 6 2 6)
                         (range -10 -100 -1) 0 1))

(print score)

The output from the print statement is:

i1    0.0    1    6.00    6.00    -10    0    1
i1    0.0    1    7.00    7.00    -11    0    1
i1    0.0    1    7.08    7.08    -12    0    1
i1    0.0    1    8.02    8.02    -13    0    1
i1    0.0    1    8.04    8.04    -14    0    1
i1    0.0    1    8.10    8.10    -15    0    1

The key part is the gen-score function.  It can take in either a number or a sequence as arguments.  If a number is given, it will be repeated for each note.  For a sequence, they can be infinite or finite. The only important part of using gen-score is that the at least one of the arguments given is a finite list.

This is fairly similar to SuperCollider’s Pattern system, though it uses standard abstractions found in Clojure. To me, it is a bit simpler to think in sequences to generate events than to think about the Pattern library’s object-oriented abstractions, but that is just my own preference.  Also, the Pattern system in SC is designed for real-time scheduling and also has an option for a delta-time generator.  I think the delta-time aspect could be added fairly easily using an optional keyword-argument to gen-score.

As for making this work in realtime, gen-score would have to be rewritten to not format the strings but instead return the list of p-field values. A priority-queue could then be used to check against the time values of the notes and pause and wait to generate new notes when the scheduling time demands it.

Ultimately, I was very pleased with the short amount of time required to write this code, as well as the succinctness of it.  One thing that has been on my mind is whether to use a CMask/PMask kind of approach to the p-field sequence generators.  In those systems, what would be comparable to the sequence generators–I think they’re just called Fields in those systems–actually take in a time argument.  That gives the generators some context about what to generate next and allows the ability to mask values over time.  I am fairly certain I will need to update or create an alternative to the gen-score function to allow generator functions.  I’ll have to consider whether to use a Clojure protocol, but I may be able to get away with just testing if the argument to gen-score is a function, sequence, or number and act appropriately.  (Something to look at next work session. 🙂 )

Published
Categorized as Blue, csound