Skip to content

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

[org_<ctx>__<class>]
mode=files
directory=/var/lib/asterisk/moh/org_<ctx>__<class>

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:

exten => <queue_num>,n,Set(CHANNEL(musicclass)=<queue.music_on_hold>)

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

  1. Operator uploads a .wav file (or pastes a YouTube URL the editor downloads) via the MOH settings page.
  2. Editor sends the file to the API.
  3. mohService.saveTrack:
  4. Validates / re-encodes to 8 kHz mono via sox (rejects unfriendly inputs).
  5. Writes .wav, .ulaw, .alaw siblings into the org's MOH class directory.
  6. Triggers musiconhold reload via AMI so Asterisk picks up the new class.
  7. The queue config does NOT need a redeploy — only the MOH catalog changes.

Gotchas

  • MP3 files don't play. format_mp3 is intentionally not compiled in on prod Asterisk (security + maintenance cost). If a .mp3 file 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 .wav survives 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_hold set fall back to Asterisk's default class (the macroform tracks). This is intentional and safe — never blank.

Audio quality investigation runbook

When operators report jitter/crackle during hold or greeting playback:

  1. Confirm .ulaw/.alaw sibling files exist alongside any .wav. Missing siblings = live transcode.
  2. Confirm every .wav is 8 kHz: for f in <dir>/*.wav; do file "$f" | grep -oE "[0-9]+ Hz"; done. Anything other than 8000 Hz is the culprit.
  3. Audit MP3s anywhere under /var/lib/asterisk/moh/.
  4. 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 reload
  • api/src/services/asterisk/dialplanGenerator.jsgenerateQueueExtension for the musicclass Set
  • IVR Architecture — same .ulaw/.alaw sibling convention applied to greetings