import type { PluginParameter } from "@suaveplan/audio-fx-plugin-base/base"; import { SPESSASYNTH_CODES, SpessaSynthError, } from "../error-codes/error-codes.js"; export const CHANNEL_COUNT = 16; export const MAX_CHANNEL_INDEX = CHANNEL_COUNT - 1; /** Per-channel parameter kind. */ export type ChannelParamKind = | "volume" | "pan" | "program" | "reverbSend" | "chorusSend"; /** Closed enum of per-channel kinds in declaration order. */ export const CHANNEL_PARAM_KINDS: ReadonlyArray = [ "volume", "pan", "program", "reverbSend", "chorusSend", ]; /** Build a deterministic parameter id from a channel + kind. */ export function channelParameterId( channel: number, kind: ChannelParamKind ): string { return `channel/${channel}/${kind}`; } /** Master-gain parameter id. */ export const MASTER_GAIN_ID = "master/gain"; /** * Build the full 16-channel × 5-kind + master parameter declaration * the `AudioFxPlugin` constructor consumes. Every parameter is * `automatable: true` so the host chain can drive it. */ export function buildParameterDeclarations(): ReadonlyArray { const out: PluginParameter[] = []; for (let ch = 0; ch < CHANNEL_COUNT; ch++) { out.push({ id: channelParameterId(ch, "volume"), name: `Ch ${ch + 1} Volume`, value: 1, min: 0, max: 1, default: 1, unit: "ratio", automatable: true, }); out.push({ id: channelParameterId(ch, "pan"), name: `Ch ${ch + 1} Pan`, value: 0, min: -1, max: 1, default: 0, unit: "ratio", automatable: true, }); out.push({ id: channelParameterId(ch, "program"), name: `Ch ${ch + 1} Program`, value: 0, min: 0, max: 127, default: 0, unit: "enum", automatable: false, }); out.push({ id: channelParameterId(ch, "reverbSend"), name: `Ch ${ch + 1} Reverb Send`, value: 0, min: 0, max: 1, default: 0, unit: "ratio", automatable: true, }); out.push({ id: channelParameterId(ch, "chorusSend"), name: `Ch ${ch + 1} Chorus Send`, value: 0, min: 0, max: 1, default: 0, unit: "ratio", automatable: true, }); } out.push({ id: MASTER_GAIN_ID, name: "Master Gain", value: 1, min: 0, max: 4, default: 1, unit: "ratio", automatable: true, }); return out; } /** * Parse a `channel//` parameter id into its components. Returns * `undefined` for the master id or any unrecognised pattern; the caller * decides whether that's an error. */ export function parseChannelParameterId( id: string ): { channel: number; kind: ChannelParamKind } | undefined { if (!id.startsWith("channel/")) return undefined; const rest = id.slice("channel/".length); const slash = rest.indexOf("/"); if (slash < 0) return undefined; const channelStr = rest.slice(0, slash); const kind = rest.slice(slash + 1) as ChannelParamKind; const channel = Number.parseInt(channelStr, 10); if ( !Number.isInteger(channel) || channel < 0 || channel > MAX_CHANNEL_INDEX ) { return undefined; } if (!CHANNEL_PARAM_KINDS.includes(kind)) return undefined; return { channel, kind }; } /** Throws `SPESSASYNTH.INVALID_CHANNEL` when channel is out of range. */ export function assertChannel(channel: number): void { if ( !Number.isInteger(channel) || channel < 0 || channel > MAX_CHANNEL_INDEX ) { throw new SpessaSynthError( SPESSASYNTH_CODES.INVALID_CHANNEL, "spessasynth.invalid-channel", { context: { channel } } ); } }