VoiceBlender v0.15.0 is out. Most of it is about running in real networks: trunks that peer by IP address, deployments behind NAT, WebRTC clients that change network mid-call, and SIP ports exposed to the internet.
A second denoiser: GTCRN
denoise_gtcrn is a new noise suppression filter next to the RNNoise-based
denoise. On 8 and 16 kHz telephony audio it uses less than half the CPU and
removes more noise. RNNoise is still the better choice for 48 kHz rooms and
for AMD in very heavy noise. The details and measurements are in
a separate post.
IP-IP trunks
Until now a SIP trunk had to register with the upstream. A new trunk type,
ip_ip, describes a peer at a fixed address: a carrier trunk, an SBC or
another PBX.
- Calls go to a fixed
peer_uri, with optional digest credentials. - Inbound calls are matched to the trunk by source address
(
inbound_sources). - An optional OPTIONS ping tracks whether the peer is answering.
curl -X POST http://localhost:8080/v1/sip/trunks \
-H 'Content-Type: application/json' \
-d '{
"type": "ip_ip",
"ip_ip": {
"peer_uri": "sip:203.0.113.10:5060",
"inbound_sources": ["198.51.100.0/24"],
"options_ping_interval_seconds": 30
}
}'
ICE restart for WebRTC legs
A WebRTC leg no longer dies the moment its network path breaks, for example
when a phone moves from Wi-Fi to mobile data. After 5 seconds without media
the leg emits leg.ice_interrupted and stays up. The client then has 25
seconds to send a new offer to POST /v1/legs/{id}/ice-restart (or the
webrtc_ice_restart VSI command). leg.ice_restored fires when media is
back. Both timeouts are configurable.
SIP allowlist and inbound ring timeout
Two settings for SIP ports that face the internet:
SIP_ALLOWED_IPSis a list of addresses and CIDR ranges that SIP requests are accepted from, on UDP, TCP and TLS. Anything else is dropped with no SIP response, so a scanner only sees a timeout.SIP_INBOUND_RING_TIMEOUT_SECONDS(default 180) releases inbound calls that are never answered and never cancelled. Before, such a call could hold its leg forever.
Running behind NAT and in Docker
In a Docker bridge network VoiceBlender used to advertise the container’s own
address, which nothing outside the host can reach. Calls connected, but the
peer’s hangup never arrived. SIP_LOCAL_NETS fixes the common mixed case:
peers on the listed networks are given the LAN address, and everyone else
gets SIP_EXTERNAL_IP. One instance can now serve a carrier and LAN phones
together. The new “Docker and NAT” section in the configuration guide covers
the options.
Custom headers on transfers
POST /v1/legs/{id}/transfer accepts refer_to_headers. They are embedded in
the Refer-To URI, so the peer copies them into the INVITE it sends to the
transfer target. This is how you pass a conversation ID through a blind
transfer. It was tested against a Genesys Cloud BYOC trunk.
Behaviour changes to know about
- Outbound
ring_timeoutnow defaults to 60 seconds. It was unlimited before. Send0to keep the old behaviour. - Deleting a room cancels outbound calls still ringing towards it. They
end with reason
room_deleted. - Inbound custom headers are matched in any case.
x-fooandp-asserted-identityare now picked up as well asX-Foo, and reported exactly as received, so look them up case-insensitively.
Fixes worth calling out
- The Contact header follows the INVITE’s transport (
;transport=tcp,;transport=tlsorsips:), so in-dialog requests come back the right way telephone-eventis picked at the selected codec’s clock rate, so PCMU gets/8000even when the peer lists/48000firsta=rtcp-muxis answered only when it was offered- WebRTC and WhatsApp legs are removed when the peer closes its media
connection (DTLS close), with the new
peer_closedanddtls_faileddisconnect reasons
Also new since v0.12.0
The last release post here covered v0.12.0, so here is a short summary of what v0.13.0 and v0.14.0 added.
v0.13.0 (September 2026)
- Outbound SIP proxy. REGISTERs and INVITEs can be sent through a proxy,
set server-wide with
SIP_OUTBOUND_PROXY, per trunk, or per call. - Speechmatics STT, as another speech-to-text provider.
- Custom data on legs. Attach your own JSON to a leg with
custom_data. It is repeated on every event for that leg, and can be changed during the call over REST or VSI. - Mixer fix for any packet size. A peer sending 10 ms or 30 ms RTP packets used to lose half of its audio in the mix, recordings and transcription. From Andreas Granig (@agranig).
- Rooms transcribe and record every audio source, not only leg participants. Also from Andreas Granig.
- Click-free WebSocket audio. Audio sent in over WebSocket, LiveKit and MoQ no longer drifts against the mixer clock, which had caused a click roughly once a second.
- Fixes for RTP port reuse, the Contact URI on outgoing REGISTERs, TLS URI parsing, and SIPREC metadata checks (@agranig). Dan Jenkins co-authored fixes for agent audio pacing and WhatsApp call routing.
v0.14.0 (September 2026)
- Audio filters. A per-leg chain of filters on incoming audio:
denoise(RNNoise),bandpass,gain,pitch,roboticandvocoder. The denoise work has its own post. - About a third less CPU per room. The resampler’s inner loop is now AVX2 assembly on amd64 and NEON on arm64, and the mixer and RTP paths reuse their buffers. No API or configuration changes.
- Comfort noise per room. It can now be turned on or off for each room.
app_idin more places. It is now on ringing events for outbound trunk calls and on inbound registrations.- Better compatibility with SIP proxies (
rport, and REGISTER routing through an outbound proxy). - The configuration reference moved from the README to
CONFIGURATION.md.
Upgrading
go get github.com/VoiceBlender/voiceblender@v0.15.0
go build -o voiceblender ./cmd/voiceblender
Full changelog: v0.14.0…v0.15.0, or everything since v0.12.0.
Check the API Documentation for the trunk, ICE restart and transfer options.
Questions or feedback? Come say hi in our Discord - issues and PRs very welcome.