/** * Volume Feature * Provides volume control capability */ import { Feature, type TDeviceReference } from './feature.abstract.js'; import type { IVolumeFeatureInfo, IFeatureOptions, } from '../interfaces/feature.interfaces.js'; /** * Options for creating a VolumeFeature */ export interface IVolumeFeatureOptions extends IFeatureOptions { /** Volume control protocol (usually same as device protocol) */ volumeProtocol?: string; /** Minimum volume level */ minVolume?: number; /** Maximum volume level */ maxVolume?: number; /** Volume step increment */ volumeStep?: number; /** Whether mute is supported */ supportsMute?: boolean; /** Protocol-specific volume controller */ volumeController?: IVolumeController; } /** * Interface for protocol-specific volume control */ export interface IVolumeController { getVolume(): Promise; setVolume(level: number): Promise; getMute(): Promise; setMute(muted: boolean): Promise; } /** * Volume Feature - provides volume control capability * * Separated from PlaybackFeature because some devices have volume control * without playback capability (e.g., amplifiers, HDMI matrix switches). * * @example * ```typescript * const volume = device.getFeature('volume'); * if (volume) { * const current = await volume.getVolume(); * await volume.setVolume(current + 10); * await volume.toggleMute(); * } * ``` */ export class VolumeFeature extends Feature { public readonly type = 'volume' as const; public readonly protocol: string; // Capabilities public readonly minVolume: number; public readonly maxVolume: number; public readonly volumeStep: number; public readonly supportsMute: boolean; // Current state protected _volume: number = 0; protected _muted: boolean = false; // Volume controller (protocol-specific) protected volumeController: IVolumeController | null = null; constructor( device: TDeviceReference, port: number, options?: IVolumeFeatureOptions ) { super(device, port, options); this.protocol = options?.volumeProtocol ?? 'generic'; this.minVolume = options?.minVolume ?? 0; this.maxVolume = options?.maxVolume ?? 100; this.volumeStep = options?.volumeStep ?? 5; this.supportsMute = options?.supportsMute ?? true; if (options?.volumeController) { this.volumeController = options.volumeController; } } // ============================================================================ // Properties // ============================================================================ /** * Get current volume level (cached) */ public get volume(): number { return this._volume; } /** * Get mute state (cached) */ public get muted(): boolean { return this._muted; } // ============================================================================ // Connection // ============================================================================ protected async doConnect(): Promise { // Fetch initial state if we have a controller if (this.volumeController) { try { this._volume = await this.volumeController.getVolume(); this._muted = await this.volumeController.getMute(); } catch { // Ignore errors fetching initial state } } } protected async doDisconnect(): Promise { // Nothing to disconnect for volume control } // ============================================================================ // Volume Control // ============================================================================ /** * Get current volume level */ public async getVolume(): Promise { if (this.volumeController) { this._volume = await this.volumeController.getVolume(); } return this._volume; } /** * Set volume level * @param level Volume level (clamped to min/max) */ public async setVolume(level: number): Promise { const clampedLevel = Math.max(this.minVolume, Math.min(this.maxVolume, level)); if (this.volumeController) { await this.volumeController.setVolume(clampedLevel); } const oldVolume = this._volume; this._volume = clampedLevel; if (oldVolume !== clampedLevel) { this.emit('volume:changed', { oldVolume, newVolume: clampedLevel }); } } /** * Increase volume by step */ public async volumeUp(step?: number): Promise { const increment = step ?? this.volumeStep; const newVolume = Math.min(this.maxVolume, this._volume + increment); await this.setVolume(newVolume); return this._volume; } /** * Decrease volume by step */ public async volumeDown(step?: number): Promise { const decrement = step ?? this.volumeStep; const newVolume = Math.max(this.minVolume, this._volume - decrement); await this.setVolume(newVolume); return this._volume; } /** * Get mute state */ public async getMute(): Promise { if (this.volumeController && this.supportsMute) { this._muted = await this.volumeController.getMute(); } return this._muted; } /** * Set mute state */ public async setMute(muted: boolean): Promise { if (!this.supportsMute) { throw new Error('Mute not supported'); } if (this.volumeController) { await this.volumeController.setMute(muted); } const oldMuted = this._muted; this._muted = muted; if (oldMuted !== muted) { this.emit('mute:changed', { oldMuted, newMuted: muted }); } } /** * Toggle mute state */ public async toggleMute(): Promise { const currentMuted = await this.getMute(); await this.setMute(!currentMuted); return this._muted; } // ============================================================================ // Serialization // ============================================================================ public getFeatureInfo(): IVolumeFeatureInfo { return { ...this.getBaseFeatureInfo(), type: 'volume', minVolume: this.minVolume, maxVolume: this.maxVolume, volumeStep: this.volumeStep, supportsMute: this.supportsMute, }; } // ============================================================================ // Static Factory // ============================================================================ /** * Create from discovery metadata */ public static fromDiscovery( device: TDeviceReference, port: number, protocol: string, metadata: Record ): VolumeFeature { return new VolumeFeature(device, port, { volumeProtocol: protocol, minVolume: metadata.minVolume as number ?? 0, maxVolume: metadata.maxVolume as number ?? 100, volumeStep: metadata.volumeStep as number ?? 5, supportsMute: metadata.supportsMute as boolean ?? true, }); } }