Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,149 @@
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 } }
);
}
}