insert effect
This commit is contained in:
Generated
+34
-34
@@ -18,7 +18,7 @@
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"lerna": "^8.2.3",
|
||||
"prettier": "^3.6.2",
|
||||
"turbo": "^2.5.4",
|
||||
"turbo": "^2.5.5",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.1.3"
|
||||
}
|
||||
@@ -5069,9 +5069,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
|
||||
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10156,27 +10156,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/turbo": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/turbo/-/turbo-2.5.4.tgz",
|
||||
"integrity": "sha512-kc8ZibdRcuWUG1pbYSBFWqmIjynlD8Lp7IB6U3vIzvOv9VG+6Sp8bzyeBWE3Oi8XV5KsQrznyRTBPvrf99E4mA==",
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/turbo/-/turbo-2.5.5.tgz",
|
||||
"integrity": "sha512-eZ7wI6KjtT1eBqCnh2JPXWNUAxtoxxfi6VdBdZFvil0ychCOTxbm7YLRBi1JSt7U3c+u3CLxpoPxLdvr/Npr3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"turbo": "bin/turbo"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"turbo-darwin-64": "2.5.4",
|
||||
"turbo-darwin-arm64": "2.5.4",
|
||||
"turbo-linux-64": "2.5.4",
|
||||
"turbo-linux-arm64": "2.5.4",
|
||||
"turbo-windows-64": "2.5.4",
|
||||
"turbo-windows-arm64": "2.5.4"
|
||||
"turbo-darwin-64": "2.5.5",
|
||||
"turbo-darwin-arm64": "2.5.5",
|
||||
"turbo-linux-64": "2.5.5",
|
||||
"turbo-linux-arm64": "2.5.5",
|
||||
"turbo-windows-64": "2.5.5",
|
||||
"turbo-windows-arm64": "2.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/turbo-darwin-64": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.5.4.tgz",
|
||||
"integrity": "sha512-ah6YnH2dErojhFooxEzmvsoZQTMImaruZhFPfMKPBq8sb+hALRdvBNLqfc8NWlZq576FkfRZ/MSi4SHvVFT9PQ==",
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.5.5.tgz",
|
||||
"integrity": "sha512-RYnTz49u4F5tDD2SUwwtlynABNBAfbyT2uU/brJcyh5k6lDLyNfYKdKmqd3K2ls4AaiALWrFKVSBsiVwhdFNzQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -10188,9 +10188,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/turbo-darwin-arm64": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-2.5.4.tgz",
|
||||
"integrity": "sha512-2+Nx6LAyuXw2MdXb7pxqle3MYignLvS7OwtsP9SgtSBaMlnNlxl9BovzqdYAgkUW3AsYiQMJ/wBRb7d+xemM5A==",
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-2.5.5.tgz",
|
||||
"integrity": "sha512-Tk+ZeSNdBobZiMw9aFypQt0DlLsWSFWu1ymqsAdJLuPoAH05qCfYtRxE1pJuYHcJB5pqI+/HOxtJoQ40726Btw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -10202,9 +10202,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/turbo-linux-64": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.5.4.tgz",
|
||||
"integrity": "sha512-5May2kjWbc8w4XxswGAl74GZ5eM4Gr6IiroqdLhXeXyfvWEdm2mFYCSWOzz0/z5cAgqyGidF1jt1qzUR8hTmOA==",
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.5.5.tgz",
|
||||
"integrity": "sha512-2/XvMGykD7VgsvWesZZYIIVXMlgBcQy+ZAryjugoTcvJv8TZzSU/B1nShcA7IAjZ0q7OsZ45uP2cOb8EgKT30w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -10216,9 +10216,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/turbo-linux-arm64": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.5.4.tgz",
|
||||
"integrity": "sha512-/2yqFaS3TbfxV3P5yG2JUI79P7OUQKOUvAnx4MV9Bdz6jqHsHwc9WZPpO4QseQm+NvmgY6ICORnoVPODxGUiJg==",
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.5.5.tgz",
|
||||
"integrity": "sha512-DW+8CjCjybu0d7TFm9dovTTVg1VRnlkZ1rceO4zqsaLrit3DgHnN4to4uwyuf9s2V/BwS3IYcRy+HG9BL596Iw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -10230,9 +10230,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/turbo-windows-64": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.5.4.tgz",
|
||||
"integrity": "sha512-EQUO4SmaCDhO6zYohxIjJpOKRN3wlfU7jMAj3CgcyTPvQR/UFLEKAYHqJOnJtymbQmiiM/ihX6c6W6Uq0yC7mA==",
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.5.5.tgz",
|
||||
"integrity": "sha512-q5p1BOy8ChtSZfULuF1BhFMYIx6bevXu4fJ+TE/hyNfyHJIfjl90Z6jWdqAlyaFLmn99X/uw+7d6T/Y/dr5JwQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -10244,9 +10244,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/turbo-windows-arm64": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.5.4.tgz",
|
||||
"integrity": "sha512-oQ8RrK1VS8lrxkLriotFq+PiF7iiGgkZtfLKF4DDKsmdbPo0O9R2mQxm7jHLuXraRCuIQDWMIw6dpcr7Iykf4A==",
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.5.5.tgz",
|
||||
"integrity": "sha512-AXbF1KmpHUq3PKQwddMGoKMYhHsy5t1YBQO8HZ04HLMR0rWv9adYlQ8kaeQJTko1Ay1anOBFTqaxfVOOsu7+1Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -10437,9 +10437,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz",
|
||||
"integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==",
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz",
|
||||
"integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"lerna": "^8.2.3",
|
||||
"prettier": "^3.6.2",
|
||||
"turbo": "^2.5.4",
|
||||
"turbo": "^2.5.5",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.1.3"
|
||||
},
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
import {PPQN} from "@opendaw/lib-dsp"
|
||||
import {EffectFactories, InstrumentFactories, Project, ProjectEnv} from "@opendaw/studio-core"
|
||||
import {AudioUnitBoxAdapter} from "@opendaw/studio-adapters"
|
||||
|
||||
const {Bar, Quarter} = PPQN
|
||||
|
||||
export const createExampleProject = (env: ProjectEnv): Project => {
|
||||
const project = Project.new(env)
|
||||
const {api, boxAdapters, editing} = project
|
||||
const result = editing.modify(() => {
|
||||
const {api, editing} = project
|
||||
// @ts-ignore
|
||||
const {boxA, boxB, boxC} = editing.modify(() => {
|
||||
const {to} = project.timelineBoxAdapter.box.loopArea
|
||||
to.setValue(Bar)
|
||||
const {trackBox, audioUnitBox} = api.createInstrument(InstrumentFactories.Vaporisateur)
|
||||
@@ -24,15 +24,11 @@ export const createExampleProject = (env: ProjectEnv): Project => {
|
||||
api.createNoteEvent({owner: noteRegionBox, position: 0, duration: Quarter, pitch: 67})
|
||||
api.createNoteEvent({owner: noteRegionBox, position: 0, duration: Quarter, pitch: 72})
|
||||
|
||||
const boxA = api.createEffect(boxAdapters.adapterFor(audioUnitBox, AudioUnitBoxAdapter), EffectFactories.Arpeggio, 0)
|
||||
const boxB = api.createEffect(boxAdapters.adapterFor(audioUnitBox, AudioUnitBoxAdapter), EffectFactories.Pitch, 0)
|
||||
return {boxA, boxB}
|
||||
const boxA = api.insertEffect(audioUnitBox.midiEffects, EffectFactories.Arpeggio, 0)
|
||||
const boxB = api.insertEffect(audioUnitBox.midiEffects, EffectFactories.Pitch, 1)
|
||||
const boxC = api.insertEffect(audioUnitBox.midiEffects, EffectFactories.Zeitgeist, 2)
|
||||
return {boxA, boxB, boxC}
|
||||
}).unwrap()
|
||||
|
||||
const {boxA, boxB} = result
|
||||
|
||||
console.debug(boxA)
|
||||
console.debug(boxB)
|
||||
|
||||
return project
|
||||
}
|
||||
@@ -16,6 +16,7 @@ module.exports = {
|
||||
rules: {
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", {"argsIgnorePattern": "^_"}],
|
||||
"no-unused-vars": "warn",
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
|
||||
@@ -248,7 +248,7 @@ describe("gen (create/navigate)", () => {
|
||||
expect(fooBox.incomingEdges().length).toBe(0)
|
||||
graph.beginTransaction()
|
||||
barBox.ref.refer(fooBox)
|
||||
expect(barBox.outgoingEdges()[0]).toStrictEqual([barBox.ref, fooBox])
|
||||
expect(barBox.outgoingEdges()[0]).toStrictEqual([barBox.ref, fooBox.ref.address])
|
||||
graph.endTransaction()
|
||||
})
|
||||
it("isDetached after deletion", () => {
|
||||
|
||||
@@ -106,7 +106,7 @@ export abstract class Box<P extends PointerTypes = PointerTypes, F extends Field
|
||||
}
|
||||
|
||||
incomingEdges(): ReadonlyArray<PointerField> {return this.graph.edges().incomingEdgesOf(this)}
|
||||
outgoingEdges(): ReadonlyArray<[PointerField, Vertex]> {return this.graph.edges().outgoingEdgesOf(this)}
|
||||
outgoingEdges(): ReadonlyArray<[PointerField, Address]> {return this.graph.edges().outgoingEdgesOf(this)}
|
||||
|
||||
mapFields<T>(map: Func<Field, T>, ...keys: FieldKey[]): ReadonlyArray<T> {
|
||||
if (keys.length === 0) {return Arrays.empty()}
|
||||
|
||||
@@ -7,14 +7,14 @@ import {Box} from "./box"
|
||||
export class GraphEdges {
|
||||
readonly #requiresTarget: SortedSet<Address, PointerField>
|
||||
readonly #requiresPointer: SortedSet<Address, Vertex>
|
||||
readonly #incoming: SortedSet<Address, [Vertex, Array<PointerField>]>
|
||||
readonly #outgoing: SortedSet<Address, [PointerField, Vertex]>
|
||||
readonly #incoming: SortedSet<Address, [Address, Array<PointerField>]>
|
||||
readonly #outgoing: SortedSet<Address, [PointerField, Address]>
|
||||
|
||||
constructor() {
|
||||
this.#requiresTarget = Address.newSet<PointerField>(source => source.address)
|
||||
this.#requiresPointer = Address.newSet<Vertex>(vertex => vertex.address)
|
||||
this.#incoming = Address.newSet<[Vertex, Array<PointerField>]>(([vertex]) => vertex.address)
|
||||
this.#outgoing = Address.newSet<[PointerField, Vertex]>(([source]) => source.address)
|
||||
this.#incoming = Address.newSet<[Address, Array<PointerField>]>(([address]) => address)
|
||||
this.#outgoing = Address.newSet<[PointerField, Address]>(([source]) => source.address)
|
||||
}
|
||||
|
||||
watchVertex(vertex: Vertex | PointerField): void {
|
||||
@@ -51,9 +51,9 @@ export class GraphEdges {
|
||||
}
|
||||
}
|
||||
|
||||
connect(source: PointerField, target: Vertex): void {
|
||||
connect(source: PointerField, target: Address): void {
|
||||
this.#outgoing.add([source, target])
|
||||
this.#incoming.opt(target.address).match<void>({
|
||||
this.#incoming.opt(target).match<void>({
|
||||
none: () => this.#incoming.add([target, [source]]),
|
||||
some: ([, sources]) => sources.push(source)
|
||||
})
|
||||
@@ -61,18 +61,18 @@ export class GraphEdges {
|
||||
|
||||
disconnect(source: PointerField): void {
|
||||
const [, target] = this.#outgoing.removeByKey(source.address)
|
||||
const [, sources] = this.#incoming.get(target.address)
|
||||
const [, sources] = this.#incoming.get(target)
|
||||
Arrays.remove(sources, source)
|
||||
if (sources.length === 0) {this.#incoming.removeByKey(target.address)}
|
||||
if (sources.length === 0) {this.#incoming.removeByKey(target)}
|
||||
}
|
||||
|
||||
outgoingEdgesOf(box: Box): ReadonlyArray<[PointerField, Vertex]> {
|
||||
outgoingEdgesOf(box: Box): ReadonlyArray<[PointerField, Address]> {
|
||||
return this.#collectSameBox(this.#outgoing, box.address.uuid, ([{box: {address: {uuid}}}]) => uuid)
|
||||
}
|
||||
|
||||
incomingEdgesOf(vertex: Box | Vertex): ReadonlyArray<PointerField> {
|
||||
if (vertex.isBox()) {
|
||||
return this.#collectSameBox(this.#incoming, vertex.address.uuid, ([{address: {uuid}}]) => uuid)
|
||||
return this.#collectSameBox(this.#incoming, vertex.address.uuid, ([{uuid}]) => uuid)
|
||||
.flatMap(([_, pointers]) => pointers)
|
||||
} else {
|
||||
return this.#incoming.opt(vertex.address).mapOr(([_, pointers]) => pointers, Arrays.empty())
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
int,
|
||||
isDefined,
|
||||
Listeners,
|
||||
Nullable,
|
||||
Option,
|
||||
panic,
|
||||
Procedure,
|
||||
@@ -78,7 +77,7 @@ export class BoxGraph<BoxMap = any> {
|
||||
this.#inTransaction = false
|
||||
if (this.#deferredPointerUpdates.length > 0) {
|
||||
this.#deferredPointerUpdates.forEach(({pointerField, update}) =>
|
||||
this.#processPointerValueUpdate(pointerField, update))
|
||||
this.#processPointerVertexUpdate(pointerField, update))
|
||||
this.#deferredPointerUpdates.length = 0
|
||||
}
|
||||
// it is possible that new observers will be added while executing
|
||||
@@ -172,29 +171,25 @@ export class BoxGraph<BoxMap = any> {
|
||||
|
||||
onPointerAddressUpdated(pointerField: PointerField, oldValue: Option<Address>, newValue: Option<Address>): void {
|
||||
this.#assertTransaction()
|
||||
if (oldValue.nonEmpty()) {this.#edges.disconnect(pointerField)}
|
||||
if (newValue.nonEmpty()) {this.#edges.connect(pointerField, newValue.unwrap())}
|
||||
const update = new PointerUpdate(pointerField.address, oldValue, newValue)
|
||||
if (this.#constructingBox) {
|
||||
this.#deferredPointerUpdates.push({pointerField, update})
|
||||
} else {
|
||||
this.#processPointerValueUpdate(pointerField, update)
|
||||
this.#processPointerVertexUpdate(pointerField, update)
|
||||
this.#immediateUpdateListeners.proxy.onUpdate(update)
|
||||
}
|
||||
}
|
||||
|
||||
#processPointerValueUpdate(pointerField: PointerField, update: PointerUpdate): void {
|
||||
const {oldValue} = update
|
||||
const oldVertex: Nullable<Vertex> = oldValue.flatMap(address => this.findVertex(address)).unwrapOrNull()
|
||||
pointerField.resolve()
|
||||
const newVertex: Nullable<Vertex> = pointerField.targetVertex.unwrapOrNull()
|
||||
if (oldVertex !== newVertex) {
|
||||
oldVertex?.pointerHub.onRemoved(pointerField)
|
||||
newVertex?.pointerHub.onAdded(pointerField)
|
||||
if (oldVertex !== null) {
|
||||
this.#edges.disconnect(pointerField)
|
||||
}
|
||||
if (newVertex !== null) {
|
||||
this.#edges.connect(pointerField, newVertex)
|
||||
}
|
||||
#processPointerVertexUpdate(pointerField: PointerField, update: PointerUpdate): void {
|
||||
const {oldAddress, newAddress} = update
|
||||
const oldVertex = oldAddress.flatMap(address => this.findVertex(address))
|
||||
const newVertex = newAddress.flatMap(address => this.findVertex(address))
|
||||
pointerField.resolvedTo(newVertex)
|
||||
if (!oldVertex.equals(newVertex)) {
|
||||
oldVertex.ifSome(vertex => vertex.pointerHub.onRemoved(pointerField))
|
||||
newVertex.ifSome(vertex => vertex.pointerHub.onAdded(pointerField))
|
||||
}
|
||||
this.#dispatchers.dispatch(update)
|
||||
this.#updateListeners.proxy.onUpdate(update)
|
||||
@@ -208,11 +203,13 @@ export class BoxGraph<BoxMap = any> {
|
||||
boxes.add(box)
|
||||
box.outgoingEdges()
|
||||
.filter(([pointer]) => !pointers.has(pointer))
|
||||
.forEach(([source, target]: [PointerField, Vertex]) => {
|
||||
.forEach(([source, targetAddress]: [PointerField, Address]) => {
|
||||
const targetVertex = this.findVertex(targetAddress)
|
||||
.unwrap(`Could not find target of ${source.toString()}`)
|
||||
pointers.add(source)
|
||||
if (target.pointerRules.mandatory &&
|
||||
target.pointerHub.incoming().every(pointer => pointers.has(pointer))) {
|
||||
return trace(target.box)
|
||||
if (targetVertex.pointerRules.mandatory &&
|
||||
targetVertex.pointerHub.incoming().every(pointer => pointers.has(pointer))) {
|
||||
return trace(targetVertex.box)
|
||||
}
|
||||
})
|
||||
box.incomingEdges()
|
||||
|
||||
@@ -23,4 +23,5 @@ export * from "./sync"
|
||||
export * from "./sync-source"
|
||||
export * from "./sync-target"
|
||||
export * from "./updates"
|
||||
export * from "./utils"
|
||||
export * from "./vertex"
|
||||
@@ -8,7 +8,9 @@ export interface PointerListener {
|
||||
onRemove(pointer: PointerField): void
|
||||
}
|
||||
|
||||
export class PointerHub implements Iterable<PointerField> {
|
||||
type ChangeLog = Array<{ type: "add" | "remove", pointerField: PointerField }>
|
||||
|
||||
export class PointerHub {
|
||||
static validate(pointer: PointerField, target: Vertex): Option<string> {
|
||||
if (pointer.address.equals(target.address)) {
|
||||
return Option.wrap(`PointerField cannot point to itself: ${pointer}`)
|
||||
@@ -25,7 +27,7 @@ export class PointerHub implements Iterable<PointerField> {
|
||||
readonly #immediateListeners: Listeners<PointerListener>
|
||||
readonly #transactualListeners: Listeners<PointerListener>
|
||||
|
||||
#inTransaction: Option<Set<PointerField>> = Option.None
|
||||
#inTransaction: Option<ChangeLog> = Option.None
|
||||
|
||||
constructor(vertex: Vertex) {
|
||||
this.#vertex = vertex
|
||||
@@ -46,9 +48,9 @@ export class PointerHub implements Iterable<PointerField> {
|
||||
const added: SortedSet<Address, PointerField> = Address.newSet(pointer => pointer.address)
|
||||
added.addMany(this.filter(...filter))
|
||||
added.forEach(pointer => listener.onAdd(pointer))
|
||||
// This takes track of the listeners notification state.
|
||||
// This takes track of the listener notification state.
|
||||
// It is possible that the pointer has been added, but it has not been notified yet.
|
||||
// That would cause the listener.onAdd method to be involked twice.
|
||||
// That would cause the listener.onAdd method to be invoked twice.
|
||||
return this.subscribeTransactual({
|
||||
onAdd: (pointer: PointerField) => {
|
||||
if (added.add(pointer)) {
|
||||
@@ -63,8 +65,9 @@ export class PointerHub implements Iterable<PointerField> {
|
||||
}
|
||||
|
||||
filter<P extends PointerTypes>(...types: ReadonlyArray<P>): Array<PointerField<P>> {
|
||||
return (types.length === 0 ? this.incoming() : Iterables.filter(this, (pointerField: PointerField) =>
|
||||
types.some((type: P) => type === pointerField.pointerType))) as Array<PointerField<P>>
|
||||
return (types.length === 0 ? this.incoming() : Iterables.filter(this.incoming().values(),
|
||||
(pointerField: PointerField) => types.some((type: P) =>
|
||||
type === pointerField.pointerType))) as Array<PointerField<P>>
|
||||
}
|
||||
|
||||
size(): int {return this.incoming().length}
|
||||
@@ -78,7 +81,9 @@ export class PointerHub implements Iterable<PointerField> {
|
||||
if (issue.nonEmpty()) {return panic(issue.unwrap())}
|
||||
if (this.#inTransaction.isEmpty()) {
|
||||
this.#vertex.graph.subscribeEndTransaction(this.#onEndTransaction)
|
||||
this.#inTransaction = Option.wrap(new Set(this))
|
||||
this.#inTransaction = Option.wrap([{type: "add", pointerField}])
|
||||
} else {
|
||||
this.#inTransaction.unwrap().push({type: "add", pointerField})
|
||||
}
|
||||
this.#immediateListeners.proxy.onAdd(pointerField)
|
||||
}
|
||||
@@ -86,15 +91,15 @@ export class PointerHub implements Iterable<PointerField> {
|
||||
onRemoved(pointerField: PointerField): void {
|
||||
if (this.#inTransaction.isEmpty()) {
|
||||
this.#vertex.graph.subscribeEndTransaction(this.#onEndTransaction)
|
||||
this.#inTransaction = Option.wrap(new Set(this))
|
||||
this.#inTransaction = Option.wrap([{type: "remove", pointerField}])
|
||||
} else {
|
||||
this.#inTransaction.unwrap().push({type: "remove", pointerField})
|
||||
}
|
||||
this.#immediateListeners.proxy.onRemove(pointerField)
|
||||
}
|
||||
|
||||
[Symbol.iterator](): Iterator<PointerField> {return this.incoming().values()}
|
||||
|
||||
toString(): string {
|
||||
return `{Pointers ${this.#vertex.address}, pointers: ${Array.from(this)
|
||||
return `{Pointers ${this.#vertex.address}, pointers: ${this.incoming().values()
|
||||
.map((pointerField: PointerField) => pointerField.toString())}}`
|
||||
}
|
||||
|
||||
@@ -117,18 +122,17 @@ export class PointerHub implements Iterable<PointerField> {
|
||||
|
||||
readonly #onEndTransaction: Exec = (): void => {
|
||||
if (this.#vertex.isAttached()) {
|
||||
const beforeState: Set<PointerField> = this.#inTransaction.unwrap("Callback without transaction")
|
||||
const afterState: Set<PointerField> = new Set(this)
|
||||
beforeState.forEach(pointer => {
|
||||
if (!afterState.has(pointer)) {
|
||||
this.#transactualListeners.proxy.onRemove(pointer)
|
||||
}
|
||||
})
|
||||
afterState.forEach(pointer => {
|
||||
if (!beforeState.has(pointer)) {
|
||||
this.#transactualListeners.proxy.onAdd(pointer)
|
||||
const log: ChangeLog = this.#inTransaction.unwrap("Callback without ChangeLog")
|
||||
log.forEach(({type, pointerField}) => {
|
||||
if (type === "add") {
|
||||
this.#transactualListeners.proxy.onAdd(pointerField)
|
||||
} else if (type === "remove") {
|
||||
this.#transactualListeners.proxy.onRemove(pointerField)
|
||||
} else {
|
||||
panic(`Unknown type: ${type}`)
|
||||
}
|
||||
})
|
||||
log.length = 0
|
||||
}
|
||||
this.#inTransaction = Option.None
|
||||
}
|
||||
|
||||
@@ -85,14 +85,7 @@ export class PointerField<P extends PointerTypes = PointerTypes> extends Field<U
|
||||
isEmpty(): boolean {return this.targetAddress.isEmpty()}
|
||||
nonEmpty(): boolean {return this.targetAddress.nonEmpty()}
|
||||
|
||||
resolve(): void {
|
||||
const targetAddress: Option<Address> = this.targetAddress
|
||||
const target: Option<Vertex> = targetAddress.flatMap((address: Address) => this.graph.findVertex(address))
|
||||
if (targetAddress.nonEmpty() && target.isEmpty()) {
|
||||
return panic(`${targetAddress.unwrapOrNull()?.toString()} could not be resolved`)
|
||||
}
|
||||
this.#targetVertex = target
|
||||
}
|
||||
resolvedTo(newTarget: Option<Vertex>): void {this.#targetVertex = newTarget}
|
||||
|
||||
read(input: DataInput) {
|
||||
this.targetAddress = input.readBoolean() ? Option.wrap(Address.read(input)) : Option.None
|
||||
|
||||
@@ -62,7 +62,7 @@ export class SyncSource<M> implements Terminable {
|
||||
updates.push({
|
||||
type: "update-pointer",
|
||||
address: update.address.decompose(),
|
||||
target: update.newValue.unwrapOrNull()?.decompose()
|
||||
target: update.newAddress.unwrapOrNull()?.decompose()
|
||||
})
|
||||
} else if (update.type === "delete") {
|
||||
updates.push({
|
||||
|
||||
@@ -139,23 +139,23 @@ export class PointerUpdate implements Modification {
|
||||
readonly type = "pointer"
|
||||
|
||||
readonly #address: Address
|
||||
readonly #oldValue: Option<Address>
|
||||
readonly #newValue: Option<Address>
|
||||
readonly #oldAddress: Option<Address>
|
||||
readonly #newAddress: Option<Address>
|
||||
|
||||
constructor(address: Address, oldValue: Option<Address>, newValue: Option<Address>) {
|
||||
constructor(address: Address, oldAddress: Option<Address>, newAddress: Option<Address>) {
|
||||
this.#address = address
|
||||
this.#oldValue = oldValue
|
||||
this.#newValue = newValue
|
||||
this.#oldAddress = oldAddress
|
||||
this.#newAddress = newAddress
|
||||
}
|
||||
|
||||
get address(): Address {return this.#address}
|
||||
get oldValue(): Option<Address> {return this.#oldValue}
|
||||
get newValue(): Option<Address> {return this.#newValue}
|
||||
get oldAddress(): Option<Address> {return this.#oldAddress}
|
||||
get newAddress(): Option<Address> {return this.#newAddress}
|
||||
|
||||
matches(field: PointerField): boolean {return field.address.equals(this.address)}
|
||||
|
||||
inverse(graph: BoxGraph): void {this.field(graph).targetAddress = this.#oldValue}
|
||||
forward(graph: BoxGraph): void {this.field(graph).targetAddress = this.#newValue}
|
||||
inverse(graph: BoxGraph): void {this.field(graph).targetAddress = this.#oldAddress}
|
||||
forward(graph: BoxGraph): void {this.field(graph).targetAddress = this.#newAddress}
|
||||
|
||||
field(graph: BoxGraph): PointerField {
|
||||
return graph.findVertex(this.#address)
|
||||
@@ -165,14 +165,14 @@ export class PointerUpdate implements Modification {
|
||||
write(output: DataOutput): void {
|
||||
output.writeString(this.type)
|
||||
this.#address.write(output)
|
||||
this.#oldValue.match({
|
||||
this.#oldAddress.match({
|
||||
none: () => output.writeBoolean(false),
|
||||
some: address => {
|
||||
output.writeBoolean(true)
|
||||
address.write(output)
|
||||
}
|
||||
})
|
||||
this.#newValue.match({
|
||||
this.#newAddress.match({
|
||||
none: () => output.writeBoolean(false),
|
||||
some: address => {
|
||||
output.writeBoolean(true)
|
||||
@@ -182,7 +182,7 @@ export class PointerUpdate implements Modification {
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `{PointerUpdate oldValue: ${this.#oldValue.unwrapOrNull()}, newValue: ${this.#newValue.unwrapOrNull()}`
|
||||
return `{PointerUpdate oldValue: ${this.#oldAddress.unwrapOrNull()}, newValue: ${this.#newAddress.unwrapOrNull()}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import {Field} from "./field"
|
||||
import {clamp, int, panic} from "@opendaw/lib-std"
|
||||
import {Int32Field} from "./primitive"
|
||||
|
||||
export namespace BoxUtils {
|
||||
/**
|
||||
* Helps to add an element to a field at a specified index, while ensuring existing indices are shifted accordingly.
|
||||
*
|
||||
* This function adjusts the indices of the elements in the field to accommodate a new element.
|
||||
* If the calculated index is less than the total number of indices, it shifts all following
|
||||
* indices by incrementing their value to maintain correct ordering.
|
||||
*
|
||||
* @param {Field} field - The target field where the new index is being inserted.
|
||||
* @param {int} [insertIndex=Number.MAX_SAFE_INTEGER] - The index at which to insert the new value.
|
||||
* Defaults to the maximum safe integer value.
|
||||
* @returns {int} The final index at which the element is to be inserted.
|
||||
* @throws Will throw an error if a box associated with the field does not contain a valid "index" field
|
||||
* or if the "index" field is not of type Int32Field.
|
||||
*/
|
||||
export const insert = (field: Field, insertIndex: int = Number.MAX_SAFE_INTEGER): int => {
|
||||
const indices = field.pointerHub.incoming()
|
||||
.map(({box}) => {
|
||||
return "index" in box && box["index"] instanceof Int32Field
|
||||
? box.index
|
||||
: panic(`${box} has no index field`)
|
||||
})
|
||||
.sort((a, b) => a.getValue() - b.getValue())
|
||||
const newIndex = clamp(insertIndex, 0, indices.length)
|
||||
if (newIndex < indices.length) {
|
||||
for (let index = newIndex; index < indices.length; index++) {
|
||||
indices[index].setValue(index + 1)
|
||||
}
|
||||
}
|
||||
return newIndex
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
ArpeggioDeviceBox,
|
||||
DelayDeviceBox,
|
||||
ModularDeviceBox,
|
||||
PitchDeviceBox,
|
||||
RevampDeviceBox,
|
||||
ReverbDeviceBox,
|
||||
StereoToolDeviceBox,
|
||||
ZeitgeistDeviceBox
|
||||
} from "@opendaw/studio-boxes"
|
||||
|
||||
export type EffectBox =
|
||||
| ArpeggioDeviceBox | PitchDeviceBox | ZeitgeistDeviceBox
|
||||
| DelayDeviceBox | ReverbDeviceBox | RevampDeviceBox | StereoToolDeviceBox | ModularDeviceBox
|
||||
@@ -50,7 +50,7 @@ export namespace EffectFactories {
|
||||
description: "Distorts space and time",
|
||||
separatorBefore: false,
|
||||
type: "midi",
|
||||
create: ({boxGraph, rootBoxAdapter}, unit, index) => {
|
||||
create: ({boxGraph, rootBoxAdapter}, unit, index): ZeitgeistDeviceBox => {
|
||||
const useGlobal = false // TODO First Zeitgeist should be true
|
||||
const shuffleBox = useGlobal
|
||||
? rootBoxAdapter.groove.box
|
||||
@@ -73,11 +73,12 @@ export namespace EffectFactories {
|
||||
description: "Computes a stereo transformation matrix with volume, panning, phase inversion and stereo width.",
|
||||
separatorBefore: false,
|
||||
type: "audio",
|
||||
create: ({boxGraph}, unit, index) => StereoToolDeviceBox.create(boxGraph, UUID.generate(), box => {
|
||||
box.label.setValue("Stereo Tool")
|
||||
box.index.setValue(index)
|
||||
box.host.refer(unit)
|
||||
})
|
||||
create: ({boxGraph}, unit, index): StereoToolDeviceBox =>
|
||||
StereoToolDeviceBox.create(boxGraph, UUID.generate(), box => {
|
||||
box.label.setValue("Stereo Tool")
|
||||
box.index.setValue(index)
|
||||
box.host.refer(unit)
|
||||
})
|
||||
}
|
||||
|
||||
export const Delay: EffectFactory = {
|
||||
@@ -86,11 +87,12 @@ export namespace EffectFactories {
|
||||
description: "Echoes the input signal with time-based repeats",
|
||||
separatorBefore: false,
|
||||
type: "audio",
|
||||
create: ({boxGraph}, unit, index) => DelayDeviceBox.create(boxGraph, UUID.generate(), box => {
|
||||
box.label.setValue("Delay")
|
||||
box.index.setValue(index)
|
||||
box.host.refer(unit)
|
||||
})
|
||||
create: ({boxGraph}, unit, index): DelayDeviceBox =>
|
||||
DelayDeviceBox.create(boxGraph, UUID.generate(), box => {
|
||||
box.label.setValue("Delay")
|
||||
box.index.setValue(index)
|
||||
box.host.refer(unit)
|
||||
})
|
||||
}
|
||||
|
||||
export const Reverb: EffectFactory = {
|
||||
@@ -99,12 +101,13 @@ export namespace EffectFactories {
|
||||
description: "Simulates space and depth with reflections",
|
||||
separatorBefore: false,
|
||||
type: "audio",
|
||||
create: ({boxGraph}, unit, index) => ReverbDeviceBox.create(boxGraph, UUID.generate(), box => {
|
||||
box.label.setValue("Reverb")
|
||||
box.preDelay.setInitValue(0.001)
|
||||
box.index.setValue(index)
|
||||
box.host.refer(unit)
|
||||
})
|
||||
create: ({boxGraph}, unit, index): ReverbDeviceBox =>
|
||||
ReverbDeviceBox.create(boxGraph, UUID.generate(), box => {
|
||||
box.label.setValue("Reverb")
|
||||
box.preDelay.setInitValue(0.001)
|
||||
box.index.setValue(index)
|
||||
box.host.refer(unit)
|
||||
})
|
||||
}
|
||||
|
||||
export const Revamp: EffectFactory = {
|
||||
@@ -113,31 +116,32 @@ export namespace EffectFactories {
|
||||
description: "Shapes the frequency balance of the sound",
|
||||
separatorBefore: false,
|
||||
type: "audio",
|
||||
create: ({boxGraph}, unit, index) => RevampDeviceBox.create(boxGraph, UUID.generate(), box => {
|
||||
box.label.setValue("Revamp")
|
||||
box.highPass.frequency.setInitValue(40.0)
|
||||
box.highPass.order.setInitValue(2)
|
||||
box.highPass.q.setInitValue(INVERSE_SQRT_2)
|
||||
box.highPass.enabled.setInitValue(true)
|
||||
box.lowShelf.frequency.setInitValue(80.0)
|
||||
box.lowShelf.gain.setInitValue(6)
|
||||
box.lowBell.frequency.setInitValue(120.0)
|
||||
box.lowBell.gain.setInitValue(6)
|
||||
box.lowBell.q.setInitValue(INVERSE_SQRT_2)
|
||||
box.midBell.frequency.setInitValue(640.0)
|
||||
box.midBell.q.setInitValue(INVERSE_SQRT_2)
|
||||
box.midBell.gain.setInitValue(6)
|
||||
box.highBell.frequency.setInitValue(3600.0)
|
||||
box.highBell.q.setInitValue(INVERSE_SQRT_2)
|
||||
box.highBell.gain.setInitValue(6)
|
||||
box.highShelf.frequency.setInitValue(10000.0)
|
||||
box.highShelf.gain.setInitValue(6)
|
||||
box.lowPass.frequency.setInitValue(15000.0)
|
||||
box.lowPass.order.setInitValue(2)
|
||||
box.lowPass.q.setInitValue(INVERSE_SQRT_2)
|
||||
box.index.setValue(index)
|
||||
box.host.refer(unit)
|
||||
})
|
||||
create: ({boxGraph}, unit, index): RevampDeviceBox =>
|
||||
RevampDeviceBox.create(boxGraph, UUID.generate(), box => {
|
||||
box.label.setValue("Revamp")
|
||||
box.highPass.frequency.setInitValue(40.0)
|
||||
box.highPass.order.setInitValue(2)
|
||||
box.highPass.q.setInitValue(INVERSE_SQRT_2)
|
||||
box.highPass.enabled.setInitValue(true)
|
||||
box.lowShelf.frequency.setInitValue(80.0)
|
||||
box.lowShelf.gain.setInitValue(6)
|
||||
box.lowBell.frequency.setInitValue(120.0)
|
||||
box.lowBell.gain.setInitValue(6)
|
||||
box.lowBell.q.setInitValue(INVERSE_SQRT_2)
|
||||
box.midBell.frequency.setInitValue(640.0)
|
||||
box.midBell.q.setInitValue(INVERSE_SQRT_2)
|
||||
box.midBell.gain.setInitValue(6)
|
||||
box.highBell.frequency.setInitValue(3600.0)
|
||||
box.highBell.q.setInitValue(INVERSE_SQRT_2)
|
||||
box.highBell.gain.setInitValue(6)
|
||||
box.highShelf.frequency.setInitValue(10000.0)
|
||||
box.highShelf.gain.setInitValue(6)
|
||||
box.lowPass.frequency.setInitValue(15000.0)
|
||||
box.lowPass.order.setInitValue(2)
|
||||
box.lowPass.q.setInitValue(INVERSE_SQRT_2)
|
||||
box.index.setValue(index)
|
||||
box.host.refer(unit)
|
||||
})
|
||||
}
|
||||
|
||||
export const Modular: EffectFactory = {
|
||||
@@ -146,7 +150,7 @@ export namespace EffectFactories {
|
||||
description: "",
|
||||
separatorBefore: true,
|
||||
type: "audio",
|
||||
create: ({boxGraph, rootBox, userEditingManager}, unit, index) => {
|
||||
create: ({boxGraph, rootBox, userEditingManager}, unit, index): ModularDeviceBox => {
|
||||
const moduleSetupBox = ModularBox.create(boxGraph, UUID.generate(), box => {
|
||||
box.collection.refer(rootBox.modularSetups)
|
||||
box.label.setValue("Modular")
|
||||
@@ -180,11 +184,9 @@ export namespace EffectFactories {
|
||||
|
||||
export const MidiNamed = {Arpeggio, Pitch, Zeitgeist}
|
||||
export const AudioNamed = {StereoTool, Delay, Reverb, Revamp, Modular}
|
||||
|
||||
export const MidiList: ReadonlyArray<Readonly<EffectFactory>> = Object.values(MidiNamed)
|
||||
export const AudioList: ReadonlyArray<Readonly<EffectFactory>> = Object.values(AudioNamed)
|
||||
export const MergedNamed = {...MidiNamed, ...AudioNamed}
|
||||
|
||||
export type MidiEffectKeys = keyof typeof MidiNamed
|
||||
export type AudioEffectKeys = keyof typeof AudioNamed
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import {EffectPointerType, IconSymbol} from "@opendaw/studio-adapters"
|
||||
import {Box, Field} from "@opendaw/lib-box"
|
||||
import {Field} from "@opendaw/lib-box"
|
||||
import {int} from "@opendaw/lib-std"
|
||||
import {Project} from "./Project"
|
||||
import {EffectBox} from "./EffectBox"
|
||||
|
||||
export interface EffectFactory {
|
||||
get defaultName(): string
|
||||
@@ -10,5 +11,5 @@ export interface EffectFactory {
|
||||
get separatorBefore(): boolean
|
||||
get type(): "audio" | "midi"
|
||||
|
||||
create(project: Project, unit: Field<EffectPointerType>, index: int): Box
|
||||
create(project: Project, unit: Field<EffectPointerType>, index: int): EffectBox
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import {assert, clamp, float, int, Observer, Option, panic, Strings, Subscription, UUID} from "@opendaw/lib-std"
|
||||
import {ppqn, PPQN} from "@opendaw/lib-dsp"
|
||||
import {Box, Field, PointerField} from "@opendaw/lib-box"
|
||||
import {BoxUtils, Field, PointerField} from "@opendaw/lib-box"
|
||||
import {AudioUnitType, Pointers} from "@opendaw/studio-enums"
|
||||
import {
|
||||
AudioBusBox,
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
AnyClipBox,
|
||||
AudioUnitBoxAdapter,
|
||||
DeviceHost,
|
||||
EffectDeviceBoxAdapter,
|
||||
EffectPointerType,
|
||||
IconSymbol,
|
||||
IndexedAdapterCollectionListener,
|
||||
@@ -31,6 +30,7 @@ import {InstrumentProduct} from "./InstrumentProduct"
|
||||
import {InstrumentOptions} from "./InstrumentOptions"
|
||||
import {EffectFactory} from "./EffectFactory"
|
||||
import {ColorCodes} from "./ColorCodes"
|
||||
import {EffectBox} from "./EffectBox"
|
||||
|
||||
export type ClipRegionOptions = {
|
||||
name?: string
|
||||
@@ -133,24 +133,8 @@ export class ProjectApi {
|
||||
return audioBusBox
|
||||
}
|
||||
|
||||
// TODO If the newIndex is occupied, move to next index
|
||||
createEffect(host: DeviceHost, factory: EffectFactory, newIndex: int): Box {
|
||||
let chain: ReadonlyArray<EffectDeviceBoxAdapter>
|
||||
let field: Field<EffectPointerType>
|
||||
if (factory.type === "audio") {
|
||||
chain = host.audioEffects.adapters()
|
||||
field = host.audioEffects.field()
|
||||
} else if (factory.type === "midi") {
|
||||
chain = host.midiEffects.adapters()
|
||||
field = host.midiEffects.field()
|
||||
} else {
|
||||
return panic(`Unknown factory type: ${factory.type}`)
|
||||
}
|
||||
const box = factory.create(this.#project, field, newIndex)
|
||||
for (let index = newIndex; index < chain.length; index++) {
|
||||
chain[index].indexField.setValue(index + 1)
|
||||
}
|
||||
return box
|
||||
insertEffect(field: Field<EffectPointerType>, factory: EffectFactory, insertIndex: int = Number.MAX_SAFE_INTEGER): EffectBox {
|
||||
return factory.create(this.#project, field, BoxUtils.insert(field, insertIndex))
|
||||
}
|
||||
|
||||
createNoteTrack(adapter: AudioUnitBoxAdapter, index: int = 0): TrackBox {
|
||||
|
||||
@@ -10,6 +10,7 @@ export * from "./samples/SampleProvider"
|
||||
|
||||
export * from "./ColorCodes"
|
||||
export * from "./Colors"
|
||||
export * from "./EffectBox"
|
||||
export * from "./EffectFactory"
|
||||
export * from "./EffectFactories"
|
||||
export * from "./Engine"
|
||||
|
||||
Reference in New Issue
Block a user