Music on Hold (MOH) Architecture¶
Canonical reference for how Music-on-Hold is stored, served, and chosen per org/queue on Astradial. Read this before touching mohService, uploading a new MOH track via the editor, or debugging audio quality during hold.
File layout on disk¶
/var/lib/asterisk/moh/
├── macroform-cold_day.{wav,ulaw,alaw} ← default system tracks (Asterisk-shipped)
├── manolo_camp-morning_coffee.{wav,ulaw,alaw}
├── reno_project-system.{wav,ulaw,alaw}
├── org_<ctx>__<class>/ ← per-org per-class dirs
│ ├── <track>.wav (8 kHz 16-bit, header)
│ ├── <track>.ulaw (raw 8 kHz mu-law, no header) ← what Asterisk actually plays for PCMU calls
│ └── <track>.alaw (raw 8 kHz a-law, no header) ← what Asterisk plays for PCMA calls
└── ...
The directory is the MOH "class" name Asterisk references in musiconhold.conf. Each org's queue declares a class via Set(CHANNEL(musicclass)=<class>) in its dialplan extension.
musiconhold.conf shape¶
mode=files cycles through files in the directory alphabetically. There's no shuffle or per-call randomisation — Asterisk plays files in ls-order.
File format rules¶
Asterisk's MOH cycler reads the same file pattern as Background() / Playback(): it looks for the format that matches the channel's codec FIRST (.ulaw for PCMU calls, .alaw for PCMA calls), then falls back to .wav which it transcodes on the fly.
The format hotfix from PR #163 (also applied to greetings — see IVR Architecture) writes BOTH .ulaw and .alaw sibling files alongside the source .wav so Asterisk never has to transcode on the wire. This eliminates the audible crackle/jitter under load that on-the-fly resampling caused.
| Format | Sample rate | Asterisk behaviour |
|---|---|---|
.ulaw (raw 8 kHz mu-law) | 8 kHz | Played byte-for-byte to PCMU calls — no transcode |
.alaw (raw 8 kHz a-law) | 8 kHz | Played byte-for-byte to PCMA calls — no transcode |
.wav (8 kHz 16-bit PCM) | 8 kHz | Transcoded to PCMU/PCMA on the fly — possible crackle under CPU load |
.wav at any other rate | ≠ 8 kHz | Resampled + transcoded — audible underwater/crackle, do not use |
.mp3 | n/a | format_mp3 is NOT loaded on prod Asterisk — silently fails or silence |
Rule: every MOH dir must contain .ulaw + .alaw sibling files. Source .wav is OK to keep for re-encoding but Asterisk should never have to read it during a call.
How a queue picks its MOH class¶
queues.music_on_hold column on the queue row → emitted by dialplanGenerator.generateQueueExtension:
Right before Queue(...) runs. The class name MUST match a context in musiconhold.conf — typos here mean the caller hears the default Asterisk MOH or silence.
Editor upload flow¶
- Operator uploads a
.wavfile (or pastes a YouTube URL the editor downloads) via the MOH settings page. - Editor sends the file to the API.
mohService.saveTrack:- Validates / re-encodes to 8 kHz mono via
sox(rejects unfriendly inputs). - Writes
.wav,.ulaw,.alawsiblings into the org's MOH class directory. - Triggers
musiconhold reloadvia AMI so Asterisk picks up the new class. - The queue config does NOT need a redeploy — only the MOH catalog changes.
Gotchas¶
- MP3 files don't play.
format_mp3is intentionally not compiled in on prod Asterisk (security + maintenance cost). If a.mp3file ends up in a MOH dir without sibling.wav/.ulaw/.alaw, callers hear silence. Audit with:find /var/lib/asterisk/moh -name "*.mp3" -not -name "._*". - Non-8 kHz
.wavsurvives MOH playback by being resampled on every frame — that resampling is a known CPU-hogger that causes "underwater" artifacts on busy servers. Always re-encode to 8 kHz BEFORE writing. - Per-org MOH dirs are NOT in git. They're per-VPS state under
/var/lib/asterisk/moh/. Backup these dirs separately if you care about restoring uploaded tracks. - Default class. Queues with no
music_on_holdset fall back to Asterisk'sdefaultclass (the macroform tracks). This is intentional and safe — never blank.
Audio quality investigation runbook¶
When operators report jitter/crackle during hold or greeting playback:
- Confirm
.ulaw/.alawsibling files exist alongside any.wav. Missing siblings = live transcode. - Confirm every
.wavis 8 kHz:for f in <dir>/*.wav; do file "$f" | grep -oE "[0-9]+ Hz"; done. Anything other than8000 Hzis the culprit. - Audit MP3s anywhere under
/var/lib/asterisk/moh/. - If files look clean, jitter is downstream (WireGuard tunnel, Tata SBC, or caller's carrier). Pull the MixMonitor recording for the call and check whether the same artifact is present there. If the recording is clean but the caller heard jitter, the issue is in transit — investigate the WireGuard tunnel handshake / packet loss first.
Where to read more¶
api/src/services/asterisk/mohService.js— encode + write + AMI reloadapi/src/services/asterisk/dialplanGenerator.js→generateQueueExtensionfor themusicclassSet- IVR Architecture — same
.ulaw/.alawsibling convention applied to greetings