Architecture
Mutara is one Go program. It does not use Asterisk. Every connection to the node is a port, and one mixer joins the ports.
The parts of the program #
Text version of figure 1
- The node core has the 20 ms mixer, the keying, the timers and the announcements.
- AllStarLink nodes and IAX2 apps connect to the IAX2 engine on UDP port 4569. The AllStar manager gives the core one port for each link and each user.
- The radio part gives the core one port for the USB radio interface.
- Browsers and the iPhone, iPad and Android apps connect to the web server. It gives the core one port for each listener.
Ports #
A port sends and receives frames of 20 ms. A frame is 960 samples of 48 kHz, 16-bit audio. The node does not know the protocol of a port. It knows only if the port is local (a radio) or on the network.
| Port | Keying in | Keying out |
|---|---|---|
| AllStar link | Voice frames, RADIO_UNKEY, or a 500 ms gap | Voice, then RADIO_UNKEY and comfort noise |
| IAX2 user | *99 and #, VOX, any voice, or RADIO_KEY | Voice |
| Radio | COS or CTCSS on a HID input, or VOX | PTT on HID GPIO3 |
| Browser | The talk button | Always while there is audio |
| Local link | Frames from the other node of the install | Frames to the other node, with no codec |
The 20 ms cycle #
Every 20 ms, the node does four steps:
- Pull. The node takes one frame from the jitter buffer of each port.
- Timers. The node applies the timeout timer and decodes DTMF on local ports. It decides on the ID and the courtesy tone.
- Mix. The node makes a separate mix for each port, without the audio of that port. This is a mix-minus.
- Send. The node sends each mix to its port, or unkeys the port. A local port stays keyed for the hang time.
The mix rules are on the Audio, timers and levels page.
Codecs at the edge #
The mixer always works at 48 kHz. Each port changes the audio to its own format:
- AllStar links and IAX2 users use 8 kHz μ-law (ulaw).
- Browsers and the apps use 48 kHz Opus: 20 ms frames, constant bit rate, in-band FEC. Node audio is 24 kb/s, and room audio is 32 kb/s. Listeners who hear the same audio share one encode.
- The radio uses the sound card at 48 kHz. Its filters also work at 48 kHz. Receive audio goes to 5 kHz by default.
- YSF, DMR and M17 modules send their voice frames (AMBE+2 or Codec2 3200) to the transcoder server and get back 8 kHz PCM. The module changes it to and from 48 kHz.
Read The audio path.
The Linux amd64 and arm64 releases and the iPhone, iPad and Android apps use libopus 1.6.1 in C, with NEON on arm64. It is linked statically, so the release builds need no system libraries to run. On a Raspberry Pi 3 it encodes and decodes 2.5 to 3.8 times faster than the Go version. Only the armv7 and armv6 releases and a plain go build use the same libopus code, translated to Go by a tool (ccgo), with CGO_ENABLED=0.
Several nodes: modules #
One Mutara process can run more than one node. Each node is a module with a type and an ID. The type is allstar, ysf, dmr or m17. The ID of an allstar module is its node number. The ID of a ysf, dmr or m17 module is the name of its file, for example ysf-club, dmr-3100 or m17-club. The first module is in mutara.yaml. Each other module has its own file in modules.d. Settings, Modules adds and removes these files while Mutara runs.
- All modules share the IAX2 port. A call goes to the module of the node that it dials.
- Only the first module can have a radio.
- All modules share one set of accounts, one web server and one update service.
- Each module has its own core, links, timers, clips, chat and favorites.
- A module restarts alone when its settings change. An internal error stops only that module.
- When an admin removes a module, Mutara stops it and moves its state folder to
modules/.removed. It deletes nothing in the folder.
Two modules of one install link inside the process. A local link carries 48 kHz audio with no codec. It acts as an AllStar link: it has the modes, DTMF, permanent links, loop checks, link lists and the names of the talkers.
Text version of figure 2
- One IAX2 engine listens on UDP port 4569.
- A call to node 2100 goes to module 2100. A call to node 2101 goes to module 2101.
- Module 2100 has the radio.
- The two modules link to each other inside the process, with 48 kHz audio.
- One web server and one set of accounts serve the two modules.
The YSF module #
A ysf module is a client of one YSF reflector over UDP. It has no radio. Its voice uses the AMBE+2 codec, which Mutara does not have. A transcoder server (tcdpool) converts the voice over a WebSocket with TLS. All YSF, DMR and M17 modules of the install share one pool of sessions at the server. A call takes a session when it starts and gives it back when it ends. Read tcdpool. Without the server, the module stays linked but passes no voice, and it sends no empty frames to the reflector.
- Half duplex into the reflector. The reflector is busy while it sends voice and for 1 second after. No local transmission starts then. A transmission from a linked node that starts while the reflector is busy is held for all its length. The users and the node still hear it.
- One local talker. The reflector takes the first local talker, a user or the linked node. A second local talker is held until the first stops.
- Full duplex on the node. The AllStar node, its radio and its links hear each other and the reflector at all times.
- Levels. Auto level brings the audio from the reflector toward an average of −25 dBFS, with a limiter at −6 dBFS peak and at most 18 dB of boost by default. The audio to the reflector has a fixed vocoder level by default: TX AGC off and −9 dB from the AllStar level, so about −34 dBFS goes into the vocoder. With auto level off, a fixed gain from −12 to +24 dB applies. A level change applies at once.
- Timer.
tx_limitends a transmission to the reflector after 3 minutes by default. It is the only timer of the module.
An AllStar module of the same install links to the YSF module with its local number. The link is a local link and does not go in the link lists for the AllStar network.
The DMR module #
A dmr module is a client of one DMR master over UDP, with the MMDVM Homebrew protocol. It logs in as a hotspot with the login ID dmr_id × 100 + essid, and sends a ping each 10 s. With nothing from the master for 60 s, it logs in again. When the master refuses the login, it waits 5 minutes. It has no radio. It listens only until dmr.talk is on. Then it sends group voice calls to the talkgroup from users who may talk. Mutara is a client only. It does not serve hotspots or repeaters.
- One talkgroup. The module passes only group voice calls to its talkgroup on its timeslot. It drops and counts all other traffic, also private calls. It makes no dynamic links: the talkgroup must be static for the login ID at the network.
- One call at a time. A call is the stream ID of the packets. Stream ID 0 is accepted. A call with no header (late entry) starts at its first voice burst. A call with no terminator ends after 1 second with no voice.
- Voice. The transcoder server converts the AMBE+2 frames of each call to audio, as for a YSF module. The DMR FEC (BPTC, Golay, QR, RS and Hamming) is written in Go from the ETSI spec.
- Talker. The talker is
DMR <id>, for exampleDMR 3100002. With the DMR ID list on (dmr_db.enabled), the talker also has the callsign and the first name, for exampleW1AW John (DMR). - Talk. With
dmr.talkon, one local talker at a time sends a group voice call to the talkgroup. A call uses the user's own DMR ID, elsedmr_id. PTT waits while a call comes from the talkgroup.
A linked AllStar module hears the talkgroup. With talk off, nothing goes from the node to the master. With talk on and the link in transceive mode, the talkgroup also hears the node.
The M17 module #
An m17 module is a client of one module (A to Z) of an M17 reflector over UDP, with the protocol of urfd. It links with its link callsign, for example N0CALL A, and answers the pings of the reflector. With nothing from the reflector for 30 s, it links again, with a backoff of 1 to 30 s. When the reflector refuses the link, it waits 5 minutes. It has no radio. It listens only until m17.talk is on. Mutara is a client only. It does not serve M17 clients.
- One stream at a time. Each packet has the full header, so a stream that starts late plays from its first packet. A stream with no last packet ends after 1 second.
- Voice. The transcoder server converts the Codec2 3200 frames, as for a YSF module. The module does not play Codec2 1600, encrypted streams or packet data.
- Talker. The talker is the base callsign, for example
W1AW (M17). - Talk. With
m17.talkon, one local talker at a time sends a stream to the destination@ALL. The source is the user's own M17 callsign, else the account callsign, else the module callsign. The module sends no META data. - Known issue. urfd can drop a stream from an M17 client that sends the destination
M17-<reflector> <module>in place of@ALL, for example the QSO One app, before it gets to Mutara. A fix for urfd is in progress.
The scan #
The node does the scan, not the browser. The scan is one audio WebSocket. The node opens one listener port on each module that the user may listen to, selects one module and sends only its audio. A switch occurs at the first frame, so the start of a transmission is not lost. The browser and the Android app use the same stream.
- A module is active while the node sends it audio. With "skip IDs and announcements", only frames that carry a talker count. A module stops being active 200 ms after its last frame.
- The scan stays on the selected module while it is active and for the hang time after (2 to 30 s, default 5 s). Then it goes to the active module that started last. A tie goes to the install order.
- Each module has an effective priority from 0 to 99 for each user: the user's own value, else the default of the module. A module with a higher priority takes the scan at once. While the user talks back, and for the hang time after, the scan does not move. A higher module is then "waiting".
- The node applies the rule at each frame and every 50 ms. A changed priority applies at the next tick, and the stream does not connect again.
- Skip is for one connection. The node saves nothing for it. The browser keeps the missed marks of the clips.
Design decisions #
- One process, several modules
- Several nodes run in one process, not as several copies of Mutara. Thus they can share the IAX2 port, the accounts and the updates. A fault in one module does not stop the others.
- Local links with no codec
- Links between modules carry 48 kHz audio and act like AllStar links. Thus the commands, the loop checks and the dashboard work the same for both.
- An IAX2 engine of its own
- Mutara answers and places IAX2 calls itself. It sends call tokens and acknowledges each sequenced frame, as ASL3 nodes expect.
- The sound card clock drives the radio
- Each captured 20 ms block writes one 20 ms playback block. Network audio waits in a buffer of 60 ms. If the buffer is full, the oldest frame is dropped.
- PTT follows the transmit state
- The node sets PTT on each poll of the HID device. A lost signal cannot leave the transmitter keyed.
Roadmap #
Done: YSF modules, DMR modules and M17 modules, through a transcoder server. Later: P25 modules through the same server.
The transcoder server is a separate program, outside the node. It runs tcd, a C/C++ program, and a vocoder emulator. Its source is in the mutara-radio/tcdpool project on GitHub.