# `commands/play` — `runPlay` ## Purpose `runPlay` plays a file through an injected `SpeakerAdapter`. The CLI package intentionally carries no platform audio binding — naming a specific speaker binary (sox, mpv, ffplay) would couple the package to whichever the user has installed. Instead, the function exercises argument validation, the input load path, and the adapter dispatch, and consumers (or installer hooks) wire the actual hardware adapter behind the `SpeakerAdapter` interface. A `noopSpeaker` is exported so headless servers, CI, and tests can run the `play` code path without audio output. ## Features - Zod-strict `PlayOptions` accepting an injected `SpeakerAdapter` and an `AbortSignal`. - Default `noopSpeaker` adapter for headless environments. - Same input-error mapping (`AUDIO_CLI.INPUT_NOT_FOUND`) as the other load-driven commands. - Adapter failures wrap as `AUDIO_CLI.PIPELINE_FAILED` with the original cause attached. ## Usage: Basic ```ts import { AudioCLI } from "@suaveplan/audio-cli"; const cli = new AudioCLI(); await cli.play("input.wav"); // noop on a headless host ``` ## Usage: Advanced ```ts import { runPlay } from "@suaveplan/audio-cli/commands/play"; import type { SpeakerAdapter } from "@suaveplan/audio-cli/commands/play"; const mySpeaker: SpeakerAdapter = { async play(doc, { signal }) { /* ...write doc to a DAC... */ }, }; await runPlay(cli, "song.flac", { speaker: mySpeaker }); ``` ## API Reference - `runPlay(cli, input, opts?): Promise` - `PlayOptions` — `{ speaker?, signal? }` - `SpeakerAdapter.play(doc, { signal? }): Promise` - `noopSpeaker: SpeakerAdapter` ## Implementation Notes The function deliberately structures the failure modes so a missing speaker falls through to the no-op rather than throwing — this matches `audio play` on a headless box: argv parses, the file decodes, and the command returns `0`. Real hardware bindings ship as separate adapters consumers add at construction time via `new AudioCLI({ ... })` or by passing `opts.speaker` directly.