Skip to content
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
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Test Suite

on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main, dev ]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up Clojure CLI
uses: DeLaGuardo/setup-clojure@10.1
with:
cli: latest

- name: Cache Maven dependencies
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('deps.edn') }}
restore-keys: |
${{ runner.os }}-m2-

- name: Cache gitlibs
uses: actions/cache@v3
with:
path: ~/.gitlibs
key: ${{ runner.os }}-gitlibs-${{ hashFiles('deps.edn') }}
restore-keys: |
${{ runner.os }}-gitlibs-

- name: Run tests
run: clojure -M:test
7 changes: 6 additions & 1 deletion deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@
org.clojure/test.check {:mvn/version "0.10.0"}
com.taoensso/timbre {:mvn/version "4.10.0"}
overtone/overtone {:mvn/version "0.10.6"} #_{:local/root "../overtone" :deps/manifest :deps}
org.clojure/tools.namespace {:mvn/version "1.3.0"}}}
org.clojure/tools.namespace {:mvn/version "1.3.0"}}
:aliases {:test {:extra-paths ["test"]
:extra-deps {io.github.cognitect-labs/test-runner
{:git/tag "v0.5.1" :git/sha "dfb30dd"}}
:main-opts ["-m" "cognitect.test-runner"]
:exec-fn cognitect.test-runner.api/test}}}
15 changes: 8 additions & 7 deletions src/time_time/dynacan/players/gen_poly.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@
`at-index` (alias `at-i`,function that get a value in a collection based on index, it wraps with `mod`)
can take an `offset` as the first argument and the collection as the second, and `offset` can be a function or a number"
[& forms]
`(fn [~'{{:keys [index dur dur-ms dur-s] :as data} :data}]
`(fn [~'{{:keys [index cycle cycle-delta dur dur-ms dur-s] :as data} :data}]
(let [~'i ~'index
~'delta ~'cycle-delta
~'at-index (partial at-index* ~'index)
~'at-i ~'at-index]
~@forms)))
Expand Down Expand Up @@ -125,9 +126,10 @@
:as config}]
(let [existing-voice? (and (@refrains id) (-> @refrains id deref :playing?))
refrains* (cond
existing-voice? (update-refrain id #(assoc %
:update config
:refrain/config config))
existing-voice? (let [config* (merge config (s/get-cycle-data durs config))]
(update-refrain id #(assoc %
:update config*
:refrain/config config*)))

(and (@refrains ref) (-> @refrains ref deref :playing?))
(let [voice (add-to! (-> @refrains ref deref) distance on-event config)]
Expand All @@ -150,10 +152,9 @@
;; A function
(ref-rain
:id :hola
:durs (fn [{:keys [index]}]
1)
:durs [1 2]
:on-event (on-event
(println "holas" (at-index sec))
(println "holas" cycle)
;; throw at some point of the execution

#_(throw (ex-info "ups" {}))))
Expand Down
214 changes: 214 additions & 0 deletions src/time_time/dynacan/players/refrain/v2.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
(ns time-time.dynacan.players.refrain.v2
"Uses sequencing-4"
(:require
[time-time.dynacan.core :refer [find-relative-voice-first-event]]
[time-time.sequencing-4 :as s4]
[time-time.standard :refer [wrap-at]]))

;; API
(declare before-update update-refrain maybe-run-on-stop-fn active-refrains add-to!)

(defonce refrains (atom {}))

(defn reset [] (reset! refrains {}))

(defn stop
([]
(doseq [id (keys @refrains)]
(update-refrain id :playing? false)
(maybe-run-on-stop-fn (get @refrains id)))
(reset))
([id]
(cond (and (@refrains id) (:playing? (deref (@refrains id))))
(do (update-refrain id :playing? false)
(maybe-run-on-stop-fn (get @refrains id)))

(@refrains id)
(println "refrain already stopped")

:else (println "Could not find refrain with id: " id))))

(defn ref-rain
[& {:keys [id durs on-event loop? ref distance]
:or {loop? true
distance 1}
:as config}]
(let [existing-voice-config (@refrains id)
existing-voice? (and existing-voice-config (:playing? @existing-voice-config))
refrains* (cond
existing-voice? (let [config* (merge config (s4/get-cycle-data (merge @existing-voice-config config)))]
(update-refrain id #(assoc %
:update config*
:refrain/config config*)))

(and (@refrains ref) (-> @refrains ref deref :playing?))
(let [voice (add-to! (-> @refrains ref deref) distance on-event config)]
(swap! voice assoc :refrain/config config)
(swap! refrains assoc id voice))

:else
(let [voice (s4/play! {:extra-data {:id id :ref ref}
:durs durs
:on-event on-event
:loop? loop?
:tempo (config :tempo 60)
:ratio (config :ratio 1)
:on-startup-error (fn [] (stop id))})]
(swap! voice assoc :refrain/config config)
(swap! refrains assoc id voice)))]
(active-refrains refrains*)))

(comment
(require '[overtone.music.time :refer [now]])
(@refrains :hola)
(ref-rain :id :hola
:durs [1 2 3]
:on-event (on-event
(println "hola")
(when (cyi? 3) (println "First" dur))))
(ref-rain :id :adios
:ref :hola
;; :reset-cycle? true
:durs [2 3]
:on-event (on-event
(println cycle cycle-delta-index cycle-delta)))

(stop))

(defn at-index*
"`offset` can be a function or a number"
([index coll] (wrap-at index coll))
([index offset coll] (if (fn? offset)
(wrap-at (offset index) coll)
(wrap-at (+ offset index) coll))))

(defn cycle-0-index?
([index cycle-0-index]
(= index cycle-0-index))
;; NOTE in most cases this will work, when the `cycle-len` is derived from the `durs` sum; however in cases where this is not, the return value will not be correct.
([index cycle-0-index offset durs cycle-len]
(= index (+ (mod offset (if-not (fn? durs) (count durs) cycle-len))
cycle-0-index))))

(defmacro cyi?
"To be used inside an on-event. If called without an argument, it will return true when the cycle is begining.

With an `offset` argument it will attempt to calculate if the current index is an `offset` away from the start of the cycle.
Using an `offset` is prone to returning false positives if the `cycle-len` has not been derived from the sum of `durs`."
([] `(~cycle-0-index? ~'index ~'cycle-0-index))
;; NOTE in most cases this will work, when the `cycle-len` is derived from the `durs` sum; however in cases where this is not, the return value will not be correct.
([offset] `(~cycle-0-index? ~'index ~'cycle-0-index ~offset ~'durs ~'cycle-len)))

(comment
((on-event (cyi?)) {:event {:index 0 :cycle-0-index 0}})
((on-event (cyi?)) {:event {:index 1 :cycle-0-index 0}})
((on-event (cyi?)) {:event {:index 1 :cycle-0-index 1}})
((on-event (cyi?)) {:event {:index 1 :cycle-0-index 0}
:voice {:durs [1 1 1] :cycle-len 3}}))

(defmacro on-event
"Provides
`index` (alias `i`),
`dur` (original duration),
`dur-s` (duration in seconds),
`dur-ms` (duration in milliseconds) and
`at-index` (alias `at-i`,function that get a value in a collection based on index, it wraps with `mod`)
can take an `offset` as the first argument and the collection as the second, and `offset` can be a function or a number"
[& forms]
`(fn [~'{{:keys [index
dur
dur-ms
dur-s
cycle
cycle-delta
cycle-0-index
cycle-delta-index
new-cycle?] :as event} :event
{:keys [id cycle-len durs] :as voice} :voice}]
(let [~'data ~'event
~'i ~'index
~'cy ~'cycle
~'cy0i ~'cycle-0-index
;; ~'cy0? (= ~'index ~'cycle-0-index)
~'cyd ~'cycle-delta
~'cydi ~'cycle-delta-index
~'at-index (partial at-index* ~'index)
~'at-i ~'at-index]
~@forms)))
(comment
(macroexpand-1 '(on-event (at-index [1 2 3])))
((on-event (at-index #(- 1 %) [1 2 3])) {:data {:index 1}})
((on-event (at-i [1 2 3])) {:data {:index 1}})
(on-event (at-index [1 2 3])
(at-index [1 2 3])))

(defmacro when-mod
"To be used within `on-event` as it assumes the existence of `index` in the context of the function"
[modulo index-set f-call]
`(when (~index-set (mod ~'index ~modulo))
~f-call))

(defn active-refrains [refrains]
(->> refrains
(filter (comp :playing? deref second))
(map first)))

(defn add-to! [ref-voice events-from-cp on-event
{:keys [id durs ratio loop? cp]
:or {durs (ref-voice :durs)
ratio (ref-voice :ratio)
loop? (ref-voice :loop?)}
:as config}]
;; TODO verify if ref-voice is playing?
(let [start-time (+ (ref-voice :elapsed-ms) (ref-voice :started-at))
tempo (ref-voice :tempo)
{:keys [index elapsed cp cp-elapsed interval-from-cp events-from-cp]}
(find-relative-voice-first-event events-from-cp ;; TODO should probably return the start time of the voice
ref-voice
{:durs durs :cp cp :ratio ratio :loop? loop?})]
#_(log/debug "adding voice... cp-at ref-voice index"
(ref-voice :index)
(+ events-from-cp (ref-voice :index)))
(s4/play! {:durs durs
:on-event on-event
:ratio ratio
:start-index index
:elapsed elapsed
:tempo tempo
:start-time start-time
:loop? loop?
:before-update before-update
:extra-data {:id id
:ref (:id ref-voice)
:cp cp
:cp-at cp-elapsed
:interval-from-cp interval-from-cp
:events-from-cp events-from-cp}})))

(defn before-update
[{:as data {dur :dur} :current-event}]
(-> data
(update :events-from-cp dec)
(update :interval-from-cp - dur)))

;; TODO verificar cómo funciona el cp en `add-to`... cómo se estan leyendo las duraciones... tal vez rotación
;; compilar en diversos momentos
;; en diversos loops

;; hacer pruebas (combinaciones de loops, corrección del cp)
;; pensar en tiempo de reposo (remainder) entre voces (que sea opcional)
;; pensar otros modos de control de las densidades* (quizá otra función aparte)
;; comenzar a pensar en la sintaxis

(defn backup-on-event [config]
(assoc config :prev-on-event (:on-event config)))

(defn update-refrain
([id key val] (update-refrain id #(assoc % key val)))
([id update-fn]
(swap! refrains update id #(do (swap! % (comp update-fn backup-on-event)) %))))

(defn- maybe-run-on-stop-fn
[refrain]
(let [on-stop (-> @refrain :refrain/config :on-stop)]
(when on-stop (on-stop @refrain))))
48 changes: 41 additions & 7 deletions src/time_time/sequencing_3.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,43 @@
#?(:clj [taoensso.timbre :as timbre]
:cljs [taoensso.timbre :as timbre :include-macros true])))

(declare schedule! get-current-dur-data)
(declare schedule! get-current-dur-data cycle-delta)

(defn get-cycle-data
[durs
{:keys [elapsed-dur durs-len] :as _config
:or {elapsed-dur 0}}]
(let [cycle-len (cond
(and (fn? durs) (not durs-len)) (do (timbre/warn "`durs` is a function but no `durs-len` has been provided, defaulting to 1")
1)
durs-len durs-len
:else (apply + durs))]
{:cycle-len cycle-len
:cycle (quot elapsed-dur cycle-len)
:cycle-delta (cycle-delta elapsed-dur cycle-len)}))

(defn play!
"Gets a config and returns a `voice-atom` that will start playing,
if `playing?` is true (default)"
[durs on-event &
{:keys [ratio tempo loop? start-index start-time elapsed playing?
before-update on-schedule extra-data on-startup-error]
{:as config
:keys [ratio tempo loop? start-index start-time elapsed elapsed-dur playing?
before-update on-schedule extra-data on-startup-error durs-len]
:or {ratio 1
tempo 60
loop? false
start-index 0
start-time (now)
elapsed 0
elapsed-dur 0 ;; TODO investigate if this is compatible with `elapsed`, if so, substitute
playing? true
before-update identity ;; receives the voice data before it is used to `reset!` the `voice-atom`
on-schedule (fn [voice event-schedule] event-schedule)
extra-data {}}}]
(let [voice (atom (merge extra-data
(let [cycle-data (get-cycle-data durs config)
voice (atom (merge extra-data
{:durs durs
:elapsed-dur elapsed-dur
:on-event on-event
:ratio ratio
:tempo tempo
Expand All @@ -45,7 +63,8 @@
:playing? playing?
:before-update before-update
:on-schedule on-schedule
:on-startup-error on-startup-error}))]
:on-startup-error on-startup-error}
cycle-data))]
(schedule! voice)
voice))

Expand All @@ -71,12 +90,23 @@
event-dur (dur->ms dur tempo)]
{:dur dur :event-dur event-dur}))

(defn cycle-delta [elapsed-dur cycle-len]
(/ (mod elapsed-dur cycle-len)
cycle-len))

(defn calculate-next-voice-state
[{:keys [index elapsed-ms] :as voice}]
[{:keys [index elapsed-ms cycle-len cycle elapsed-dur] :as voice}]
(let [{:keys [dur event-dur]} (get-current-dur-data voice)
elapsed-dur* (+ elapsed-dur dur)
updated-state {:index (inc index)
:cycle (quot elapsed-dur* cycle-len)
:cycle-delta (cycle-delta elapsed-dur* cycle-len)
:elapsed-dur elapsed-dur*
:elapsed-ms (+ elapsed-ms event-dur)
:current-event {:dur-ms event-dur :dur dur}}]
:current-event {:dur-ms event-dur
:dur dur
:index index
:cycle cycle}}]
(merge voice updated-state)))

(defn play-event?
Expand All @@ -101,6 +131,10 @@
;; TODO calculate-next-voice-state should only return the fields below
(select-keys voice-update
[:index
:cycle
:cycle-delta
:cycle-len
:elapsed-dur
:elapsed-ms
:current-event
:prev-on-event
Expand Down
Loading