import type { ErrorCode } from "./error"; export const SEQUENCER_AUDIO_SESSION_SCHEMA = 1 as const; export type SequencerAudioContextState = "UNAVAILABLE" | "SUSPENDED" | "RUNNING" | "CLOSED"; export type SequencerAudioOutputState = "BLOCKED" | "SILENT" | "ENABLED"; export type SequencerAudioSessionIssueCode = Extract< ErrorCode, | "SEQUENCER_AUDIO_DEVICE_UNAVAILABLE" | "SEQUENCER_AUDIO_RESUME_FAILED" | "SEQUENCER_AUDIO_SUSPEND_FAILED" >; export interface SequencerAudioSessionReportIR { schemaVersion: typeof SEQUENCER_AUDIO_SESSION_SCHEMA; revision: number; contextState: SequencerAudioContextState; outputState: SequencerAudioOutputState; muted: boolean; outputGain: number; issueCode: SequencerAudioSessionIssueCode | null; } export class SequencerAudioSessionValidationError extends Error { readonly code: ErrorCode; constructor(message: string) { super(`SEQUENCER_AUDIO_CONTEXT_INVALID: ${message}`); this.name = "SequencerAudioSessionValidationError"; this.code = "SEQUENCER_AUDIO_CONTEXT_INVALID"; } } const REPORT_KEYS = new Set([ "schemaVersion", "revision", "contextState", "outputState", "muted", "outputGain", "issueCode", ]); const CONTEXT_STATES = new Set(["UNAVAILABLE", "SUSPENDED", "RUNNING", "CLOSED"]); const OUTPUT_STATES = new Set(["BLOCKED", "SILENT", "ENABLED"]); const ISSUE_CODES = new Set([ "SEQUENCER_AUDIO_DEVICE_UNAVAILABLE", "SEQUENCER_AUDIO_RESUME_FAILED", "SEQUENCER_AUDIO_SUSPEND_FAILED", ]); function record(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } export function parseSequencerAudioSessionReport(value: unknown): SequencerAudioSessionReportIR { if (!record(value) || value.schemaVersion !== SEQUENCER_AUDIO_SESSION_SCHEMA || Object.keys(value).length !== REPORT_KEYS.size || Object.keys(value).some((key) => !REPORT_KEYS.has(key))) { throw new SequencerAudioSessionValidationError("audio session report fields are invalid"); } if (!Number.isSafeInteger(value.revision) || (value.revision as number) < 0 || !CONTEXT_STATES.has(value.contextState as SequencerAudioContextState) || !OUTPUT_STATES.has(value.outputState as SequencerAudioOutputState) || typeof value.muted !== "boolean" || typeof value.outputGain !== "number" || !Number.isFinite(value.outputGain) || value.outputGain < 0 || value.outputGain > 1 || (value.issueCode !== null && !ISSUE_CODES.has(value.issueCode as SequencerAudioSessionIssueCode))) { throw new SequencerAudioSessionValidationError("audio session report values are invalid"); } const report = value as unknown as SequencerAudioSessionReportIR; if (report.contextState === "UNAVAILABLE" && (report.outputState !== "BLOCKED" || report.outputGain !== 0 || report.issueCode !== "SEQUENCER_AUDIO_DEVICE_UNAVAILABLE")) { throw new SequencerAudioSessionValidationError("unavailable audio context must be blocked"); } if (report.contextState === "CLOSED" && (report.outputState !== "SILENT" || report.outputGain !== 0 || report.issueCode !== null)) { throw new SequencerAudioSessionValidationError("closed audio context must release output"); } if ((report.contextState === "RUNNING" || report.contextState === "SUSPENDED") && report.outputState === "BLOCKED") { throw new SequencerAudioSessionValidationError("an allocated audio context cannot report blocked output"); } if (report.contextState === "SUSPENDED" && report.outputState !== "SILENT") { throw new SequencerAudioSessionValidationError("a suspended audio context must be silent"); } if (report.outputState === "ENABLED" && (report.contextState !== "RUNNING" || report.muted || report.outputGain <= 0 || report.issueCode !== null)) { throw new SequencerAudioSessionValidationError("enabled audio output invariants are invalid"); } if (report.muted && report.outputGain !== 0) { throw new SequencerAudioSessionValidationError("muted audio output gain must be zero"); } return { ...report }; }