adds grain and fixes
This commit is contained in:
@@ -36,17 +36,21 @@ component
|
||||
align-items: center
|
||||
justify-content: center
|
||||
outline: 1px dashed rgba(white, 0.1)
|
||||
margin: 0.375em
|
||||
margin: 0.5em
|
||||
position: relative
|
||||
cursor: pointer
|
||||
pointer-events: all
|
||||
|
||||
> svg
|
||||
width: 2em
|
||||
height: 2em
|
||||
width: 1em
|
||||
height: 1em
|
||||
pointer-events: none
|
||||
|
||||
> span
|
||||
text-align: center
|
||||
width: 75%
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
font-size: 0.5em
|
||||
pointer-events: none
|
||||
|
||||
@@ -60,9 +64,13 @@ component
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
position: absolute
|
||||
bottom: -1.5em
|
||||
bottom: -1em
|
||||
width: 100%
|
||||
text-align: center
|
||||
padding: 1px 2px
|
||||
border-radius: 3px
|
||||
color: black
|
||||
background-color: var(--color-blue)
|
||||
|
||||
&.accept
|
||||
color: var(--color-black)
|
||||
|
||||
@@ -198,8 +198,17 @@ export const ScriptDeviceEditor = ({lifecycle, service, adapter, deviceHost, con
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
)
|
||||
const sampleSelector = new SampleSelector(service,
|
||||
SampleSelectStrategy.forPointerField(sample.file))
|
||||
const sampleSelector = new SampleSelector(service, {
|
||||
hasSample: () => sample.file.nonEmpty(),
|
||||
replace: (replacement) => replacement.match({
|
||||
none: () => sample.file.targetVertex.ifSome(({box: fileBox}) => {
|
||||
const mustDelete = fileBox.pointerHub.size() === 1
|
||||
sample.file.defer()
|
||||
if (mustDelete) {fileBox.delete()}
|
||||
}),
|
||||
some: () => SampleSelectStrategy.changePointer(sample.file, replacement)
|
||||
})
|
||||
})
|
||||
terminator.ownAll(
|
||||
sample.file.catchupAndSubscribe(pointer => pointer.targetVertex.match({
|
||||
none: () => dropZone.removeAttribute("sample"),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import SimpleSine from "./examples/simple-sine.js?raw"
|
||||
import GrainSynth from "./examples/grain-synth.js?raw"
|
||||
import {CodeEditorExample} from "@/ui/werkstatt-editor/CodeEditorState"
|
||||
|
||||
export const ApparatExamples: ReadonlyArray<CodeEditorExample> = [
|
||||
{name: "Simple Sine Synth", code: SimpleSine}
|
||||
{name: "Simple Sine Synth", code: SimpleSine},
|
||||
{name: "Grain Synthesizer", code: GrainSynth}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// Grain Synthesizer
|
||||
// @sample grain
|
||||
// @param density 8 1 32 int
|
||||
// @param size 0.2 0.01 0.5 exp s
|
||||
// @param spread 0.5 0 1 linear
|
||||
// @param pitch 0 -12 12 int
|
||||
|
||||
class Processor {
|
||||
density = 8
|
||||
size = 0.05
|
||||
spread = 0.5
|
||||
pitchShift = 0
|
||||
grains = []
|
||||
held = []
|
||||
paramChanged(name, value) {
|
||||
if (name === "density") this.density = value
|
||||
if (name === "size") this.size = value
|
||||
if (name === "spread") this.spread = value
|
||||
if (name === "pitch") this.pitchShift = value
|
||||
}
|
||||
noteOn(pitch, velocity, cent, id) {
|
||||
this.held.push({id, pitch, velocity, cent, elapsed: 0})
|
||||
}
|
||||
noteOff(id) {
|
||||
this.held = this.held.filter(note => note.id !== id)
|
||||
}
|
||||
reset() {
|
||||
this.held = []
|
||||
this.grains = []
|
||||
}
|
||||
process(output, block) {
|
||||
const data = this.samples.grain
|
||||
if (data === null || data.numberOfFrames === 0) return
|
||||
const [outL, outR] = output
|
||||
const rate = data.sampleRate / sampleRate
|
||||
const srcL = data.frames[0]
|
||||
const srcR = data.frames[data.numberOfChannels > 1 ? 1 : 0]
|
||||
const interval = sampleRate / this.density
|
||||
const grainSamples = Math.round(this.size * sampleRate)
|
||||
for (const note of this.held) {
|
||||
const playbackRate = rate * Math.pow(2, (note.pitch - 60 + note.cent / 100 + this.pitchShift) / 12)
|
||||
for (let s = block.s0; s < block.s1; s++) {
|
||||
if (note.elapsed % Math.round(interval) === 0) {
|
||||
const offset = Math.random() * this.spread * data.numberOfFrames
|
||||
this.grains.push({
|
||||
position: offset,
|
||||
age: 0,
|
||||
length: grainSamples,
|
||||
rate: playbackRate,
|
||||
velocity: note.velocity,
|
||||
pan: Math.random() * 2 - 1
|
||||
})
|
||||
}
|
||||
note.elapsed++
|
||||
}
|
||||
}
|
||||
for (let i = this.grains.length - 1; i >= 0; i--) {
|
||||
const grain = this.grains[i]
|
||||
for (let s = block.s0; s < block.s1; s++) {
|
||||
if (grain.age >= grain.length) {
|
||||
this.grains.splice(i, 1)
|
||||
break
|
||||
}
|
||||
const env = Math.sin(grain.age / grain.length * Math.PI)
|
||||
const pos = Math.floor(grain.position) % data.numberOfFrames
|
||||
const sample = srcL[pos] * env * grain.velocity * 0.2
|
||||
const sampleR = srcR[pos] * env * grain.velocity * 0.2
|
||||
const panL = 1 - Math.max(0, grain.pan)
|
||||
const panR = 1 + Math.min(0, grain.pan)
|
||||
outL[s] += sample * panL
|
||||
outR[s] += sampleR * panR
|
||||
grain.position += grain.rate
|
||||
grain.age++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import {asInstanceOf, Editing, isDefined, UUID} from "@opendaw/lib-std"
|
||||
import {BoxGraph, Field, StringField} from "@opendaw/lib-box"
|
||||
import {Pointers} from "@opendaw/studio-enums"
|
||||
import {WerkstattParameterBox, WerkstattSampleBox} from "@opendaw/studio-boxes"
|
||||
import {parseParams, parseSamples} from "@opendaw/studio-adapters"
|
||||
import {parseDeclarationOrder, parseParams, parseSamples} from "@opendaw/studio-adapters"
|
||||
import type {ParamDeclaration, SampleDeclaration} from "@opendaw/studio-adapters"
|
||||
|
||||
export interface ScriptDeviceBox {
|
||||
@@ -35,7 +35,7 @@ const parseHeader = (source: string, pattern: RegExp): {userCode: string, update
|
||||
}
|
||||
}
|
||||
|
||||
const reconcileParameters = (deviceBox: ScriptDeviceBox, declared: ReadonlyArray<ParamDeclaration>): void => {
|
||||
const reconcileParameters = (deviceBox: ScriptDeviceBox, declared: ReadonlyArray<ParamDeclaration>, order: Map<string, number>): void => {
|
||||
const boxGraph = deviceBox.graph
|
||||
const existingPointers = deviceBox.parameters.pointerHub.filter()
|
||||
const existingByLabel = new Map<string, WerkstattParameterBox>()
|
||||
@@ -51,14 +51,14 @@ const reconcileParameters = (deviceBox: ScriptDeviceBox, declared: ReadonlyArray
|
||||
}
|
||||
}
|
||||
seen.clear()
|
||||
for (let index = 0; index < declared.length; index++) {
|
||||
const declaration = declared[index]
|
||||
for (const declaration of declared) {
|
||||
if (seen.has(declaration.label)) {continue}
|
||||
seen.add(declaration.label)
|
||||
const unifiedIndex = order.get(declaration.label) ?? 0
|
||||
const existing = existingByLabel.get(declaration.label)
|
||||
const mappedDefault = declaration.defaultValue
|
||||
if (isDefined(existing)) {
|
||||
existing.index.setValue(index)
|
||||
existing.index.setValue(unifiedIndex)
|
||||
if (Math.abs(existing.defaultValue.getValue() - mappedDefault) > FLOAT_TOLERANCE) {
|
||||
existing.defaultValue.setValue(mappedDefault)
|
||||
existing.value.setValue(mappedDefault)
|
||||
@@ -67,7 +67,7 @@ const reconcileParameters = (deviceBox: ScriptDeviceBox, declared: ReadonlyArray
|
||||
WerkstattParameterBox.create(boxGraph, UUID.generate(), paramBox => {
|
||||
paramBox.owner.refer(deviceBox.parameters)
|
||||
paramBox.label.setValue(declaration.label)
|
||||
paramBox.index.setValue(index)
|
||||
paramBox.index.setValue(unifiedIndex)
|
||||
paramBox.value.setValue(mappedDefault)
|
||||
paramBox.defaultValue.setValue(mappedDefault)
|
||||
})
|
||||
@@ -75,7 +75,7 @@ const reconcileParameters = (deviceBox: ScriptDeviceBox, declared: ReadonlyArray
|
||||
}
|
||||
}
|
||||
|
||||
const reconcileSamples = (deviceBox: ScriptDeviceBox, declared: ReadonlyArray<SampleDeclaration>): void => {
|
||||
const reconcileSamples = (deviceBox: ScriptDeviceBox, declared: ReadonlyArray<SampleDeclaration>, order: Map<string, number>): void => {
|
||||
const boxGraph = deviceBox.graph
|
||||
const existingPointers = deviceBox.samples.pointerHub.filter()
|
||||
const existingByLabel = new Map<string, WerkstattSampleBox>()
|
||||
@@ -91,18 +91,18 @@ const reconcileSamples = (deviceBox: ScriptDeviceBox, declared: ReadonlyArray<Sa
|
||||
}
|
||||
}
|
||||
seen.clear()
|
||||
for (let index = 0; index < declared.length; index++) {
|
||||
const declaration = declared[index]
|
||||
for (const declaration of declared) {
|
||||
if (seen.has(declaration.label)) {continue}
|
||||
seen.add(declaration.label)
|
||||
const unifiedIndex = order.get(declaration.label) ?? 0
|
||||
const existing = existingByLabel.get(declaration.label)
|
||||
if (isDefined(existing)) {
|
||||
existing.index.setValue(index)
|
||||
existing.index.setValue(unifiedIndex)
|
||||
} else {
|
||||
WerkstattSampleBox.create(boxGraph, UUID.generate(), sampleBox => {
|
||||
sampleBox.owner.refer(deviceBox.samples)
|
||||
sampleBox.label.setValue(declaration.label)
|
||||
sampleBox.index.setValue(index)
|
||||
sampleBox.index.setValue(unifiedIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -157,12 +157,13 @@ export const createScriptCompiler = (config: ScriptCompilerConfig) => {
|
||||
const uuid = UUID.toString(deviceBox.address.uuid)
|
||||
const params = parseParams(userCode)
|
||||
const samples = parseSamples(userCode)
|
||||
const order = parseDeclarationOrder(userCode)
|
||||
const wrappedCode = wrapCode(config, uuid, newUpdate, userCode)
|
||||
validateCode(wrappedCode)
|
||||
const modifier = () => {
|
||||
deviceBox.code.setValue(createHeader(newUpdate) + userCode)
|
||||
reconcileParameters(deviceBox, params)
|
||||
reconcileSamples(deviceBox, samples)
|
||||
reconcileParameters(deviceBox, params, order)
|
||||
reconcileSamples(deviceBox, samples, order)
|
||||
}
|
||||
if (append) {
|
||||
editing.append(modifier)
|
||||
|
||||
@@ -96,6 +96,22 @@ export const parseSamples = (code: string): ReadonlyArray<SampleDeclaration> =>
|
||||
return samples
|
||||
}
|
||||
|
||||
const DECLARATION_LINE = /^\/\/ @(?:param|sample) \S+/gm
|
||||
|
||||
export const parseDeclarationOrder = (code: string): Map<string, number> => {
|
||||
const order = new Map<string, number>()
|
||||
let match: Nullable<RegExpExecArray>
|
||||
DECLARATION_LINE.lastIndex = 0
|
||||
let index = 0
|
||||
while ((match = DECLARATION_LINE.exec(code)) !== null) {
|
||||
const label = match[0].replace(/^\/\/ @(?:param|sample)\s+/, "").split(/\s+/)[0]
|
||||
if (!order.has(label)) {
|
||||
order.set(label, index++)
|
||||
}
|
||||
}
|
||||
return order
|
||||
}
|
||||
|
||||
export const resolveValueMapping = (declaration: ParamDeclaration): ValueMapping<number> => {
|
||||
switch (declaration.mapping) {
|
||||
case "unipolar": return ValueMapping.unipolar()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Arrays, asInstanceOf, int, isDefined, isNotNull, Nullable, Option, Terminable, UUID} from "@opendaw/lib-std"
|
||||
import {ApparatDeviceBoxAdapter} from "@opendaw/studio-adapters"
|
||||
import {ApparatDeviceBoxAdapter, SampleLoader} from "@opendaw/studio-adapters"
|
||||
import {WerkstattParameterBox, WerkstattSampleBox} from "@opendaw/studio-boxes"
|
||||
import {AudioBuffer, AudioData, Event, NoteEvent, SimpleLimiter} from "@opendaw/lib-dsp"
|
||||
import {EngineContext} from "../../EngineContext"
|
||||
@@ -53,7 +53,7 @@ export class ApparatDeviceProcessor extends AudioProcessor
|
||||
readonly #peakBroadcaster: PeakBroadcaster
|
||||
readonly #uuid: string
|
||||
readonly #boundParameters: Array<AutomatableParameter<number>>
|
||||
readonly #sampleLoaders: Map<string, {label: string, lifecycle: Terminable}> = new Map()
|
||||
readonly #sampleSlots: Map<string, {loader: Option<SampleLoader>, lifecycle: Terminable}> = new Map()
|
||||
|
||||
#userProcessor: Option<UserProcessor> = Option.None
|
||||
#currentUpdate: int = -1
|
||||
@@ -102,29 +102,27 @@ export class ApparatDeviceProcessor extends AudioProcessor
|
||||
onAdded: (({box: sampleBox}) => {
|
||||
const sample = asInstanceOf(sampleBox, WerkstattSampleBox)
|
||||
const label = sample.label.getValue()
|
||||
const subscription = sample.file.catchupAndSubscribe(pointer => {
|
||||
pointer.targetVertex.match({
|
||||
none: () => this.#updateSampleData(label, null),
|
||||
some: ({box: fileBox}) => {
|
||||
const fileUUID = fileBox.address.uuid
|
||||
const loader = context.sampleManager.getOrCreate(fileUUID)
|
||||
loader.subscribe(state => {
|
||||
if (state.type === "loaded") {
|
||||
this.#updateSampleData(label, loader.data.unwrap())
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
const slot: {loader: Option<SampleLoader>, lifecycle: Terminable} = {
|
||||
loader: Option.None,
|
||||
lifecycle: Terminable.Empty
|
||||
}
|
||||
this.#sampleSlots.set(label, slot)
|
||||
slot.lifecycle = sample.file.catchupAndSubscribe(pointer => {
|
||||
const target = pointer.targetVertex.unwrapOrNull()
|
||||
if (target === null) {
|
||||
slot.loader = Option.None
|
||||
} else {
|
||||
slot.loader = Option.wrap(context.sampleManager.getOrCreate(target.box.address.uuid))
|
||||
}
|
||||
})
|
||||
this.#sampleLoaders.set(label, {label, lifecycle: subscription})
|
||||
}),
|
||||
onRemoved: (({box: sampleBox}) => {
|
||||
const sample = asInstanceOf(sampleBox, WerkstattSampleBox)
|
||||
const label = sample.label.getValue()
|
||||
const entry = this.#sampleLoaders.get(label)
|
||||
const entry = this.#sampleSlots.get(label)
|
||||
if (isDefined(entry)) {
|
||||
entry.lifecycle.terminate()
|
||||
this.#sampleLoaders.delete(label)
|
||||
this.#sampleSlots.delete(label)
|
||||
}
|
||||
this.#updateSampleData(label, null)
|
||||
})
|
||||
@@ -178,6 +176,7 @@ export class ApparatDeviceProcessor extends AudioProcessor
|
||||
}
|
||||
if (this.#userProcessor.isEmpty()) {return}
|
||||
const proc = this.#userProcessor.unwrap()
|
||||
this.#pollSamples(proc)
|
||||
const outL = this.#audioOutput.getChannel(0)
|
||||
const outR = this.#audioOutput.getChannel(1)
|
||||
outL.fill(0.0, block.s0, block.s1)
|
||||
@@ -225,14 +224,14 @@ export class ApparatDeviceProcessor extends AudioProcessor
|
||||
try {
|
||||
const proc = new ProcessorClass() as UserProcessor
|
||||
proc.samples = {}
|
||||
for (const [label] of this.#sampleLoaders) {
|
||||
for (const [label] of this.#sampleSlots) {
|
||||
proc.samples[label] = null
|
||||
}
|
||||
this.#userProcessor = Option.wrap(proc)
|
||||
this.#currentUpdate = update
|
||||
this.#silenced = false
|
||||
this.#pushAllParameters()
|
||||
this.#pushAllSamples()
|
||||
this.#pollSamples(proc)
|
||||
} catch (error) {
|
||||
this.#silence(`Failed to instantiate Processor: ${error}`)
|
||||
}
|
||||
@@ -249,13 +248,15 @@ export class ApparatDeviceProcessor extends AudioProcessor
|
||||
})
|
||||
}
|
||||
|
||||
#pushAllSamples(): void {
|
||||
this.#userProcessor.ifSome(proc => {
|
||||
for (const [label] of this.#sampleLoaders) {
|
||||
const existing = proc.samples[label]
|
||||
if (isDefined(existing)) {continue}
|
||||
}
|
||||
})
|
||||
#pollSamples(proc: UserProcessor): void {
|
||||
for (const [label, slot] of this.#sampleSlots) {
|
||||
slot.loader.ifSome(loader => {
|
||||
const data = loader.data.unwrapOrNull()
|
||||
if (proc.samples[label] !== data) {
|
||||
proc.samples[label] = data
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#updateSampleData(label: string, data: Nullable<AudioData>): void {
|
||||
|
||||
Reference in New Issue
Block a user