import mergeWith from "lodash/mergeWith" import { nanoid } from "nanoid" import { DeeplyPartial } from "@/lib/util/types" import { AudioNodeClassOptions, AudioNodeKeyName, AudioNodeKind, DefaultAudioNodeKindFromKeyName, getAudioNodeConfig, } from "@/nativeWebAudio" import { VNodeLookupMap, VNodePath } from "./internal/virtualAudioNode" /** An interface representing an AudioNode abstractly */ export interface VirtualAudioNode< NodeKeyName extends AudioNodeKeyName = any, NodeKind extends AudioNodeKind = DefaultAudioNodeKindFromKeyName > { id: string node: NodeKeyName options: AudioNodeClassOptions inputs: VirtualAudioNode>[] destination?: NodeKeyName extends AudioNodeKeyName<"destination"> ? undefined : VirtualAudioNode> } export type VirtualAudioNodeOfKind = VirtualAudioNode> export interface CreateVirtualAudioNodeRootOptions< NodeKeyName extends AudioNodeKeyName = any, NodeKind extends AudioNodeKind = DefaultAudioNodeKindFromKeyName > { /** Reference to a specific AudioNode class */ node: NodeKeyName /** The args object passed to the AudioNode class's second parameter, if available */ options?: AudioNodeClassOptions /** A list of virtual nodes to create. These cannot be destination nodes, as they have 0 outputs */ inputs?: NodeKeyName extends AudioNodeKeyName<"source"> ? undefined : CreateVirtualAudioNodeRootOptions>[] destination?: NodeKeyName extends AudioNodeKeyName<"destination"> ? undefined : CreateVirtualAudioNodeRootOptions< AudioNodeKeyName<"destination" | "effect"> > } const _createVirtualAudioNode = < NodeKeyName extends AudioNodeKeyName, NodeKind extends AudioNodeKind = DefaultAudioNodeKindFromKeyName >( options: CreateVirtualAudioNodeRootOptions, lookupMap: VNodeLookupMap, path: VNodePath ): { node: VirtualAudioNode; lookupMap: VNodeLookupMap } => { const node: VirtualAudioNode = { id: nanoid(), node: options.node, options: (options?.options as any) || {}, destination: (options?.destination ? _createVirtualAudioNode(options.destination, lookupMap, [...path, "D"]) .node : undefined) as any, inputs: [], } if (options.inputs) { node.inputs = options?.inputs?.map((input, i) => ({ ..._createVirtualAudioNode(input, lookupMap, [...path, `I${i}`]).node, destination: (input.destination || options.destination || node) as VirtualAudioNode>, })) || [] } lookupMap[node.id] = { node, path } if ( node.destination && (!lookupMap[node.destination.id] || path.length + 1 < lookupMap[node.destination.id].path.length) ) { lookupMap[node.destination.id] = { node: node.destination, path: [...path, "D"], } } return { node, lookupMap } } const createRootVirtualAudioNode = < NodeKeyName extends AudioNodeKeyName, NodeKind extends AudioNodeKind = DefaultAudioNodeKindFromKeyName >( options: CreateVirtualAudioNodeRootOptions ) => _createVirtualAudioNode(options, {}, []) /** Merges new options in deeply (arrays overwrite the existing value) */ const updateNodeOptions = ( node: Node, newOptions: DeeplyPartial> ) => ({ ...node, options: mergeWith({}, node.options, newOptions, (_objValue, srcValue) => { if (Array.isArray(srcValue)) { return srcValue } }), }) /** * Note that this will override previous options if * the new options aren't supplied, unless the node * type is not a change. */ const updateNodeType = < Node extends VirtualAudioNode, NewNodeKeyName extends AudioNodeKeyName >( node: Node, newNodeType: NewNodeKeyName, newOptions?: AudioNodeClassOptions ) => ({ ...node, node: newNodeType, options: newOptions || newNodeType === node.node ? node.options : {}, }) const getNodeConfig = (node: Node) => getAudioNodeConfig(node.node) const isSourceNode = ( node: VirtualAudioNode ): node is VirtualAudioNode> => !!getAudioNodeConfig(node.node as any, "source") const isDestinationNode = ( node: VirtualAudioNode ): node is VirtualAudioNode> => !!getAudioNodeConfig(node.node as any, "destination") const isEffectNode = ( node: VirtualAudioNode ): node is VirtualAudioNode> => !!getAudioNodeConfig(node.node as any, "effect") export const VirtualAudioNodeUtil = { createRoot: createRootVirtualAudioNode, updateOptions: updateNodeOptions, updateType: updateNodeType, getConfig: getNodeConfig, isSource: isSourceNode, isDestination: isDestinationNode, isEffect: isEffectNode, }