microduck/sounds/Cargo.toml
apirrone 626556779b Audio & microphone: the robot's voice and ear, all in Rust (API v10)
Migration step: the prototype's audio stack, with its two satellite
repos absorbed as workspace crates and the Python removed entirely.

sounds/ is microduck_sounds rewritten in Rust: the seedable synth
(personality, recipes, variants) plus a CLI whose ensure-bank replaces
generate_sounds.sh — no venv, no numpy, no ffmpeg. It renders at
48 kHz natively (the resample step existed only because the synth was
22.05 kHz and the Radxa's I2S tree is 48k-family), with the two
rate-tuned constants rescaled to keep their time-domain character. The
RNG is a vendored xoshiro256++ with a pinned-stream test, because the
generator IS the voice: every robot re-rolls once (bank v5, the same
event as a synth retune), then stays itself forever. The seed
derivation (SoC serial -> sha256 -> u32) is unchanged, so each robot
keeps its personality traits' provenance. The parrot experiment stays
unported.

pet-detect/ is microduck_pet_detect plus the prototype's pet_worker:
mel extractor, streaming detector, the arecord worker and the ambient
sound sentry, the pet-detect/pet-features binaries, the vendored model
(shipped as models/pet_detect.onnx) and the training script — which
extracts features through the Rust binary, so train/infer parity
crosses the port intact.

robotd owns playback (root already, and the codec PCM is single-
client): greet as the loop comes up, goodbye peck on shutdown, coo on
petting (walk mode, per the prototype's launch lines), and the new
robot.sound call (v10) for the rest. padd's triggers do what the
prototype's did — RT quacks on its rising edge, LT rides the wheee,
streamed start->loop->end into one aplay with the writer paced 250 ms
ahead. The hold is a per-tick notification that decays, so a client
that dies mid-ride leaves a ride that lands. robotctl quack replaces
the quack binary, over the socket — so it also works from a laptop on
a forwarded socket, which is when you actually need to know which duck
you are talking to.

Found by the fake-bus torture test: robotd restores SIGPIPE's default
disposition at startup, so a write into a dead aplay killed the whole
daemon — the wheee writer thread now blocks SIGPIPE and gets EPIPE
like it expects.

Provisioning: setup-board.sh gains configure_audio() — alsa-utils +
DKMS toolchain, the Armbian vendor kernel (+headers), the i2c3 +
codec/I2S overlays compiled from deploy/audio/, the aic3x DKMS module
(the vendor kernel does not build SND_SOC_AIC3X), and the mixer-init
service ordered Before=robotd. The voice bank is NOT provisioned
there: every release install renders it via postinstall (sounds
ensure-bank, idempotent by seed+version marker). Packaging ships the
three new binaries and the model at all three sites, with a tripwire
for the model like the policies have.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 14:59:32 +02:00

19 lines
649 B
TOML

# The robot's voice: a tiny seedable synth for pet vocalisations.
#
# Ported from `apirrone/microduck_sounds` (Python/numpy) so a release carries its own voice
# generator instead of a pip install — the bank renders on-device in a few seconds of Rust
# rather than minutes of numpy on a venv nobody provisioned. See src/lib.rs for what changed
# in the port and why every robot's voice shifts once.
[package]
name = "sounds"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
anyhow = "1"
clap = { workspace = true, features = ["derive"] }
hound.workspace = true
sha2 = "0.11"