diff --git a/src/beep.ffi.mjs b/src/beep.ffi.mjs index debe81a..6643ab2 100644 --- a/src/beep.ffi.mjs +++ b/src/beep.ffi.mjs @@ -262,8 +262,6 @@ class AudioEngine extends (globalThis.AudioContext ?? Object) { source.connect(targetParam); } } else { - console.log({ source, target, isDeferred }); - source.connect(target); } } @@ -285,6 +283,23 @@ class AudioEngine extends (globalThis.AudioContext ?? Object) { } } +class NameRegistry { + #prefix = ""; + #count = 0; + + constructor(prefix = "#") { + this.#prefix = prefix; + } + + name() { + return this.#prefix + this.#count++; + } + + namespace() { + return new NameRegistry(this.#prefix + this.#count++ + "-"); + } +} + // CONSTRUCTORS ---------------------------------------------------------------- export const engine = () => @@ -323,23 +338,22 @@ export const suspend = (engine) => engine.suspend(); export const close = (engine) => engine.close(); -export const commit = (engine, vnodes) => engine.commit(vnodes); +export const commit = (engine, to_audio_graph) => { + const registry = new NameRegistry(); + const vnodes = to_audio_graph(registry); + + return engine.commit(vnodes); +}; + +export const name = (registry) => registry.name(); + +export const namespace = (registry) => registry.namespace(); // UTILS ----------------------------------------------------------------------- -const iterate = (collection, callback) => { - if (Array.isArray(collection)) { - for (let i = 0; i < collection.length; i++) { - callback(collection[i]); - } - } else { - for ( - collection; - List$NonEmpty$rest(collection); - collection = List$NonEmpty$rest(collection) - ) { - callback(List$NonEmpty$first(collection)); - } +const iterate = (list, callback) => { + for (list; List$NonEmpty$rest(list); list = List$NonEmpty$rest(list)) { + callback(List$NonEmpty$first(list)); } }; diff --git a/src/beep.gleam b/src/beep.gleam index 854866a..c5fd334 100644 --- a/src/beep.gleam +++ b/src/beep.gleam @@ -40,6 +40,15 @@ pub type AudioEngineState { Suspended } +pub type NameRegistry = + internal.NameRegistry + +pub type Name = + internal.Name + +pub type Namespace = + internal.Namespace + // CONSTRUCTORS ---------------------------------------------------------------- /// Construct a new [`AudioEngine`](#AudioEngine) instance. @@ -61,8 +70,8 @@ pub fn node( /// /// -pub fn keyed( - name: String, +pub fn named( + name: Name, kind: String, properties: List(Property), connections: List(AudioNode), @@ -70,25 +79,25 @@ pub fn keyed( AudioNode(name: Some(name), kind:, properties:, connections:) } -/// Create a connection to a [keyed node](#keyed) elsewhere in the audio graph. +/// Create a connection to a [named node](#named) elsewhere in the audio graph. /// Cycles are allowed – feedback loops are a very important part of audio synthesis /// – but be careful to avoid loops that run away to infinity and explode your /// speakers! /// /// -pub fn connection(to name: String) -> AudioNode { +pub fn connection(to name: Name) -> AudioNode { Connection(name:, param: None) } /// Like [`connection`](#connection), this function lets you declare a connection -/// to a [keyed node](#keyed) somewhere else in the audio graph. The second parameter +/// to a [named node](#named) somewhere else in the audio graph. The second parameter /// is the name of the [AudioParam](https://developer.mozilla.org/en-US/docs/Web/API/AudioParam) /// on the target node to connect to. /// /// This lets the output of an audio node directly control a parameter like the /// frequency of an oscillator: that's called modulation! /// -pub fn modulation(name: String, param: String) -> AudioNode { +pub fn modulation(name: Name, param: String) -> AudioNode { Connection(name:, param: Some(param)) } @@ -121,6 +130,24 @@ pub fn property(name: String, value: Dynamic) -> Property { NodeProperty(name:, value:) } +/// +/// +pub fn name(registry: NameRegistry) -> Name { + internal.name(registry) +} + +/// +/// +pub fn namespace(registry: NameRegistry) -> Namespace { + internal.namespace(registry) +} + +/// +/// +pub fn namespaced_name(namespace: Namespace) -> Name { + internal.namespaced_name(namespace) +} + // AUDIO NODES ----------------------------------------------------------------- pub fn oscillator( @@ -223,7 +250,10 @@ pub fn current_time(_context: AudioEngine) -> Float { // MANIPULATIONS --------------------------------------------------------------- @external(javascript, "./beep.ffi.mjs", "commit") -pub fn commit(_context: AudioEngine, _nodes: List(AudioNode)) -> Nil { +pub fn commit( + _context: AudioEngine, + _callback: fn(NameRegistry) -> List(AudioNode), +) -> Nil { Nil } diff --git a/src/beep/internal.gleam b/src/beep/internal.gleam index 048a9e2..b68e7d0 100644 --- a/src/beep/internal.gleam +++ b/src/beep/internal.gleam @@ -11,12 +11,12 @@ import gleam/set.{type Set} pub type AudioNode { AudioNode( - name: Option(String), + name: Option(Name), kind: String, properties: List(Property), connections: List(AudioNode), ) - Connection(name: String, param: Option(String)) + Connection(name: Name, param: Option(String)) } pub type Property { @@ -25,30 +25,38 @@ pub type Property { } pub type Patch { - NodeCreated(name: String, kind: String, properties: List(Property)) - NodeAudioParamUpdated(node: String, name: String, value: Float) - NodePropertyUpdated(node: String, name: String, value: Dynamic) - NodeRemoved(name: String) - ConnectionCreated(from: String, to: String, param: Option(String)) - ConnectionRemoved(from: String, to: String, param: Option(String)) + NodeCreated(name: Name, kind: String, properties: List(Property)) + NodeAudioParamUpdated(node: Name, name: String, value: Float) + NodePropertyUpdated(node: Name, name: String, value: Dynamic) + NodeRemoved(name: Name) + ConnectionCreated(from: Name, to: Name, param: Option(String)) + ConnectionRemoved(from: Name, to: Name, param: Option(String)) } +pub type NameRegistry + +pub type Name + +pub type Namespace + // pub fn diff(prev: List(AudioNode), next: List(AudioNode)) -> List(Patch) { - do_diff([], prev, dict.new(), next, dict.new(), "", 0, False, set.new()) + let root = root_name("") + + do_diff([], prev, dict.new(), next, dict.new(), root, 0, False, set.new()) } fn do_diff( patches: List(Patch), prev: List(AudioNode), - prev_keyed: Dict(String, AudioNode), + prev_keyed: Dict(Name, AudioNode), next: List(AudioNode), - next_keyed: Dict(String, AudioNode), - path: String, + next_keyed: Dict(Name, AudioNode), + path: Name, index: Int, keyed: Bool, - seen: Set(String), + seen: Set(Name), ) -> List(Patch) { case prev, next { [], [] -> patches @@ -201,7 +209,7 @@ fn do_diff( let index = increment(old.name, index) patches - |> do_update_diff(new.properties, key) + |> do_update_diff(old.properties, new.properties, key) |> do_diff(prev, prev_keyed, next, next_keyed, path, index, keyed, seen) |> do_diff( old.connections, @@ -266,7 +274,7 @@ fn do_diff( fn do_insert_diff( patches: List(Patch), new: AudioNode, - path: String, + path: Name, index: Int, ) -> List(Patch) { case new { @@ -286,7 +294,7 @@ fn do_insert_diff( fn do_insert_children( patches: List(Patch), children: List(AudioNode), - path: String, + path: Name, index: Int, ) -> List(Patch) { case children { @@ -313,24 +321,37 @@ fn do_insert_children( fn do_update_diff( patches: List(Patch), + prev: List(Property), next: List(Property), - node: String, + node: Name, ) -> List(Patch) { - case next { - [] -> patches + case prev, next { + _, [] -> patches + + [AudioParam(..) as old, ..prev], [AudioParam(..) as new, ..next] + if old.name == new.name && old.value == new.value + -> do_update_diff(patches, prev, next, node) - [AudioParam(..) as new, ..next] -> { + [_, ..prev], [AudioParam(..) as new, ..next] + | [] as prev, [AudioParam(..) as new, ..next] + -> { let patch = NodeAudioParamUpdated(node:, name: new.name, value: new.value) let patches = [patch, ..patches] - do_update_diff(patches, next, node) + do_update_diff(patches, prev, next, node) } - [NodeProperty(..) as new, ..next] -> { + [NodeProperty(..) as old, ..prev], [NodeProperty(..) as new, ..next] + if old.name == new.name && old.value == new.value + -> do_update_diff(patches, prev, next, node) + + [_, ..prev], [NodeProperty(..) as new, ..next] + | [] as prev, [NodeProperty(..) as new, ..next] + -> { let patch = NodePropertyUpdated(node:, name: new.name, value: new.value) let patches = [patch, ..patches] - do_update_diff(patches, next, node) + do_update_diff(patches, prev, next, node) } } } @@ -338,7 +359,7 @@ fn do_update_diff( fn do_remove_diff( patches: List(Patch), old: AudioNode, - path: String, + path: Name, index: Int, ) -> List(Patch) { case old { @@ -359,9 +380,9 @@ fn do_remove_diff( fn build_keyed_lookup( prev: List(AudioNode), next: List(AudioNode), - path: String, + path: Name, index: Int, -) -> #(Dict(String, AudioNode), Dict(String, AudioNode)) { +) -> #(Dict(Name, AudioNode), Dict(Name, AudioNode)) { let prev_keyed = dict.new() let next_keyed = dict.new() @@ -370,12 +391,12 @@ fn build_keyed_lookup( fn do_build_keyed_lookup( prev: List(AudioNode), - prev_keyed: Dict(String, AudioNode), + prev_keyed: Dict(Name, AudioNode), next: List(AudioNode), - next_keyed: Dict(String, AudioNode), - path: String, + next_keyed: Dict(Name, AudioNode), + path: Name, index: Int, -) -> #(Dict(String, AudioNode), Dict(String, AudioNode)) { +) -> #(Dict(Name, AudioNode), Dict(Name, AudioNode)) { case prev, next { [], [] | [], [_, ..] | [_, ..], [] -> #(prev_keyed, next_keyed) @@ -429,16 +450,33 @@ fn do_build_keyed_lookup( // -fn key(name: Option(String), path: String, index: Int) -> String { +fn key(name: Option(Name), path: Name, index: Int) -> Name { case name { Some(name) -> name - None -> path <> "." <> int.to_string(index) + None -> internal_name(path, "." <> int.to_string(index)) } } -fn increment(name: Option(String), index: Int) -> Int { +fn increment(name: Option(Name), index: Int) -> Int { case name { Some(_) -> index None -> index + 1 } } + +// + +@external(javascript, "../beep.ffi.mjs", "name") +pub fn name(registry: NameRegistry) -> Name + +@external(javascript, "../beep.ffi.mjs", "name") +pub fn namespaced_name(namespace: Namespace) -> Name + +@external(javascript, "../../gleam_stdlib/gleam/string.mjs", "append") +fn internal_name(prefix: Name, suffix: String) -> Name + +@external(javascript, "../../gleam_stdlib/gleam/function.mjs", "identity") +fn root_name(value: String) -> Name + +@external(javascript, "../beep.ffi.mjs", "namespace") +pub fn namespace(registry: NameRegistry) -> Namespace diff --git a/src/beep/named.gleam b/src/beep/named.gleam index 7068e0e..9b3a32d 100644 --- a/src/beep/named.gleam +++ b/src/beep/named.gleam @@ -1,146 +1,113 @@ // IMPORTS --------------------------------------------------------------------- -import beep.{type AudioNode, type Property} +import beep.{type AudioNode, type Name, type Property} // CONSTRUCTORS ---------------------------------------------------------------- /// /// pub fn oscillator( - name: String, + name: Name, properties: List(Property), connections: List(AudioNode), ) -> AudioNode { - case name { - "" -> beep.oscillator(properties, connections) - name -> beep.keyed(name, "OscillatorNode", properties, connections) - } + beep.named(name, "OscillatorNode", properties, connections) } /// /// pub fn gain( - name: String, + name: Name, properties: List(Property), connections: List(AudioNode), ) -> AudioNode { - case name { - "" -> beep.gain(properties, connections) - name -> beep.keyed(name, "GainNode", properties, connections) - } + beep.named(name, "GainNode", properties, connections) } pub fn analyser( - name: String, + name: Name, properties: List(Property), connections: List(AudioNode), ) -> AudioNode { - case name { - "" -> beep.analyser(properties, connections) - name -> beep.keyed(name, "AnalyserNode", properties, connections) - } + beep.named(name, "AnalyserNode", properties, connections) } /// /// pub fn biquad_filter( - name: String, + name: Name, properties: List(Property), connections: List(AudioNode), ) -> AudioNode { - case name { - "" -> beep.biquad_filter(properties, connections) - name -> beep.keyed(name, "BiquadFilterNode", properties, connections) - } + beep.named(name, "BiquadFilterNode", properties, connections) } /// /// pub fn channel_merger( - name: String, + name: Name, properties: List(Property), connections: List(AudioNode), ) -> AudioNode { - case name { - "" -> beep.channel_merger(properties, connections) - name -> beep.keyed(name, "ChannelMergerNode", properties, connections) - } + beep.named(name, "ChannelMergerNode", properties, connections) } /// /// pub fn channel_splitter( - name: String, + name: Name, properties: List(Property), connections: List(AudioNode), ) -> AudioNode { - case name { - "" -> beep.channel_splitter(properties, connections) - name -> beep.keyed(name, "ChannelSplitterNode", properties, connections) - } + beep.named(name, "ChannelSplitterNode", properties, connections) } /// /// pub fn constant_source( - name: String, + name: Name, properties: List(Property), connections: List(AudioNode), ) -> AudioNode { - case name { - "" -> beep.constant_source(properties, connections) - name -> beep.keyed(name, "ConstantSourceNode", properties, connections) - } + beep.named(name, "ConstantSourceNode", properties, connections) } /// /// pub fn delay( - name: String, + name: Name, properties: List(Property), connections: List(AudioNode), ) -> AudioNode { - case name { - "" -> beep.delay(properties, connections) - name -> beep.keyed(name, "DelayNode", properties, connections) - } + beep.named(name, "DelayNode", properties, connections) } /// /// pub fn dynamics_compressor( - name: String, + name: Name, properties: List(Property), connections: List(AudioNode), ) -> AudioNode { - case name { - "" -> beep.dynamics_compressor(properties, connections) - name -> beep.keyed(name, "DynamicsCompressorNode", properties, connections) - } + beep.named(name, "DynamicsCompressorNode", properties, connections) } /// /// pub fn panner( - name: String, + name: Name, properties: List(Property), connections: List(AudioNode), ) -> AudioNode { - case name { - "" -> beep.panner(properties, connections) - name -> beep.keyed(name, "PannerNode", properties, connections) - } + beep.named(name, "PannerNode", properties, connections) } /// /// pub fn stereo_panner( - name: String, + name: Name, properties: List(Property), connections: List(AudioNode), ) -> AudioNode { - case name { - "" -> beep.stereo_panner(properties, connections) - name -> beep.keyed(name, "StereoPannerNode", properties, connections) - } + beep.named(name, "StereoPannerNode", properties, connections) }