150 lines
4.0 KiB
TypeScript
150 lines
4.0 KiB
TypeScript
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<ChannelParamKind> = [
|
||
"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<PluginParameter> {
|
||
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/<n>/<kind>` 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 } }
|
||
);
|
||
}
|
||
}
|