import { isStruct } from '../../../model/Properties'; import { DEFAULT_TEMPO, heldTempo, MAX_TEMPO, MIN_TEMPO } from '../../../music/time'; import type { FxParams } from '../../../soundfx/nodes'; import type { NodeDef, PortDef } from '../../NodeRegistry'; import { asBoolean, asNumber, asString, type Value } from '../../values'; import { detachedPort, exec, fxParamsPort, num, playedBusPort, soundFxPort, soundPort, withDefault, } from '../shared'; /** * Three answers, not two: a checkbox cannot say "leave it to the asset", and without that a sound * whose own `Loop` is on could never be played as a one-shot. */ const LOOP_OPTIONS = [ { value: '', label: 'From the asset' }, { value: 'true', label: 'On' }, { value: 'false', label: 'Off' }, ]; /** `any`, not `boolean`, so a graph can still drive the choice – see `loopChoice`. */ const loopPort = (): PortDef => ({ key: 'loop', label: 'loop', type: 'any', default: '', field: { key: 'loop', label: 'Loop', kind: 'enum', default: '', options: LOOP_OPTIONS }, }); /** * What the node says about looping: nothing (the asset decides) or an answer that overrules it. * A boolean arriving over an edge is taken literally, which is what wiring a comparison or a * variable into this input is for. */ function loopChoice(value: Value | undefined): boolean | undefined { if (typeof value === 'boolean') return value; const text = asString(value); if (text === 'true') return true; if (text === 'false') return false; return undefined; } export const PlaySound: NodeDef = { type: 'PlaySound', label: 'Play sound', category: 'action', description: 'Plays a sound asset. "volume" multiplies the volume the asset itself carries and "loop" either ' + "leaves the asset's own setting alone or overrules it, so the asset says what the sound is and " + 'the node only colors it. Each play is a voice of its own: firing this twice in a row is heard ' + 'twice, overlapping. The sound belongs to this object and stops when the object is destroyed, ' + 'unless "detached" is on. "fx" sends it through a sound FX asset instead of the one the sound ' + 'names; left empty, it takes that one, and None plays it as it is. ' + '"fx params" takes values for its parameters from a Sound FX parameters node. "voice" is the ' + 'number of this one play, for Set sound FX parameters to change it while it plays; 0 where ' + 'nothing was played. "length" is how many seconds one run of the sound lasts, to hand to the ' + '"time" of Set sound FX parameters so a change spans the sound; 0 where nothing was played or ' + 'the sound has not been loaded yet. A pitch never changes it. "tail" is that length and the ' + 'echoes of its FX after it, as they are when it starts – how long until the voice is quiet. ' + '"tempo" is what a Delay of its FX synced to beats counts by, in quarter notes a minute. "bus" is ' + 'the bus it is heard on; left empty, the one the sound names.', inputs: [ exec(), soundPort(), withDefault(num('volume'), 1), loopPort(), detachedPort(), soundFxPort(), fxParamsPort(), tempoPort(), playedBusPort(), ], outputs: [exec(), num('voice'), num('length'), num('tail')], params: [], run: (ctx, inputs) => { const sound = asString(inputs.sound); const voice = sound ? ctx.playSound( sound, asNumber(inputs.volume), loopChoice(inputs.loop), asBoolean(inputs.detached), asString(inputs.fx), fxParamsOf(inputs.fxParams), heldTempo(asNumber(inputs.tempo)), asString(inputs.bus), ) : 0; const length = voice ? ctx.soundLength(sound) : 0; const tail = length > 0 ? length + ctx.soundTail(voice) : 0; return { next: 'exec', outputs: { voice, length, tail } }; }, }; /** The tempo of a voice outside a song (#332), for a Delay synced to beats. */ export function tempoPort(): PortDef { return { key: 'tempo', label: 'tempo', type: 'number', default: DEFAULT_TEMPO, field: { key: 'tempo', label: 'Tempo (BPM)', kind: 'number', default: DEFAULT_TEMPO, min: MIN_TEMPO, max: MAX_TEMPO, step: 1, }, }; } /** The numbers of a parameter bundle; anything else hands in none. */ export function fxParamsOf(value: Value | undefined): FxParams { if (!isStruct(value)) return {}; const params: Record = {}; for (const [key, item] of Object.entries(value)) if (typeof item === 'number') params[key] = item; return params; }