Skip to main content

Audio files

Everything Lyno plays — greetings, announcements, music on hold — must be:

8 kHz, mono, 16-bit PCM WAV

That is not a preference. It is the format that can be encoded to G.711 alaw/ulaw on the wire without resampling, and anything else is rejected.

Validation is at startup

Files are parsed, validated and cached in memory when the PBX starts, so a bad file fails fast instead of mid-call. The checks and their messages:

ProblemMessage
Not a WAV containerwav: not a RIFF/WAVE file
Compressed audiowav: audio format N is not PCM
Stereowav: N channels, expected mono
Wrong sample ratewav: sample rate N, expected 8000
Wrong bit depthwav: N bits per sample, expected 16

Caching in memory also means the files are read once: replacing a WAV on disk has no effect until the PBX restarts.

Converting

ffmpeg -i input.mp3 -ar 8000 -ac 1 -acodec pcm_s16le output.wav
FlagWhy
-ar 80008 kHz sample rate
-ac 1mono
-acodec pcm_s16le16-bit signed little-endian PCM

Check a file you did not produce yourself:

ffprobe -v error -show_entries stream=codec_name,sample_rate,channels,bits_per_raw_sample output.wav

Two practical notes on the source material: normalise before downsampling, since 8 kHz mono leaves no headroom to fix a quiet recording, and low-pass anything with music in it — content above 4 kHz cannot survive the sample rate and only adds aliasing.

Where files live

Every relative path resolves against one root: the directory -config names. It does not matter which document the path is written in.

kind: Extension
metadata: { name: "200", tenant: acme }
spec:
steps:
- type: play
file: ../sounds/welcome.wav # configs/ → sounds/ at the repository root
---
kind: System
spec:
moh:
default: ../sounds/moh/default.wav # the same prefix, from another kind

See Documents and paths for the full rule.

Music on hold classes

Music on hold is referenced by class name, never by path, so the file behind a class can differ per tenant:

kind: System
spec:
moh:
default: ../sounds/moh/default.wav
classes:
jazz: ../sounds/moh/jazz.wav
corporate: ../sounds/moh/corporate.wav
---
kind: Tenant
metadata: { name: acme }
spec:
domains: [acme.pbx.example.com]
moh:
default: ../sounds/moh/acme.wav # acme's own default
classes:
jazz: ../sounds/moh/acme-jazz.wav # overrides the global jazz

Resolution for a class name:

  1. the tenant's moh.classes;
  2. the global moh.classes;
  3. for the special name default, the tenant's moh.default then the global moh.default.

A step naming a class no level provides is a validation error, as is using default when neither level defines one.

Using a class:

- id: hold
type: moh
moh: jazz

- id: ring
type: dial
moh: default # caller hears music instead of ringback
target: {kind: ring_group, group: support}

Omitting moh on a dial step leaves the caller with normal ringback.

Length and looping

Music-on-hold classes loop until the call moves on. Announcements play once and then the plan continues.

A plan that runs off its last step hangs the caller up, so an announcement at the bottom of a file does not need a hangup after it. Writing one anyway makes the intent obvious, and it is required when the announcement is not the last step:

steps:
- id: closed
type: play
file: ../sounds/closed.wav
- id: done
type: hangup

Generating prompts

The fragments voicemail and the menus assemble are generated from a manifest rather than recorded by hand, and -tts can produce a one-off announcement in the same voice:

./bin/lyno -tts -text "We are closed for the holidays" -out sounds/closed.wav

Synthesis happens at configuration time and never during a call. See Prompt audio.