From 1b9c62b6484863e48373a80d56d11eb71ade3634 Mon Sep 17 00:00:00 2001 From: James Hamlin Date: Tue, 7 May 2024 02:44:39 +0200 Subject: [PATCH 1/7] Stdlib tweaks, more slice bound check lifting Signed-off-by: James Hamlin --- pkg/effects/amplitude.go | 1 + pkg/effects/lores.go | 4 + pkg/effects/rhpf.go | 4 + pkg/effects/rlpf.go | 4 + pkg/stdlib/mrat/core.glj | 267 ++++++++++++++++++++------------------- pkg/ugen/exp.go | 1 + 6 files changed, 151 insertions(+), 130 deletions(-) diff --git a/pkg/effects/amplitude.go b/pkg/effects/amplitude.go index 578e18d..9f11d7c 100644 --- a/pkg/effects/amplitude.go +++ b/pkg/effects/amplitude.go @@ -36,6 +36,7 @@ func NewAmplitude(attackTime, releaseTime float64, opts ...ugen.Option) ugen.UGe // https://doc.sccode.org/Classes/Amplitude.html in := cfg.InputSamples["in"] + _ = in[len(out)-1] for i := range out { val := math.Abs(in[i]) if val < prevIn { diff --git a/pkg/effects/lores.go b/pkg/effects/lores.go index 45f69d6..854fc3e 100644 --- a/pkg/effects/lores.go +++ b/pkg/effects/lores.go @@ -18,6 +18,10 @@ func NewLowpassFilter() ugen.UGen { cuts := cfg.InputSamples["cutoff"] ress := cfg.InputSamples["resonance"] + _ = in[len(out)-1] + _ = cuts[len(out)-1] + _ = ress[len(out)-1] + for i := range out { cut := cuts[i] res := ress[i] diff --git a/pkg/effects/rhpf.go b/pkg/effects/rhpf.go index 416f296..a03985d 100644 --- a/pkg/effects/rhpf.go +++ b/pkg/effects/rhpf.go @@ -33,6 +33,10 @@ func NewRHPF() ugen.UGen { freq := cfg.InputSamples["freq"] reson := cfg.InputSamples["reson"] + _ = in[len(out)-1] + _ = freq[len(out)-1] + _ = reson[len(out)-1] + for i := range out { if !coefficientsComputed || freq[i] != prevFreq || reson[i] != prevReson { computeCoefficients(freq[i], reson[i]) diff --git a/pkg/effects/rlpf.go b/pkg/effects/rlpf.go index 2406ea4..ec4887d 100644 --- a/pkg/effects/rlpf.go +++ b/pkg/effects/rlpf.go @@ -33,6 +33,10 @@ func NewRLPF() ugen.UGen { freq := cfg.InputSamples["freq"] reson := cfg.InputSamples["reson"] + _ = in[len(out)-1] + _ = freq[len(out)-1] + _ = reson[len(out)-1] + for i := range out { if !coefficientsComputed || freq[i] != prevFreq || reson[i] != prevReson { computeCoefficients(freq[i], reson[i]) diff --git a/pkg/stdlib/mrat/core.glj b/pkg/stdlib/mrat/core.glj index 9fbd32a..8194d5d 100644 --- a/pkg/stdlib/mrat/core.glj +++ b/pkg/stdlib/mrat/core.glj @@ -206,136 +206,6 @@ [x] (add-node! :const NewConstant :args [(double x)])) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Utilities - -(defn pow - "Returns b^p. If b or p are nodes, creates a new node that computes b^p. Else, - returns the result of b^p directly. pow extends exponentiation to - allow for a negative base with a non-integral exponent, returning - -((-b)^p) when b is negative." - [b p] - (if (some (partial is-node?) [b p]) - (add-node! :pow NewPow :in-edges {"base" b "exp" p}) - (if (neg? b) - (- (math.Pow (- b) p)) - (math.Pow b p)))) - -(defn sine - "Returns sin(theta). If theta is a node, creates a new node that - computes sin(theta). Else, returns the result of sin(theta) - directly." - [theta] - (if (is-node? theta) - (add-node! :sine github.com$jfhamlin$muscrat$pkg$ugen.NewSine :in-edges {:in theta}) - (math.Sin theta))) - -(defn min - "Returns the minimum of xs. If any element of xs is a node, creates a new - node that computes the minimum of xs. Else, returns the minimum of xs" - [& xs] - (if (some is-node? xs) - (add-node! :min NewMin :in-edges (zipmap (map str (range (count xs))) xs)) - (apply glojure.core/min xs))) - -(defn max - "Returns the maximum of xs. If any element of xs is a node, creates a new - node that computes the maximum of xs. Else, returns the maximum of xs" - [& xs] - (if (some is-node? xs) - (add-node! :max NewMax :in-edges (zipmap (map str (range (count xs))) xs)) - (apply glojure.core/max xs))) - -(defn abs - "Returns the absolute value of x. If x is a node, creates a new node - that computes the absolute value of x. Else, returns the absolute - value of x directly." - [x] - (if (is-node? x) - (add-node! :abs NewAbs :in-edges {:in x}) - (math.Abs x))) - -(defn exp - "Returns e^x. If x is a node, creates a new node that computes e^x. - Else, returns e^x directly." - [x] - (if (is-node? x) - (add-node! :exp NewExp :in-edges {:in x}) - (math.Exp x))) - -(defn tanh - "Returns the hyperbolic tangent of x. If x is a node, creates a new - node that computes the hyperbolic tangent of x. Else, returns the - hyperbolic tangent of x directly." - [x] - (if (is-node? x) - (add-node! :tanh NewTanh :in-edges {:in x}) - (math.Tanh x))) - -(defn linexp - "Maps x from the linear range [srclo, srchi] to the exponential - range [dstlo, dsthi]" - [x srclo srchi dstlo dsthi] - (add-node! :linexp NewLinExp :in-edges {:in x - :srclo srclo - :srchi srchi - :dstlo dstlo - :dsthi dsthi})) - -(defn copy-sign - "Returns x with the sign of s. If x or s are nodes, creates a new node - that computes x with the sign of s. Else, returns x with the sign of - s directly." - [x s] - (if (or (is-node? x) (is-node? s)) - (add-node! :copy-sign NewCopySign :in-edges {"in" x "sign" s}) - (math.Copysign x s))) - -(defn- freq-ratio - [x kind] - (add-node! :freq-ratio NewFreqRatio :args [kind] :in-edges {"in" x})) - -(defn dbamp - "Return the amplitude ratio corresponding to the given decibel value." - [db] - (if (is-node? db) - (freq-ratio db "decibels") - (pow 10 (/ db 20.0)))) - -(defn cents - "Return the frequency ratio corresponding to the given number of - cents." - [x] - (if (is-node? x) - (freq-ratio x "cents") - (pow 2 (/ x 1200.0)))) - -(defn semitones - "Return the frequency ratio corresponding to the given number of - semitones." - [x] - (if (is-node? x) - (freq-ratio x "semitones") - (pow 2 (/ x 12.0)))) - -(defn octaves - "Return the frequency ratio corresponding to the given number of - octaves." - [x] - (if (is-node? x) - (freq-ratio x "octaves") - (pow 2 x))) - -(defn mtof - "Return the frequency corresponding to the given MIDI note number." - [note] - (if (is-node? note) - (add-node! :midifreq NewMIDIFreq :in-edges {"in" note}) - (* 440.0 (pow 2 (/ (- note 69.0) 12.0))))) - -(docgroup "Utilities") -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Helper for custom sample generators @@ -592,6 +462,136 @@ (docgroup "Macros") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Utilities + +(defn pow + "Returns b^p. If b or p are nodes, creates a new node that computes b^p. Else, + returns the result of b^p directly. pow extends exponentiation to + allow for a negative base with a non-integral exponent, returning + -((-b)^p) when b is negative." + [b p] + (if (some (partial is-node?) [b p]) + (add-node! :pow NewPow :in-edges {"base" b "exp" p}) + (if (neg? b) + (- (math.Pow (- b) p)) + (math.Pow b p)))) + +(defn sine + "Returns sin(theta). If theta is a node, creates a new node that + computes sin(theta). Else, returns the result of sin(theta) + directly." + [theta] + (if (is-node? theta) + (add-node! :sine github.com$jfhamlin$muscrat$pkg$ugen.NewSine :in-edges {:in theta}) + (math.Sin theta))) + +(defn min + "Returns the minimum of xs. If any element of xs is a node, creates a new + node that computes the minimum of xs. Else, returns the minimum of xs" + [& xs] + (if (some is-node? xs) + (add-node! :min NewMin :in-edges (zipmap (map str (range (count xs))) xs)) + (apply glojure.core/min xs))) + +(defn max + "Returns the maximum of xs. If any element of xs is a node, creates a new + node that computes the maximum of xs. Else, returns the maximum of xs" + [& xs] + (if (some is-node? xs) + (add-node! :max NewMax :in-edges (zipmap (map str (range (count xs))) xs)) + (apply glojure.core/max xs))) + +(defn abs + "Returns the absolute value of x. If x is a node, creates a new node + that computes the absolute value of x. Else, returns the absolute + value of x directly." + [x] + (if (is-node? x) + (add-node! :abs NewAbs :in-edges {:in x}) + (math.Abs x))) + +(defn exp + "Returns e^x. If x is a node, creates a new node that computes e^x. + Else, returns e^x directly." + [x] + (if (is-node? x) + (add-node! :exp NewExp :in-edges {:in x}) + (math.Exp x))) + +(defn tanh + "Returns the hyperbolic tangent of x. If x is a node, creates a new + node that computes the hyperbolic tangent of x. Else, returns the + hyperbolic tangent of x directly." + [x] + (if (is-node? x) + (add-node! :tanh NewTanh :in-edges {:in x}) + (math.Tanh x))) + +(defn linexp + "Maps x from the linear range [srclo, srchi] to the exponential + range [dstlo, dsthi]" + [x srclo srchi dstlo dsthi] + (add-node! :linexp NewLinExp :in-edges {:in x + :srclo srclo + :srchi srchi + :dstlo dstlo + :dsthi dsthi})) + +(defn copy-sign + "Returns x with the sign of s. If x or s are nodes, creates a new node + that computes x with the sign of s. Else, returns x with the sign of + s directly." + [x s] + (if (or (is-node? x) (is-node? s)) + (add-node! :copy-sign NewCopySign :in-edges {"in" x "sign" s}) + (math.Copysign x s))) + +(defn- freq-ratio + [x kind] + (add-node! :freq-ratio NewFreqRatio :args [kind] :in-edges {"in" x})) + +(defn dbamp + "Return the amplitude ratio corresponding to the given decibel value." + [db] + (if (is-node? db) + (freq-ratio db "decibels") + (pow 10 (/ db 20.0)))) + +(defn cents + "Return the frequency ratio corresponding to the given number of + cents." + [x] + (if (is-node? x) + (freq-ratio x "cents") + (pow 2 (/ x 1200.0)))) + +(defn semitones + "Return the frequency ratio corresponding to the given number of + semitones." + [x] + (if (is-node? x) + (freq-ratio x "semitones") + (pow 2 (/ x 12.0)))) + +(defn octaves + "Return the frequency ratio corresponding to the given number of + octaves." + [x] + (if (is-node? x) + (freq-ratio x "octaves") + (pow 2 x))) + +(defugen mtof + "Return the frequency corresponding to the given MIDI note number." + [note 69] + (if (is-node? note) + (add-node! :midifreq NewMIDIFreq :in-edges {"in" note}) + (* 440.0 (pow 2 (/ (- note 69.0) 12.0))))) + +(docgroup "Utilities") +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Oscillators @@ -1118,6 +1118,13 @@ [] (add-node! :in NewInputDevice)) +(defugen knob + [^:noexpand name nil + default 0 + min-value -1 + max-value 1] + (add-node! :knob NewKnob :args [name default min-value max-value])) + (defn qwerty-in [name & {:keys [voices]}] (let [num-voices (or voices 1) diff --git a/pkg/ugen/exp.go b/pkg/ugen/exp.go index fbd8da3..8d1052b 100644 --- a/pkg/ugen/exp.go +++ b/pkg/ugen/exp.go @@ -8,6 +8,7 @@ import ( func NewExp() UGen { return UGenFunc(func(ctx context.Context, cfg SampleConfig, out []float64) { in := cfg.InputSamples["in"] + _ = in[len(out)-1] for i := range out { out[i] = math.Exp(in[i]) } From f57fdf551f1232f354327e797c1d703ebdeac86e Mon Sep 17 00:00:00 2001 From: James Hamlin Date: Tue, 7 May 2024 14:23:24 +0200 Subject: [PATCH 2/7] Add dynamic knobs Signed-off-by: James Hamlin --- app.go | 24 +++++ frontend/src/App.jsx | 2 + frontend/src/components/Knobs/index.jsx | 74 +++++++++++++++ frontend/wailsjs/go/main/App.d.ts | 3 + frontend/wailsjs/go/main/App.js | 4 + frontend/wailsjs/go/models.ts | 25 +++++ pkg/gen/gljimports/gljimports.go | 4 + pkg/stdlib/mrat/core.glj | 1 + pkg/ugen/knob.go | 121 ++++++++++++++++++++++++ 9 files changed, 258 insertions(+) create mode 100644 frontend/src/components/Knobs/index.jsx create mode 100644 pkg/ugen/knob.go diff --git a/app.go b/app.go index af6448d..b171c89 100644 --- a/app.go +++ b/app.go @@ -9,6 +9,8 @@ import ( "github.com/jfhamlin/muscrat/pkg/conf" "github.com/jfhamlin/muscrat/pkg/mrat" "github.com/jfhamlin/muscrat/pkg/pubsub" + "github.com/jfhamlin/muscrat/pkg/ugen" + "github.com/wailsapp/wails/v2/pkg/runtime" ) @@ -76,6 +78,24 @@ func (a *App) startup(ctx context.Context) { } }) + pubsub.Subscribe(ugen.KnobsChangedEvent, func(event string, data any) { + // send the new knobs to the UI + go func() { + runtime.EventsEmit(ctx, "knobs-changed", ugen.GetKnobs()) + }() + }) + + // forward knob value changes from the UI to the pubsub + runtime.EventsOn(ctx, "knob-value-change", func(data ...any) { + id := data[0].(float64) + value := data[1].(float64) + update := ugen.KnobUpdate{ + ID: uint64(id), + Value: value, + } + pubsub.Publish(ugen.KnobValueChangeEvent, update) + }) + pubsub.Subscribe("console.log", func(event string, data any) { go runtime.EventsEmit(ctx, "console.log", data) }) @@ -187,3 +207,7 @@ func (a *App) stopFile() { func (a *App) GetNSPublics() []mrat.Symbol { return mrat.GetNSPublics() } + +func (a *App) GetKnobs() []*ugen.Knob { + return ugen.GetKnobs() +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2bfdfba..e52b88e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -24,6 +24,7 @@ import Spectrogram from "./components/Spectrogram"; import Oscilloscope from "./components/Oscilloscope"; import HydraView from "./components/HydraView"; import Docs from "./components/Docs"; +import Knobs from "./components/Knobs"; const createAudioResources = () => { const audioContext = new AudioContext(); @@ -105,6 +106,7 @@ function App() {
+ ) diff --git a/frontend/src/components/Knobs/index.jsx b/frontend/src/components/Knobs/index.jsx new file mode 100644 index 0000000..91efcf2 --- /dev/null +++ b/frontend/src/components/Knobs/index.jsx @@ -0,0 +1,74 @@ +import { + useState, + useEffect, +} from 'react'; + +import { + GetKnobs, +} from "../../../wailsjs/go/main/App"; + +import { + EventsOn, + EventsEmit, +} from '../../../wailsjs/runtime'; + +const Knob = ({ knob }) => { + const [value, setValue] = useState(knob.def); + + return ( +
+

{knob.name}

+ {/* input */} + { + EventsEmit('knob-value-change', knob.id, new Number(e.target.value)); + setValue(e.target.value); + }} + /> + {/* value */} +
{value}
+
+ ) +} + +const sortKnobs = (knobs) => { + return knobs.sort((a, b) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + }); +} + +export default () => { + const [knobs, setKnobs] = useState([]); + + useEffect(() => { + GetKnobs().then((data) => { + sortKnobs(data); + setKnobs(data); + }); + EventsOn('knobs-changed', (data) => { + sortKnobs(data); + setKnobs(data); + console.log('knobs', data); + }); + }, []); + + return ( +
+

Knobs

+ {knobs.map((knob) => ( + + ))} +
+ ) +} diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index e6e8d42..3681d01 100755 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -1,8 +1,11 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT +import {ugen} from '../models'; import {mrat} from '../models'; import {main} from '../models'; +export function GetKnobs():Promise>; + export function GetNSPublics():Promise>; export function GetSampleRate():Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index 0679f1a..f048bab 100755 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -2,6 +2,10 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT +export function GetKnobs() { + return window['go']['main']['App']['GetKnobs'](); +} + export function GetNSPublics() { return window['go']['main']['App']['GetNSPublics'](); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 85030b1..08c9466 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -57,3 +57,28 @@ export namespace mrat { } +export namespace ugen { + + export class Knob { + name: string; + id: number; + min: number; + max: number; + def: number; + + static createFrom(source: any = {}) { + return new Knob(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.id = source["id"]; + this.min = source["min"]; + this.max = source["max"]; + this.def = source["def"]; + } + } + +} + diff --git a/pkg/gen/gljimports/gljimports.go b/pkg/gen/gljimports/gljimports.go index 84eaf31..0e4252d 100644 --- a/pkg/gen/gljimports/gljimports.go +++ b/pkg/gen/gljimports/gljimports.go @@ -164,10 +164,13 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/jfhamlin/muscrat/pkg/ugen.CollectIndexedInputs", github_com_jfhamlin_muscrat_pkg_ugen.CollectIndexedInputs) _register("github.com/jfhamlin/muscrat/pkg/ugen.CubInterp", github_com_jfhamlin_muscrat_pkg_ugen.CubInterp) _register("github.com/jfhamlin/muscrat/pkg/ugen.DefaultOptions", github_com_jfhamlin_muscrat_pkg_ugen.DefaultOptions) + _register("github.com/jfhamlin/muscrat/pkg/ugen.GetKnobs", github_com_jfhamlin_muscrat_pkg_ugen.GetKnobs) _register("github.com/jfhamlin/muscrat/pkg/ugen.Interp", reflect.TypeOf((*github_com_jfhamlin_muscrat_pkg_ugen.Interp)(nil)).Elem()) _register("github.com/jfhamlin/muscrat/pkg/ugen.InterpCubic", github_com_jfhamlin_muscrat_pkg_ugen.InterpCubic) _register("github.com/jfhamlin/muscrat/pkg/ugen.InterpLinear", github_com_jfhamlin_muscrat_pkg_ugen.InterpLinear) _register("github.com/jfhamlin/muscrat/pkg/ugen.InterpNone", github_com_jfhamlin_muscrat_pkg_ugen.InterpNone) + _register("github.com/jfhamlin/muscrat/pkg/ugen.Knob", reflect.TypeOf((*github_com_jfhamlin_muscrat_pkg_ugen.Knob)(nil)).Elem()) + _register("github.com/jfhamlin/muscrat/pkg/ugen.*Knob", reflect.TypeOf((*github_com_jfhamlin_muscrat_pkg_ugen.Knob)(nil))) _register("github.com/jfhamlin/muscrat/pkg/ugen.LinInterp", github_com_jfhamlin_muscrat_pkg_ugen.LinInterp) _register("github.com/jfhamlin/muscrat/pkg/ugen.NewAbs", github_com_jfhamlin_muscrat_pkg_ugen.NewAbs) _register("github.com/jfhamlin/muscrat/pkg/ugen.NewConstant", github_com_jfhamlin_muscrat_pkg_ugen.NewConstant) @@ -177,6 +180,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/jfhamlin/muscrat/pkg/ugen.NewFMAStatic", github_com_jfhamlin_muscrat_pkg_ugen.NewFMAStatic) _register("github.com/jfhamlin/muscrat/pkg/ugen.NewFreqRatio", github_com_jfhamlin_muscrat_pkg_ugen.NewFreqRatio) _register("github.com/jfhamlin/muscrat/pkg/ugen.NewImpulse", github_com_jfhamlin_muscrat_pkg_ugen.NewImpulse) + _register("github.com/jfhamlin/muscrat/pkg/ugen.NewKnob", github_com_jfhamlin_muscrat_pkg_ugen.NewKnob) _register("github.com/jfhamlin/muscrat/pkg/ugen.NewLatch", github_com_jfhamlin_muscrat_pkg_ugen.NewLatch) _register("github.com/jfhamlin/muscrat/pkg/ugen.NewLinExp", github_com_jfhamlin_muscrat_pkg_ugen.NewLinExp) _register("github.com/jfhamlin/muscrat/pkg/ugen.NewMIDIFreq", github_com_jfhamlin_muscrat_pkg_ugen.NewMIDIFreq) diff --git a/pkg/stdlib/mrat/core.glj b/pkg/stdlib/mrat/core.glj index 8194d5d..7782f61 100644 --- a/pkg/stdlib/mrat/core.glj +++ b/pkg/stdlib/mrat/core.glj @@ -19,6 +19,7 @@ NewMIDIFreq NewImpulse NewPulseDiv + NewKnob SimpleUGenFunc WithInterp WithDefaultDutyCycle diff --git a/pkg/ugen/knob.go b/pkg/ugen/knob.go new file mode 100644 index 0000000..bf53b39 --- /dev/null +++ b/pkg/ugen/knob.go @@ -0,0 +1,121 @@ +package ugen + +import ( + "context" + "math" + "sync" + "sync/atomic" + + "github.com/jfhamlin/muscrat/pkg/pubsub" +) + +type ( + // Knob is a ugen representing a control that can be turned to + // adjust a value. + Knob struct { + Name string `json:"name"` + ID uint64 `json:"id"` + Min float64 `json:"min"` + Max float64 `json:"max"` + Def float64 `json:"def"` + + valueBits atomic.Uint64 + + unsubscribe func() + } + + // KnobUpdate is a message sent to update a knob. + KnobUpdate struct { + ID uint64 `json:"id"` + Value float64 `json:"value"` + } +) + +const ( + // KnobValueChangeEvent is the event that is sent when a knob's + // value changes. + KnobValueChangeEvent = "knob-value-change" + + // KnobsChangedEvent is the event that is sent when the list of + // knobs changes. + KnobsChangedEvent = "knobs-changed" +) + +var ( + knobLock sync.Mutex + nextKnobID uint64 + knobs = map[uint64]*Knob{} +) + +func GetKnobs() []*Knob { + knobLock.Lock() + defer knobLock.Unlock() + + knobsList := make([]*Knob, 0, len(knobs)) + for _, knob := range knobs { + knobsList = append(knobsList, knob) + } + return knobsList +} + +// NewKnob returns a new Knob ugen. +func NewKnob(name string, def float64, min float64, max float64) *Knob { + knobLock.Lock() + defer knobLock.Unlock() + + k := &Knob{ + Name: name, + ID: nextKnobID, + Min: min, + Max: max, + Def: def, + } + k.valueBits.Store(math.Float64bits(def)) + + nextKnobID++ + + return k +} + +func (k *Knob) Start(ctx context.Context) error { + knobLock.Lock() + defer knobLock.Unlock() + + knobs[k.ID] = k + + k.unsubscribe = pubsub.Subscribe(KnobValueChangeEvent, func(event string, data any) { + update := data.(KnobUpdate) + if update.ID != k.ID { + return + } + + bits := math.Float64bits(update.Value) + k.valueBits.Store(bits) + }) + + pubsub.Publish(KnobsChangedEvent, nil) + + return nil +} + +func (k *Knob) Stop(ctx context.Context) error { + knobLock.Lock() + defer knobLock.Unlock() + + delete(knobs, k.ID) + + pubsub.Publish(KnobsChangedEvent, nil) + + if k.unsubscribe != nil { + k.unsubscribe() + } + return nil +} + +func (k *Knob) Gen(ctx context.Context, cfg SampleConfig, out []float64) { + for i := range out { + bits := k.valueBits.Load() + value := math.Float64frombits(bits) + out[i] = value + } +} From 5013608ae0b5f98e77cf2882e50dc2b42b8932c7 Mon Sep 17 00:00:00 2001 From: James Hamlin Date: Thu, 9 May 2024 15:39:44 +0200 Subject: [PATCH 3/7] Add knobs, fm synth Signed-off-by: James Hamlin --- frontend/src/components/Knobs/index.jsx | 8 ++- frontend/wailsjs/go/models.ts | 2 + pkg/conf/constants.go | 2 +- pkg/gen/gljimports/gljimports.go | 5 ++ pkg/graph/runner.go | 2 +- pkg/stdlib/mrat/core.glj | 90 +++++++++++++++++++++++-- pkg/ugen/knob.go | 4 +- pkg/ugen/log2.go | 16 +++++ 8 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 pkg/ugen/log2.go diff --git a/frontend/src/components/Knobs/index.jsx b/frontend/src/components/Knobs/index.jsx index 91efcf2..2dba37e 100644 --- a/frontend/src/components/Knobs/index.jsx +++ b/frontend/src/components/Knobs/index.jsx @@ -18,17 +18,20 @@ const Knob = ({ knob }) => { return (

{knob.name}

- {/* input */} + {/* input, focus on click */} { EventsEmit('knob-value-change', knob.id, new Number(e.target.value)); setValue(e.target.value); }} + onClick={(e) => { + e.target.focus(); + }} /> {/* value */}
{value}
@@ -59,7 +62,6 @@ export default () => { EventsOn('knobs-changed', (data) => { sortKnobs(data); setKnobs(data); - console.log('knobs', data); }); }, []); diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 08c9466..71b0e80 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -65,6 +65,7 @@ export namespace ugen { min: number; max: number; def: number; + step: number; static createFrom(source: any = {}) { return new Knob(source); @@ -77,6 +78,7 @@ export namespace ugen { this.min = source["min"]; this.max = source["max"]; this.def = source["def"]; + this.step = source["step"]; } } diff --git a/pkg/conf/constants.go b/pkg/conf/constants.go index 28376df..e780434 100644 --- a/pkg/conf/constants.go +++ b/pkg/conf/constants.go @@ -17,7 +17,7 @@ var ( // BufferSize is the size of the buffer used for processing one // block of samples. BufferSize = func() int { - val := getValueInt("MUSCRAT_BUFFER_SIZE", 512) + val := getValueInt("MUSCRAT_BUFFER_SIZE", 128) // if not a power of 2, round up to the next power of 2 if val&(val-1) != 0 { leadingZeros := bits.LeadingZeros(uint(val)) diff --git a/pkg/gen/gljimports/gljimports.go b/pkg/gen/gljimports/gljimports.go index 0e4252d..bb93742 100644 --- a/pkg/gen/gljimports/gljimports.go +++ b/pkg/gen/gljimports/gljimports.go @@ -171,6 +171,10 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/jfhamlin/muscrat/pkg/ugen.InterpNone", github_com_jfhamlin_muscrat_pkg_ugen.InterpNone) _register("github.com/jfhamlin/muscrat/pkg/ugen.Knob", reflect.TypeOf((*github_com_jfhamlin_muscrat_pkg_ugen.Knob)(nil)).Elem()) _register("github.com/jfhamlin/muscrat/pkg/ugen.*Knob", reflect.TypeOf((*github_com_jfhamlin_muscrat_pkg_ugen.Knob)(nil))) + _register("github.com/jfhamlin/muscrat/pkg/ugen.KnobUpdate", reflect.TypeOf((*github_com_jfhamlin_muscrat_pkg_ugen.KnobUpdate)(nil)).Elem()) + _register("github.com/jfhamlin/muscrat/pkg/ugen.*KnobUpdate", reflect.TypeOf((*github_com_jfhamlin_muscrat_pkg_ugen.KnobUpdate)(nil))) + _register("github.com/jfhamlin/muscrat/pkg/ugen.KnobValueChangeEvent", github_com_jfhamlin_muscrat_pkg_ugen.KnobValueChangeEvent) + _register("github.com/jfhamlin/muscrat/pkg/ugen.KnobsChangedEvent", github_com_jfhamlin_muscrat_pkg_ugen.KnobsChangedEvent) _register("github.com/jfhamlin/muscrat/pkg/ugen.LinInterp", github_com_jfhamlin_muscrat_pkg_ugen.LinInterp) _register("github.com/jfhamlin/muscrat/pkg/ugen.NewAbs", github_com_jfhamlin_muscrat_pkg_ugen.NewAbs) _register("github.com/jfhamlin/muscrat/pkg/ugen.NewConstant", github_com_jfhamlin_muscrat_pkg_ugen.NewConstant) @@ -183,6 +187,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/jfhamlin/muscrat/pkg/ugen.NewKnob", github_com_jfhamlin_muscrat_pkg_ugen.NewKnob) _register("github.com/jfhamlin/muscrat/pkg/ugen.NewLatch", github_com_jfhamlin_muscrat_pkg_ugen.NewLatch) _register("github.com/jfhamlin/muscrat/pkg/ugen.NewLinExp", github_com_jfhamlin_muscrat_pkg_ugen.NewLinExp) + _register("github.com/jfhamlin/muscrat/pkg/ugen.NewLog2", github_com_jfhamlin_muscrat_pkg_ugen.NewLog2) _register("github.com/jfhamlin/muscrat/pkg/ugen.NewMIDIFreq", github_com_jfhamlin_muscrat_pkg_ugen.NewMIDIFreq) _register("github.com/jfhamlin/muscrat/pkg/ugen.NewMax", github_com_jfhamlin_muscrat_pkg_ugen.NewMax) _register("github.com/jfhamlin/muscrat/pkg/ugen.NewMin", github_com_jfhamlin_muscrat_pkg_ugen.NewMin) diff --git a/pkg/graph/runner.go b/pkg/graph/runner.go index fd90679..9953a43 100644 --- a/pkg/graph/runner.go +++ b/pkg/graph/runner.go @@ -257,7 +257,7 @@ func (r *Runner) newRunState(g *Graph) *runState { node.id = id - graphNode := nodeMap[id] + graphNode := nodeMap[id] // BUGBUG: possible for node not to be found? node.node = graphNode var alignment GraphAlignment diff --git a/pkg/stdlib/mrat/core.glj b/pkg/stdlib/mrat/core.glj index 7782f61..577d9f6 100644 --- a/pkg/stdlib/mrat/core.glj +++ b/pkg/stdlib/mrat/core.glj @@ -10,6 +10,7 @@ NewMin NewAbs NewExp + NewLog2 NewPow NewTanh NewLinExp @@ -196,9 +197,10 @@ (add-node! :out nil :args [i] :sink true))))] (doseq [[ch i] ch-inds] (let [gen (as-node ch) - sink (find-or-create-sink i)] + sink (find-or-create-sink i) + port (str "in" (count (:edges @*graph*)))] ;; port name doesn't matter for out nodes - (add-edge! gen sink (str "in" (count (:nodes @*graph*)))))))) + (add-edge! gen sink port))))) (docgroup "Output") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -520,6 +522,15 @@ (add-node! :exp NewExp :in-edges {:in x}) (math.Exp x))) +(defn log2 + "Returns the base-2 logarithm of x. If x is a node, creates a new node + that computes the base-2 logarithm of x. Else, returns the base-2 logarithm + of x directly." + [x] + (if (is-node? x) + (add-node! :exp NewLog2 :in-edges {:in x}) + (math.Log2 x))) + (defn tanh "Returns the hyperbolic tangent of x. If x is a node, creates a new node that computes the hyperbolic tangent of x. Else, returns the @@ -1123,8 +1134,13 @@ [^:noexpand name nil default 0 min-value -1 - max-value 1] - (add-node! :knob NewKnob :args [name default min-value max-value])) + max-value 1 + step nil + xform identity] + (let [rng (- max-value min-value) + step (or step (/ rng 100.0)) + ugen (add-node! :knob NewKnob :args [name default min-value max-value step])] + (xform ugen))) (defn qwerty-in [name & {:keys [voices]}] @@ -1339,3 +1355,69 @@ (docgroup "Scales") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Scales + +(defn- fm-op + "Creates a simple FM 'operator' in the style of the Yamaha DX series." + [freq envelope input & {:keys [feedback]}] + (let [phase (phasor freq) + feedback (or feedback 0) + ;; log2 is used to scale the feedback amount + feedback (log2 (+ feedback 1)) + has-feedback (or (not (number? feedback)) (> feedback 0)) + fb (if has-feedback (pipe) 0) + osc (tri :phase (+ phase input fb)) ;; todo: sync with the trigger so that the phase is reset + out (* osc envelope)] + (if has-feedback (pipeset! fb (* feedback out))) + out)) + +(defn fm-synth + "Create a simple FM synthesizer with the given configuration. The + configuration is a sequence of operator configurations, where each + operator configuration is a vector of the form: + + [ratio amplitude envelope modulators feedback carrier] + - ratio: the frequency ratio of the operator + - amplitude: the amplitude of the operator + - envelope: the ADSR envelope configuration for the operator + - modulators: a sequence of indices of other operators to modulate + this operator + - feedback: the amount of feedback to apply to the operator + - carrier: a boolean indicating whether this operator is a carrier + (i.e. the output of the synth) + + The synth will sum the outputs of all carrier operators and return + the result. + + For example, to create a simple FM synth with two operators, the + first modulating the second: + + ```clojure + (fm-synth [[1 1 [0.01 0.1 0.01 0.1] [] 0 false] + [2 1 [0.01 0.1 0.01 0.1] [0] 0 true]] + gate freq)```" + [op-conf gate freq] + (let [ops (mapv (fn [[r a e m f c]] + (let [input (if-not (empty? m) (pipe) 0) + op (fm-op (* freq r) + (* a (env-adsr gate e)) + input + :feedback f)] + {:op op + :mods m + :input input + :carrier c})) + op-conf)] + ;; wire up the modulators + (doseq [{:keys [op mods input]} ops + :when (not (empty? mods))] + (let [in-sum (sum (map #(:op (nth ops %)) mods))] + (pipeset! input in-sum))) + (sum (->> ops + (filter :carrier) + (map :op))))) + +(docgroup "Synth") +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/pkg/ugen/knob.go b/pkg/ugen/knob.go index bf53b39..1bb8598 100644 --- a/pkg/ugen/knob.go +++ b/pkg/ugen/knob.go @@ -18,6 +18,7 @@ type ( Min float64 `json:"min"` Max float64 `json:"max"` Def float64 `json:"def"` + Step float64 `json:"step"` valueBits atomic.Uint64 @@ -59,7 +60,7 @@ func GetKnobs() []*Knob { } // NewKnob returns a new Knob ugen. -func NewKnob(name string, def float64, min float64, max float64) *Knob { +func NewKnob(name string, def, min, max, step float64) *Knob { knobLock.Lock() defer knobLock.Unlock() @@ -69,6 +70,7 @@ func NewKnob(name string, def float64, min float64, max float64) *Knob { Min: min, Max: max, Def: def, + Step: step, } k.valueBits.Store(math.Float64bits(def)) diff --git a/pkg/ugen/log2.go b/pkg/ugen/log2.go new file mode 100644 index 0000000..e2fd586 --- /dev/null +++ b/pkg/ugen/log2.go @@ -0,0 +1,16 @@ +package ugen + +import ( + "context" + "math" +) + +func NewLog2() UGen { + return UGenFunc(func(ctx context.Context, cfg SampleConfig, out []float64) { + in := cfg.InputSamples["in"] + _ = in[len(out)-1] + for i := range out { + out[i] = math.Log2(in[i]) + } + }) +} From 07b9d38c34e3dab58110fc0497dbbcbefea92b71 Mon Sep 17 00:00:00 2001 From: James Hamlin Date: Sun, 12 May 2024 18:23:54 +0200 Subject: [PATCH 4/7] Fuzzy sample search Signed-off-by: James Hamlin --- pkg/mrat/script.go | 8 +++++ pkg/stdlib/mrat/core.glj | 72 ++++++++++++++++++++++++++-------------- 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/pkg/mrat/script.go b/pkg/mrat/script.go index 03fa5b6..17ceea6 100644 --- a/pkg/mrat/script.go +++ b/pkg/mrat/script.go @@ -14,6 +14,7 @@ import ( value "github.com/glojurelang/glojure/pkg/lang" "github.com/glojurelang/glojure/pkg/runtime" + "github.com/jfhamlin/muscrat/pkg/conf" "github.com/jfhamlin/muscrat/pkg/console" "github.com/jfhamlin/muscrat/pkg/graph" ) @@ -110,8 +111,15 @@ func EvalScript(filename string) (res *graph.Graph, err error) { require.Invoke(glj.Read("mrat.core")) graphAtom := lang.NewAtom(glj.Read(`{:nodes [] :edges []}`)) + + anyPaths := make([]any, len(conf.SampleFilePaths)) + for i, p := range conf.SampleFilePaths { + anyPaths[i] = p + } + sampleFilePathsAtom := lang.NewAtom(lang.NewVector(anyPaths...)) value.PushThreadBindings(value.NewMap( glj.Var("mrat.core", "*graph*"), graphAtom, + glj.Var("mrat.core", "*sample-file-paths*"), sampleFilePathsAtom, glj.Var("glojure.core", "*out*"), &consoleWriter{}, )) defer value.PopThreadBindings() diff --git a/pkg/stdlib/mrat/core.glj b/pkg/stdlib/mrat/core.glj index 577d9f6..35e3ece 100644 --- a/pkg/stdlib/mrat/core.glj +++ b/pkg/stdlib/mrat/core.glj @@ -84,7 +84,9 @@ (def BUFFER-SIZE github.com$jfhamlin$muscrat$pkg$conf.BufferSize) (def BUFFER-DUR (/ BUFFER-SIZE SAMPLE-RATE)) (def SAMPLE-DUR (/ 1 SAMPLE-RATE)) -(def SAMPLE-FILE-PATHS github.com$jfhamlin$muscrat$pkg$conf.SampleFilePaths) + +(def ^:dynamic *sample-file-paths* + (atom (vec github.com$jfhamlin$muscrat$pkg$conf.SampleFilePaths))) (docgroup "Constants") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1060,28 +1062,45 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Sampler -(defn find-sample-path - "find-sample-path searches the directory given by the env var +(defn add-sample-path! + [& paths] + (swap! *sample-file-paths* #(apply conj % paths))) + +(def ^:private supported-sample-suffixes #{".wav" ".aiff" ".aif" ".flac" ".mp3"}) + +(defn search-samples + [pat & pats] + (let [matches (atom []) + pats (cons pat pats)] + (letfn [(walk [s dir-entry err] + (when-not err + (let [ext (path$filepath.Ext s) + [info err] (os.Stat s)] + (cond + err nil + (not (contains? supported-sample-suffixes ext)) nil + (.IsDir info) nil + (->> pats + (map name) + (map #(re-pattern %)) + (map #(re-seq % s)) + (every? identity)) (do (swap! matches conj s) + nil)))))] + (doseq [dir @*sample-file-paths*] + (path$filepath.WalkDir dir walk)) + @matches))) + +(defn find-sample + "find-sample searches the directories given by the env var MUSCRAT_SAMPLE_PATH for a sample file whose base name matches the given keyword. If the sample is not found, an error is thrown. Supports the following file extensions: .wav, .aiff, .aif, .flac," - [kw] - (letfn [(file-exists? [file] (some? (first (os.Stat file))))] - (let [supported-suffixes [".wav" ".aiff" ".aif" ".flac" ".mp3"] - sample-path SAMPLE-FILE-PATHS - sample-path (if (empty? sample-path) - (throw (errors.New "no sample paths defined")) - sample-path) - sample-path (map #(strings.TrimRight % "/") sample-path) - matches (for [path sample-path - suffix supported-suffixes - file [(str path "/" (name kw) suffix)] - :when (file-exists? file)] - file)] - (if (empty? matches) - (throw (errors.New (str "sample not found: " kw))) - (first matches))))) + [pat & pats] + (let [matches (apply search-samples pat pats)] + (if (empty? matches) + (throw (errors.New (str "sample not found matching " (cons pat pats)))) + (first matches)))) (defn load-sample "Load an audio sample from a file into a buffer (slice of float64s) or @@ -1089,10 +1108,11 @@ resampled from the source to the engine's sample rate (available in the SAMPLE-RATE var). See play-buf for an example of how to play a loaded sample." - [path-or-kw] - (if (keyword? path-or-kw) - (load-sample (find-sample-path path-or-kw)) - (LoadSample path-or-kw))) + [pat-or-pats] + (cond + (keyword? pat-or-pats) (load-sample [pat-or-pats]) + (string? pat-or-pats) (LoadSample pat-or-pats) + :else (load-sample (apply find-sample pat-or-pats)))) (defugen play-buf "Play a buffer (single-channel) or slice of buffers (multi-channel)." @@ -1101,7 +1121,9 @@ trigger 1 start-pos 0 loop 0] - (let [buf-or-bufs (if (keyword? buf-or-bufs) + (let [buf-or-bufs (if (or (keyword? buf-or-bufs) + (vector? buf-or-bufs) + (string? buf-or-bufs)) (load-sample buf-or-bufs) buf-or-bufs)] (when-not (pos? (count buf-or-bufs)) @@ -1139,7 +1161,7 @@ xform identity] (let [rng (- max-value min-value) step (or step (/ rng 100.0)) - ugen (add-node! :knob NewKnob :args [name default min-value max-value step])] + ugen (add-node! :knob NewKnob :args [(str name) default min-value max-value step])] (xform ugen))) (defn qwerty-in From ef146bb3ed4d7c50e73a18df864bcff70eb0c6ec Mon Sep 17 00:00:00 2001 From: James Hamlin Date: Thu, 16 May 2024 08:16:41 -0700 Subject: [PATCH 5/7] Add tidal cycles stuff Signed-off-by: James Hamlin --- go.mod | 8 ++--- go.sum | 16 ++++----- pkg/mrat/script.go | 33 ++++++++++------- pkg/pattern/sequencer.go | 11 ++++++ pkg/stdlib/mrat/core.glj | 78 +++++++++++++++++++++++++++++++++++----- pkg/ugen/impulse.go | 12 +++++++ pkg/ugen/zeros.go | 7 ++++ 7 files changed, 132 insertions(+), 33 deletions(-) create mode 100644 pkg/ugen/zeros.go diff --git a/go.mod b/go.mod index bf9723e..896f2fa 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.21.9 require ( github.com/ebitengine/purego v0.7.0 github.com/fsnotify/fsnotify v1.6.0 - github.com/glojurelang/glojure v0.2.3-0.20240129062610-e04ecdbb91c9 + github.com/glojurelang/glojure v0.2.4 github.com/go-audio/audio v1.0.0 github.com/go-audio/wav v1.1.0 github.com/gordonklaus/portaudio v0.0.0-20221027163845-7c3b689db3cc @@ -62,13 +62,13 @@ require ( github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wailsapp/go-webview2 v1.0.10 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - go4.org/intern v0.0.0-20230205224052-192e9f60865c // indirect - go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect + go4.org/intern v0.0.0-20230525184215-6c62f75575cb // indirect + go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect golang.org/x/crypto v0.18.0 // indirect golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/image v0.12.0 // indirect golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 2553246..185c746 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/glojurelang/glojure v0.2.3-0.20240129062610-e04ecdbb91c9 h1:ZzJgl6+j5Yp9qOF071TO3shKWkRfEt0jO7hWB49UAag= -github.com/glojurelang/glojure v0.2.3-0.20240129062610-e04ecdbb91c9/go.mod h1:wbKmjpKBqMBHKSgj5DjGg7NJVxQva2W7BqdVWW8UfFA= +github.com/glojurelang/glojure v0.2.4 h1:nAY5jt8PyZTjpn5XgmGJIMfDopnxgqoFLohX1Qgi1S8= +github.com/glojurelang/glojure v0.2.4/go.mod h1:wbKmjpKBqMBHKSgj5DjGg7NJVxQva2W7BqdVWW8UfFA= github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4= github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= @@ -160,11 +160,11 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= gitlab.com/gomidi/midi/v2 v2.0.30 h1:RgRYbQeQSab5ZaP1lqRcCTnTSBQroE3CE6V9HgMmOAc= gitlab.com/gomidi/midi/v2 v2.0.30/go.mod h1:Y6IFFyABN415AYsFMPJb0/43TRIuVYDpGKp2gDYLTLI= -go4.org/intern v0.0.0-20230205224052-192e9f60865c h1:b8WZ7Ja8nKegYxfwDLLwT00ZKv4lXAQrw8LYPK+cHSI= -go4.org/intern v0.0.0-20230205224052-192e9f60865c/go.mod h1:RJ0SVrOMpxLhgb5noIV+09zI1RsRlMsbUcSxpWHqbrE= -go4.org/unsafe/assume-no-moving-gc v0.0.0-20230204201903-c31fa085b70e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= -go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg= +go4.org/intern v0.0.0-20230525184215-6c62f75575cb h1:ae7kzL5Cfdmcecbh22ll7lYP3iuUdnfnhiPcSaDgH/8= +go4.org/intern v0.0.0-20230525184215-6c62f75575cb/go.mod h1:Ycrt6raEcnF5FTsLiLKkhBTO6DPX3RCUCUVnks3gFJU= go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 h1:lGdhQUN/cnWdSH3291CUuxSEqc+AsGTiDxPP3r2J0l4= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -235,8 +235,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/pkg/mrat/script.go b/pkg/mrat/script.go index 17ceea6..ff349f4 100644 --- a/pkg/mrat/script.go +++ b/pkg/mrat/script.go @@ -11,7 +11,6 @@ import ( "github.com/glojurelang/glojure/pkg/glj" "github.com/glojurelang/glojure/pkg/lang" - value "github.com/glojurelang/glojure/pkg/lang" "github.com/glojurelang/glojure/pkg/runtime" "github.com/jfhamlin/muscrat/pkg/conf" @@ -112,17 +111,13 @@ func EvalScript(filename string) (res *graph.Graph, err error) { graphAtom := lang.NewAtom(glj.Read(`{:nodes [] :edges []}`)) - anyPaths := make([]any, len(conf.SampleFilePaths)) - for i, p := range conf.SampleFilePaths { - anyPaths[i] = p + lang.PushThreadBindings(getScriptThreadBindings(graphAtom)) + defer lang.PopThreadBindings() + + { // initialize dynamic vars + initCPS := glj.Var("mrat.core", "init-cps!") + initCPS.Invoke() } - sampleFilePathsAtom := lang.NewAtom(lang.NewVector(anyPaths...)) - value.PushThreadBindings(value.NewMap( - glj.Var("mrat.core", "*graph*"), graphAtom, - glj.Var("mrat.core", "*sample-file-paths*"), sampleFilePathsAtom, - glj.Var("glojure.core", "*out*"), &consoleWriter{}, - )) - defer value.PopThreadBindings() // get the absolute path to the script absPath, err := filepath.Abs(filename) @@ -140,7 +135,7 @@ func EvalScript(filename string) (res *graph.Graph, err error) { runtime.AddLoadPath(os.DirFS(dir)) addedPaths[dir] = true } - require.Invoke(glj.Read(strings.TrimSuffix(name, ".glj")), value.NewKeyword("reload")) + require.Invoke(glj.Read(strings.TrimSuffix(name, ".glj")), lang.NewKeyword("reload")) require.Invoke(glj.Read("mrat.graph")) simplifyGraph := glj.Var("mrat.graph", "simplify-graph") @@ -148,6 +143,20 @@ func EvalScript(filename string) (res *graph.Graph, err error) { return graph.SExprToGraph(g), nil } +func getScriptThreadBindings(graphAtom *lang.Atom) lang.IPersistentMap { + anyPaths := make([]any, len(conf.SampleFilePaths)) + for i, p := range conf.SampleFilePaths { + anyPaths[i] = p + } + sampleFilePathsAtom := lang.NewAtom(lang.NewVector(anyPaths...)) + + return lang.NewMap( + glj.Var("mrat.core", "*graph*"), graphAtom, + glj.Var("mrat.core", "*sample-file-paths*"), sampleFilePathsAtom, + glj.Var("glojure.core", "*out*"), &consoleWriter{}, + ) +} + func GetNSPublics() []Symbol { require := glj.Var("glojure.core", "require") require.Invoke(glj.Read("mrat.core")) diff --git a/pkg/pattern/sequencer.go b/pkg/pattern/sequencer.go index dc4d840..b8470dc 100644 --- a/pkg/pattern/sequencer.go +++ b/pkg/pattern/sequencer.go @@ -9,8 +9,15 @@ import ( func NewSequencer() ugen.UGen { index := 0 lastTrig := 1.0 + lastSync := 1.0 return ugen.UGenFunc(func(ctx context.Context, cfg ugen.SampleConfig, out []float64) { trigs := cfg.InputSamples["trigger"] + syncs := cfg.InputSamples["sync"] + if len(syncs) == 0 { + syncs = ugen.Zeros + } + _ = syncs[len(out)-1] + vals := ugen.CollectIndexedInputs(cfg) if len(vals) == 0 { return @@ -22,8 +29,12 @@ func NewSequencer() ugen.UGen { if trigs[i] > 0.0 && lastTrig <= 0.0 { index = (index + 1) % len(vals) } + if syncs[i] > 0 && lastSync <= 0 { + index = 0 // this case before or after the increment case? + } out[i] = vals[index][i] lastTrig = trigs[i] + lastSync = syncs[i] } }) } diff --git a/pkg/stdlib/mrat/core.glj b/pkg/stdlib/mrat/core.glj index 35e3ece..bbef1f7 100644 --- a/pkg/stdlib/mrat/core.glj +++ b/pkg/stdlib/mrat/core.glj @@ -524,20 +524,20 @@ (add-node! :exp NewExp :in-edges {:in x}) (math.Exp x))) -(defn log2 +(defugen log2 "Returns the base-2 logarithm of x. If x is a node, creates a new node that computes the base-2 logarithm of x. Else, returns the base-2 logarithm of x directly." - [x] + [x 1] (if (is-node? x) (add-node! :exp NewLog2 :in-edges {:in x}) (math.Log2 x))) -(defn tanh +(defugen tanh "Returns the hyperbolic tangent of x. If x is a node, creates a new node that computes the hyperbolic tangent of x. Else, returns the hyperbolic tangent of x directly." - [x] + [x 0] (if (is-node? x) (add-node! :tanh NewTanh :in-edges {:in x}) (math.Tanh x))) @@ -603,6 +603,12 @@ (add-node! :midifreq NewMIDIFreq :in-edges {"in" note}) (* 440.0 (pow 2 (/ (- note 69.0) 12.0))))) +(defn lcm + [& x] + (letfn [(gcd [a b] (if (zero? b) a (gcd b (mod a b)))) + (lcm [a b] (/ (* a b) (gcd a b)))] + (reduce lcm x))) + (docgroup "Utilities") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -662,11 +668,13 @@ (defugen impulse [freq default-freq - iphase nil] + iphase nil + sync nil] ;; TODO: implement sync (let [iphase (if iphase (as-node iphase)) freq-node (as-node freq) node (add-node! :impulse NewImpulse)] (if iphase (add-edge! iphase node "iphase")) + (if sync (add-edge! sync node "sync")) (add-edge! freq-node node "w") node)) @@ -1258,11 +1266,13 @@ (defn step "Cycle through a sequence of values on each trigger." - [trig freqs] + [trig freqs & {:keys [sync]}] (let [node (add-node! :sequencer github.com$jfhamlin$muscrat$pkg$pattern.NewSequencer) trig (as-node trig) - freqs (map as-node freqs)] + freqs (map as-node freqs) + sync (if sync (as-node sync) nil)] (add-edge! trig node "trigger") + (when sync (add-edge! sync node "sync")) (doseq-idx [[freq i] freqs] (add-edge! freq node (str \$ i))) node)) @@ -1316,13 +1326,63 @@ (defugen impulse-pattern [impulse 1 "The impulse signal to trigger the pattern." - ^:noexpand pattern (euclid 3 8) "The pattern to trigger on the impulse signal."] - (let [pat-seq (step impulse pattern)] + ^:noexpand pattern (euclid 3 8) "The pattern to trigger on the impulse signal." + sync nil] + (let [pat-seq (step impulse pattern :sync sync)] (* impulse pat-seq))) (docgroup "Patterns") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Tidal Cycles-like + +(def ^{:dynamic true, :private true} *cps* (atom nil)) +(def ^:dynamic *tctick* (atom nil)) + +(defn ^:private init-cps! + [] + (reset! *cps* (pipe)) + (pipeset! @*cps* (/ 135 60 4)) + (reset! *tctick* (impulse @*cps*))) + +(defn setcps! + [cps] + (pipeset! @*cps* cps)) + +(defn tcpat + [pattern] + (cond + (= pattern '_) 0 + (not (coll? pattern)) 1 + :else (let [sub-patterns (map tcpat pattern) + sub-patterns (map #(if (coll? %) % [%]) sub-patterns) + counts (map count sub-patterns) + lcm-steps (reduce lcm counts) + expanded (map (fn [pat cnt] (concat pat (repeat (- lcm-steps cnt) 0))) + sub-patterns counts)] + (flatten expanded)))) + +(defn tcvals + [value-pattern] + (let [pattern (tcpat value-pattern) + values (filter #(not= % '_) (flatten value-pattern)) + ticks (-> (impulse (* (count pattern) @*cps*) :sync @*tctick*) + (impulse-pattern pattern :sync @*tctick*)) + value-steps (step ticks values :sync @*tctick*)] + [value-steps ticks])) + +(defn tctrig + [trig-pattern] + (let [pattern (tcpat trig-pattern) + values (filter #(not= % '_) (flatten trig-pattern)) + ticks (-> (impulse (* (count pattern) @*cps*) :sync @*tctick*) + (impulse-pattern pattern :sync @*tctick*))] + ticks)) + +(docgroup "Patterns - Tidal-like") +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; MIDI notes diff --git a/pkg/ugen/impulse.go b/pkg/ugen/impulse.go index 82e466c..75b4f3b 100644 --- a/pkg/ugen/impulse.go +++ b/pkg/ugen/impulse.go @@ -13,12 +13,18 @@ func NewImpulse(opts ...Option) UGen { var phaseOffset, freq, phase, phaseIncrement float64 initialized := false + lastSync := 1.0 return UGenFunc(func(ctx context.Context, cfg SampleConfig, out []float64) { ws := cfg.InputSamples["w"] iphases := cfg.InputSamples["iphase"] + syncs := cfg.InputSamples["sync"] + if len(syncs) == 0 { + syncs = Zeros + } _ = ws[len(out)-1] + _ = syncs[len(out)-1] if !initialized { freq = math.Max(ws[0], 0) @@ -50,6 +56,12 @@ func NewImpulse(opts ...Option) UGen { } } phase += phaseIncrement + + if syncs[i] > 0 && lastSync <= 0 { + phase = 1 // wrap around and trigger a tick + } + lastSync = syncs[i] + if phase >= 1 { phase = math.Mod(phase, 1) out[i] = 1 diff --git a/pkg/ugen/zeros.go b/pkg/ugen/zeros.go new file mode 100644 index 0000000..432161a --- /dev/null +++ b/pkg/ugen/zeros.go @@ -0,0 +1,7 @@ +package ugen + +import "github.com/jfhamlin/muscrat/pkg/conf" + +var ( + Zeros = make([]float64, conf.BufferSize) +) From c72e60c9af84b9de963dbab4a7d2c684cc968705 Mon Sep 17 00:00:00 2001 From: James Hamlin Date: Sun, 2 Jun 2024 11:46:18 -0700 Subject: [PATCH 6/7] play-buf rename, better knobs Signed-off-by: James Hamlin --- frontend/package-lock.json | 87 +++++++++++++++++++++++- frontend/package.json | 1 + frontend/package.json.md5 | 2 +- frontend/src/components/Editor/index.jsx | 6 +- frontend/src/components/Knobs/index.jsx | 53 +++++++++------ pkg/mrat/script.go | 18 ++++- pkg/stdlib/mrat/core.glj | 54 ++++++++------- 7 files changed, 168 insertions(+), 53 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9aeaac3..f3ddb20 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "@monaco-editor/react": "^4.6.0", "github-markdown-css": "^5.5.1", "hydra-synth": "^1.3.29", + "primereact": "^10.6.6", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^9.0.1", @@ -390,6 +391,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz", + "integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", @@ -671,6 +683,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", @@ -1169,6 +1189,15 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2783,7 +2812,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3051,6 +3079,38 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/primereact": { + "version": "10.6.6", + "resolved": "https://registry.npmjs.org/primereact/-/primereact-10.6.6.tgz", + "integrity": "sha512-+C0Bt6vS/jh09DQVS4UXpVctbvqJDUC3t3mVdGmhmIINYD8kdfL3fvc3bUGniGxkKKzwkSYdAQXhZlcgj8LUgw==", + "dependencies": { + "@types/react-transition-group": "^4.4.1", + "react-transition-group": "^4.4.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/property-information": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", @@ -3122,6 +3182,11 @@ "react": "^18.2.0" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/react-markdown": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz", @@ -3156,6 +3221,21 @@ "node": ">=0.10.0" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -3188,6 +3268,11 @@ "node": ">=8.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/regl": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/regl/-/regl-1.7.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7001478..73592cd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "@monaco-editor/react": "^4.6.0", "github-markdown-css": "^5.5.1", "hydra-synth": "^1.3.29", + "primereact": "^10.6.6", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^9.0.1", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 1b05967..207b920 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -320e1f1ff03eab1e1f33df45866c8ecf \ No newline at end of file +0692201ae20cdef84c5492222617614f \ No newline at end of file diff --git a/frontend/src/components/Editor/index.jsx b/frontend/src/components/Editor/index.jsx index 96f48d2..51dd6c8 100644 --- a/frontend/src/components/Editor/index.jsx +++ b/frontend/src/components/Editor/index.jsx @@ -51,9 +51,9 @@ export default (props) => { }); }); - GetNSPublics().then((nsPublics) => { - console.log("got publics", nsPublics); - }); + /* GetNSPublics().then((nsPublics) => { + * console.log("got publics", nsPublics); + * }); */ // custom autocomplete const completionItemProvider = monaco.languages.registerCompletionItemProvider('clojure', { diff --git a/frontend/src/components/Knobs/index.jsx b/frontend/src/components/Knobs/index.jsx index 2dba37e..bfdc033 100644 --- a/frontend/src/components/Knobs/index.jsx +++ b/frontend/src/components/Knobs/index.jsx @@ -3,6 +3,9 @@ import { useEffect, } from 'react'; +import { Knob as PRKnob } from 'primereact/knob'; +import { InputNumber } from 'primereact/inputnumber'; + import { GetKnobs, } from "../../../wailsjs/go/main/App"; @@ -15,26 +18,32 @@ import { const Knob = ({ knob }) => { const [value, setValue] = useState(knob.def); + const knobValueChange = (value) => { + // at most 4 decimal places + value = parseFloat(value.toFixed(4)); + EventsEmit('knob-value-change', knob.id, new Number(value)); + setValue(value); + } + + // label is centered return ( -
-

{knob.name}

- {/* input, focus on click */} - { - EventsEmit('knob-value-change', knob.id, new Number(e.target.value)); - setValue(e.target.value); - }} - onClick={(e) => { - e.target.focus(); - }} - /> - {/* value */} -
{value}
+
+

+ {knob.name} +

+ knobValueChange(e.value)} /> +
+ knobValueChange(e.value)} /> +
) } @@ -66,8 +75,10 @@ export default () => { }, []); return ( -
-

Knobs

+
+

+ Knobs +

{knobs.map((knob) => ( ))} diff --git a/pkg/mrat/script.go b/pkg/mrat/script.go index ff349f4..d5358d4 100644 --- a/pkg/mrat/script.go +++ b/pkg/mrat/script.go @@ -114,9 +114,21 @@ func EvalScript(filename string) (res *graph.Graph, err error) { lang.PushThreadBindings(getScriptThreadBindings(graphAtom)) defer lang.PopThreadBindings() - { // initialize dynamic vars - initCPS := glj.Var("mrat.core", "init-cps!") - initCPS.Invoke() + { // initialize other dynamic vars + pipeFn := glj.Var("mrat.core", "pipe") + impulse := glj.Var("mrat.core", "impulse") + setCPS := glj.Var("mrat.core", "setcps!") + + pipe := pipeFn.Invoke() + lang.PushThreadBindings(lang.NewMap( + glj.Var("mrat.core", "*cps*"), pipe, + glj.Var("mrat.core", "*tctick*"), impulse.Invoke(pipe), + )) + + // default to 135 bpm + setCPS.Invoke(135.0 / 60.0 / 4.0) + + defer lang.PopThreadBindings() } // get the absolute path to the script diff --git a/pkg/stdlib/mrat/core.glj b/pkg/stdlib/mrat/core.glj index bbef1f7..e8457ad 100644 --- a/pkg/stdlib/mrat/core.glj +++ b/pkg/stdlib/mrat/core.glj @@ -1114,7 +1114,7 @@ "Load an audio sample from a file into a buffer (slice of float64s) or a slice of buffers for multi-channel audio. The buffer will be resampled from the source to the engine's sample rate (available in - the SAMPLE-RATE var). See play-buf for an example of how to play a + the SAMPLE-RATE var). See smp for an example of how to play a loaded sample." [pat-or-pats] (cond @@ -1122,7 +1122,7 @@ (string? pat-or-pats) (LoadSample pat-or-pats) :else (load-sample (apply find-sample pat-or-pats)))) -(defugen play-buf +(defugen smp "Play a buffer (single-channel) or slice of buffers (multi-channel)." [^:noexpand buf-or-bufs nil rate 1 @@ -1135,7 +1135,7 @@ (load-sample buf-or-bufs) buf-or-bufs)] (when-not (pos? (count buf-or-bufs)) - (throw (errors.New "play-buf requires a non-empty buffer or slice of buffers"))) + (throw (errors.New "smp requires a non-empty buffer or slice of buffers"))) (let [bufs (if (#(or (number? %) (keyword? %)) (first buf-or-bufs)) [buf-or-bufs] ;; wrap single buffer in a vector buf-or-bufs)] @@ -1143,7 +1143,7 @@ (let [buf (if (keyword? buf) (load-sample buf) buf)] - (add-node! :play-buf NewSampler + (add-node! :smp NewSampler :args [buf] :in-edges {:trigger trigger :rate rate @@ -1337,18 +1337,12 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Tidal Cycles-like -(def ^{:dynamic true, :private true} *cps* (atom nil)) -(def ^:dynamic *tctick* (atom nil)) - -(defn ^:private init-cps! - [] - (reset! *cps* (pipe)) - (pipeset! @*cps* (/ 135 60 4)) - (reset! *tctick* (impulse @*cps*))) +(def ^{:dynamic true, :private true} *cps* nil) +(def ^:dynamic *tctick* nil) (defn setcps! [cps] - (pipeset! @*cps* cps)) + (pipeset! *cps* cps)) (defn tcpat [pattern] @@ -1359,25 +1353,37 @@ sub-patterns (map #(if (coll? %) % [%]) sub-patterns) counts (map count sub-patterns) lcm-steps (reduce lcm counts) - expanded (map (fn [pat cnt] (concat pat (repeat (- lcm-steps cnt) 0))) - sub-patterns counts)] + expanded (map (fn [pat cnt] + ;; pad each element with 0s to make + ;; the whole pat lcm-steps long + (let [rem (- lcm-steps cnt) + rep (/ rem cnt) + pad (repeat rep 0)] + (map #(cons % pad) pat))) + sub-patterns counts)] (flatten expanded)))) (defn tcvals - [value-pattern] - (let [pattern (tcpat value-pattern) + [value-pattern & {:keys [slow]}] + (let [slow (long (or slow 1)) ;; must be integral + cps (/ *cps* slow) + tctick (pulse-div *tctick* slow) + pattern (tcpat value-pattern) values (filter #(not= % '_) (flatten value-pattern)) - ticks (-> (impulse (* (count pattern) @*cps*) :sync @*tctick*) - (impulse-pattern pattern :sync @*tctick*)) - value-steps (step ticks values :sync @*tctick*)] + ticks (-> (impulse (* (count pattern) cps) :sync tctick) + (impulse-pattern pattern :sync tctick)) + value-steps (step ticks values :sync tctick)] [value-steps ticks])) (defn tctrig - [trig-pattern] - (let [pattern (tcpat trig-pattern) + [trig-pattern & {:keys [slow]}] + (let [slow (long (or slow 1)) ;; must be integral + cps (/ *cps* slow) + tctick (pulse-div *tctick* slow) + pattern (tcpat trig-pattern) values (filter #(not= % '_) (flatten trig-pattern)) - ticks (-> (impulse (* (count pattern) @*cps*) :sync @*tctick*) - (impulse-pattern pattern :sync @*tctick*))] + ticks (-> (impulse (* (count pattern) cps) :sync tctick) + (impulse-pattern pattern :sync tctick))] ticks)) (docgroup "Patterns - Tidal-like") From d7fb947b74f6dd4cbc6c014437edf6af7b07ba73 Mon Sep 17 00:00:00 2001 From: James Hamlin Date: Sun, 2 Jun 2024 13:52:18 -0700 Subject: [PATCH 7/7] Fix examples Signed-off-by: James Hamlin --- examples/bitcrush.glj | 12 +++++-- examples/calculating.glj | 2 +- examples/compus.glj | 4 +-- examples/drumcircle.glj | 2 +- examples/fm.glj | 64 ++++++++++++------------------------- examples/ihylp.glj | 8 ++--- examples/karplus_strong.glj | 19 +++++------ examples/kick.glj | 4 +-- examples/scriabin.glj | 8 ++--- examples/st.glj | 2 +- examples/sunset.glj | 18 +++++------ 11 files changed, 64 insertions(+), 79 deletions(-) diff --git a/examples/bitcrush.glj b/examples/bitcrush.glj index 664ce04..3d0fd4f 100644 --- a/examples/bitcrush.glj +++ b/examples/bitcrush.glj @@ -1,9 +1,17 @@ (ns user (:require [mrat.core :refer :all])) +(def metro (impulse (knob "metro" 4 1 10))) + +(def cutoff (knob "cutoff" 300 200 10000)) + (play (-> (saw [100 150 202 304 606] :mul 0.5 :iphase [0.2 0 0.5]) - (bitcrush :bits (step (impulse 8) [3 2 16 5])) - (rlpf 15000 :mul 0.25) + (bitcrush :bits (step metro [3 2 16 5])) sum + (rlpf (env metro [5000 cutoff cutoff 5000] [0.01 0.01 0.1]) + (- 1 (* 0.9 (env-perc metro [0.01 0.1]))) + :mul 0.25) (* 0.6) + (* (env-perc metro [0.01 0.1])) + (freeverb :room-size (knob "room-size" 0.5 0 1)) limiter)) diff --git a/examples/calculating.glj b/examples/calculating.glj index 4ddeb50..bc3babc 100644 --- a/examples/calculating.glj +++ b/examples/calculating.glj @@ -7,6 +7,6 @@ (play (-> (sin freq :duty (sin 4.1 :mul 0.4 :add 0.5)) (* metro) - (combc 2 0.01 3) + (combc 2 0.018 3) (rlpf (* freq 8) 1) (* 0.1))) diff --git a/examples/compus.glj b/examples/compus.glj index 44a1955..ea91b2d 100644 --- a/examples/compus.glj +++ b/examples/compus.glj @@ -41,10 +41,10 @@ (def trig4 (impulse (/ 1 (* 4 dur)))) (def loopr - (play-buf compus :trigger trig4 :rate (choose trig4 [0.5 1 1 1 1 2]))) + (smp compus :trigger trig4 :rate (choose trig4 [0.5 1 1 1 1 2]))) (def bass - (play-buf :bass_voxy_c + (smp :bass_voxy_c :trigger (choose trig [0 0 0 1]) :rate (choose trig [0.5 0.5 1 1 2 4]))) diff --git a/examples/drumcircle.glj b/examples/drumcircle.glj index 91b69ce..20581ce 100644 --- a/examples/drumcircle.glj +++ b/examples/drumcircle.glj @@ -9,6 +9,6 @@ (combc 2 0.014 1) (* 0.5) (rlpf 300 0.3) - (+ (-> (play-buf :loop_amen_full :rate (step trig [0.4 1 0.1 2 1.5 0.6 0.25]) :trigger trig :mul 0.1) + (+ (-> (smp :loop_amen_full :rate (step trig [0.4 1 0.1 2 1.5 0.6 0.25]) :trigger trig :mul 0.1) (rhpf 1400 0.15 :mul 0.2))) (freeverb :room-size 0.1))) diff --git a/examples/fm.glj b/examples/fm.glj index fb44f31..d88f5c1 100644 --- a/examples/fm.glj +++ b/examples/fm.glj @@ -1,63 +1,39 @@ (ns examples.fm - (:require [mrat.core :refer :all])) - -(defn fm-op - "Creates a simple FM 'operator' in the style of the Yamaha DX series." - [freq envelope input & {:keys [feedback]}] - (let [phase (phasor freq) - feedback (or feedback 0) - ;; log2 is used to scale the feedback amount - ;; todo: add log2 to the core namespace - feedback (math.Log2 (+ feedback 1)) - has-feedback (> feedback 0) - fb (if has-feedback (pipe) 0) - osc (sin :phase (+ phase input fb)) ;; todo: sync with the trigger so that the phase is reset - out (* osc envelope)] - (if has-feedback (pipeset! fb (* feedback out))) - out)) + (:use [mrat.core])) (def impulse-rate 6) (def gate (lfpulse impulse-rate :duty 0.2)) (def freq - (let [offset (choose gate [0 0 0 0 0 0 12 12 12 24])] + (let [offset (step gate [0 0 0 0 0 0 12 12 12 24])] (step gate - (->> mixolydian - (map #(+ D2 % offset)) + (->> locrian + (map #(+ A2 % offset)) (map mtof))))) (def op-conf ;; freq-ratio amp adsr mod-indexes feedback carrier [[1 0.8 [0.001 0.15 1 0.15] [1] 0.1 true] - [2 0.5 [0.001 0.15 0.1 0.15] [2] 0 false] - [7 0.3 [0.1 0.15 1 0.01] nil 0.4 false] + [2 0.5 [0.01 0.15 0.1 0.15] [2] 0.0 false] + [7 0.3 [0.01 0.15 1 0.01] nil 0.4 false] + [0.5 1 [0.1 0.15 0.5 0.001] [1 2] (knob "fb" 0 0 1) true] ]) -(def fm-out - (let [ops (mapv (fn [[r a e m f c]] - (let [input (if-not (empty? m) (pipe) 0) - op (fm-op (* freq r) - (* a (env-adsr gate e)) - input - :feedback f)] - {:op op - :mods m - :input input - :carrier c})) - op-conf)] - ;; wire up the modulators - (doseq [{:keys [op mods input]} ops - :when (not (empty? mods))] - (let [in-sum (sum (map #(:op (nth ops %)) mods))] - (pipeset! input in-sum))) - (sum (->> ops - (filter :carrier) - (map :op))))) +(println (double (/ BUFFER-SIZE SAMPLE-RATE))) + +(def trig (impulse 6)) + +(def kick (smp :kick :trigger (pulse-div trig 2) :mul 0.7)) +(def kick-amp (amplitude kick)) +(play kick) (def sig - (-> fm-out - (combc 0.5 0.5 3) - (* 0.25) + (-> (fm-synth op-conf trig (step trig (map (comp mtof #(+ % 2)) [C3 E3 B4 E3 C3 E3 G3]))) + (combc 0.5 (knob "delay" 0.25 0.1 1) 3) + (* 0.75) + tanh + (* 0.3) + (* (- 1 kick-amp)) limiter)) ;; (wavout sig "fm.wav") diff --git a/examples/ihylp.glj b/examples/ihylp.glj index 91275e3..cd50748 100644 --- a/examples/ihylp.glj +++ b/examples/ihylp.glj @@ -1,11 +1,11 @@ (ns examples.ihylpr "A cover of 'I heard you like polyrhythms' by Virtual Riot https://www.youtube.com/watch?v=SthcxWPXG_E" - (:require [mrat.core :refer :all])) + (:use [mrat.core])) (def bps 1.0) -(def speed-factor 0.005) +(def speed-factor (knob "speed-factor" 0.005 0.001 0.5 0.001)) (def offset (choose (impulse (/ bps 10)) [-9 -5 0 3 7 12])) @@ -24,7 +24,7 @@ (defn synth [rate note] - (let [osc (sin (mtof note) :duty 0.2) + (let [osc (sin (mtof note) :duty 0.6) trig (impulse rate :iphase (.Float64 mrand)) amp (env trig [0 1 1 0] [0.01 0.02 0.1])] (* osc amp))) @@ -34,6 +34,6 @@ (pan2 (/ %1 (count notes)))) (range (count notes)) notes) (sum) (* (/ 4 (inc (count notes)))) - (freeverb :room-size 0.5))) + (freeverb :room-size (knob "room-size" 0.5 0 1)))) (play ihylpr) diff --git a/examples/karplus_strong.glj b/examples/karplus_strong.glj index 6c37743..5341f5a 100644 --- a/examples/karplus_strong.glj +++ b/examples/karplus_strong.glj @@ -3,10 +3,10 @@ (def fade-in-time 60) -(def metro (impulse 10)) +(def metro (impulse 8)) -(def burst (-> (pink-noise) - (* (env-adsr metro [0.001 0.001 1 0.001])) +(def burst (-> (saw 50) + (* (env-adsr metro [0.001 0.001 1 0.1])) (* 1))) (def feedback (pipe)) @@ -14,14 +14,15 @@ (def delay-filter (-> feedback (delayc 0.5 (step metro [0.0001 0.002 0.001 0.01 0.0015 0.0019])) - (lores 1000 0) - (* (sin 4 :mul 0.02 :add 0.98)))) + (lores 400 0) + (* 0.98))) (pipeset! feedback (+ burst delay-filter)) (play (-> feedback - wfold - (loshelf 20 :db -20) - (rhpf 1200) - (hishelf 12000 :db -90) + (* 0.1) + tanh +;; (loshelf 20 :db -20) + (rlpf 500 0.2) +;; (hishelf 12000 :db -90) limiter)) diff --git a/examples/kick.glj b/examples/kick.glj index 0ffc9c9..e0e3c72 100644 --- a/examples/kick.glj +++ b/examples/kick.glj @@ -3,7 +3,7 @@ (defugen kick [trig 0] - (let [[hi lo] [700 54] + (let [[hi lo] [(knob "high-freq" 700 100 2000) 54] freq (env-perc trig [0 0.13] :curve -6) freq (linexp freq 0 1 lo hi) ;; another little layer @@ -18,4 +18,4 @@ snd)) -(play (kick (impulse 2) :mul 0.5)) +(play (kick (impulse (knob "bps" 2 1 10)) :mul 0.5)) diff --git a/examples/scriabin.glj b/examples/scriabin.glj index 6a8be42..bcb5b7f 100644 --- a/examples/scriabin.glj +++ b/examples/scriabin.glj @@ -1,6 +1,6 @@ (ns examples.scriabin - (:require [mrat.core :refer :all] :reload - [mrat.abc :as abc] :reload)) + (:require [mrat.core :refer :all] + [mrat.abc :as abc])) (def tempo-in (midi-in "tempo" :cc :controller 70 :default 0.5)) (def tempo (* 2 72 tempo-in)) ;; 72 quarter notes per minute default @@ -62,10 +62,10 @@ (def envelope-bass (mkenv metro-bass dur-bass)) (def tune - (let [sig (-> (saw (* root (semitones (sin 6 :mul 0.05))) :duty 0.8) + (let [sig (-> (saw (* root (semitones (sin 8 :mul 0.05))) :duty 0.8) (lores (* root (semitones 7)) 0.8) (* envelope)) - sig-bass (-> (saw (* root-bass (semitones (sin 6 :mul 0.1)))) + sig-bass (-> (sin (* root-bass (semitones (sin 6 :mul 0.1))) :duty 0.8) (lores (* root-bass 3) 0.9) (* envelope-bass))] (-> (+ sig (* (dbamp -5) sig-bass)) diff --git a/examples/st.glj b/examples/st.glj index 071c5e6..f21fcdf 100644 --- a/examples/st.glj +++ b/examples/st.glj @@ -23,7 +23,7 @@ (def melody-freq-seq (env 1 [4 16 32] [melody-cf1-time melody-cf2-dur] - :interp :hold)) + :curve :hold)) (def duty-env (env 1 [0.8 0.8 0.2 0.2] [30 2 100])) diff --git a/examples/sunset.glj b/examples/sunset.glj index 06b18c0..082fbfe 100644 --- a/examples/sunset.glj +++ b/examples/sunset.glj @@ -1,16 +1,16 @@ (ns examples.sunset (:require [mrat.core :refer :all])) -(def melody-amp (* 0.3 (midi-in "melody" :cc :controller 70 :default 0.5))) -(def drone-amp (midi-in "drone" :cc :controller 74 :default 0.6)) -(def amp (* 0.8 (midi-in "amp" :cc :controller 73 :default 0.5))) +(def melody-amp (* 0.3 (knob "melody" 0.5 0 1))) +(def drone-amp (knob "drone" 0.6 0 1)) +(def amp (* 0.8 (knob "amp" 0.5 0 1))) -(def split-cutoff-midi (+ 40 36 (* 40 (midi-in "split" :cc :controller 71 :default 0.5)))) +(def split-cutoff-midi (+ 40 36 (* 40 (knob "split" 0.5 0 1)))) -(def res (* 0.95 (midi-in "res" :cc :controller 75 :default 0.6))) +(def res (* 0.95 (knob "res" 0.6 0 1))) -(def hpcutoff (mtof (+ 40 (* 90 (midi-in "hpcutoff" :cc :controller 76 :default 0))))) -(def lpcutoff (mtof (+ 20 (* 110 (midi-in "lpcutoff" :cc :controller 77 :default 1))))) +(def hpcutoff (mtof (+ 40 (* 90 (knob "hpcutoff" 0 0 1))))) +(def lpcutoff (mtof (+ 20 (* 110 (knob "lpcutoff" 1 0 1))))) (def intervals [0 7 0 5 0 10 0 5 0 3 0 7 @@ -19,9 +19,9 @@ 0 0 0 0 0 0 ]) -(def root-midi 50) +(def root-midi (knob "root-midi" 40 30 70 1)) -(def metro (impulse 6)) +(def metro (impulse (knob "metro" 4 4 6 1))) (def metro-slo (impulse 0.1))