insert effect

This commit is contained in:
André Michelle
2025-07-17 20:20:16 +02:00
parent 89338a917a
commit fbcbc6b227
19 changed files with 218 additions and 188 deletions
+34 -34
View File
@@ -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
View File
@@ -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"
},
+7 -11
View File
@@ -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
}
+1
View File
@@ -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",
{
+1 -1
View File
@@ -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", () => {
+1 -1
View File
@@ -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()}
+10 -10
View File
@@ -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())
+18 -21
View File
@@ -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()
+1
View File
@@ -23,4 +23,5 @@ export * from "./sync"
export * from "./sync-source"
export * from "./sync-target"
export * from "./updates"
export * from "./utils"
export * from "./vertex"
+25 -21
View File
@@ -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
}
+1 -8
View File
@@ -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
+1 -1
View File
@@ -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({
+12 -12
View File
@@ -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()}`
}
}
+36
View File
@@ -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
}
}
+14
View File
@@ -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
+47 -45
View File
@@ -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
}
+3 -2
View File
@@ -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
}
+4 -20
View File
@@ -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 {
+1
View File
@@ -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"