Ren'Py does not have a built-in voice blip feature. This is not a limitation of the engine; it is a deliberate omission, because the dialogue system is designed to be extended through callbacks and config variables rather than through a fixed set of features. The callback mechanism is the hook that makes dialogue blips possible, and it has been part of Ren'Py's API for years.
The implementation is small: a Python function that responds to dialogue events, a character definition that uses the function as a callback, and a blip sound that loops for the duration of the text. The part that most tutorials get wrong is the pause handling. A blip that loops from the start of the dialogue to the end does not stop at punctuation, which means the sound continues through the pause after a comma or a period. The fix for that is a specific Ren'Py text tag, and it is the reason this article exists.
This article covers the character callback method, the
{w} tag that fixes the pause problem, the
per-character blip setup, and the blip parameters that
keep the sound from becoming fatiguing. The
generic typewriter blip guide
covers the underlying design principles; this article is
specific to Ren'Py's implementation.
How does the character callback method work?
The character callback is a Python function that Ren'Py
calls at specific points during a say statement. The
function receives an event string that
describes what happened, and it can play or stop sounds
based on that event. The events that matter for blips are
"show", which fires before the dialogue is
displayed, and "slow_done" or
"end", which fire when the text has finished
revealing[reference:0].
The official Ren'Py documentation provides a minimal
example of this pattern: a function called
beepy_voice that plays a looping sound on
"show_done" and stops it on
"slow_done"[reference:1]. The
"show_done" event fires after the first
segment of dialogue is shown, which is the moment the
text begins to reveal. The "slow_done"
event fires when the reveal completes.
init python:
def blip_voice(event, interact=True, **kwargs):
if not interact:
return
if event == "show_done":
renpy.sound.play("audio/blip.ogg", channel="blip", loop=True)
elif event == "slow_done":
renpy.sound.stop(channel="blip", fadeout=0.05)
define e = Character("Eileen", callback=blip_voice)
The channel="blip" argument is the part that
most tutorials omit. Ren'Py has three built-in audio
channels: music, sound, and
voice[reference:2]. The voice
channel is reserved for voice acting and has
voice-specific behavior (it stops automatically when a
new line begins). Using it for blips would conflict with
actual voice acting if the game has it. The
sound channel is shared with all other sound
effects. A custom channel is the clean solution, because
it allows the blip volume to be adjusted independently
and prevents the blip from interfering with other sounds.
Registering the custom channel is a one-time setup in an
init python block:
init python:
renpy.music.register_channel(
"blip", mixer="sfx", loop=True
)
The mixer="sfx" argument tells Ren'Py to use
the sound effects volume slider for this channel, which
means the player can adjust the blip volume through the
existing settings menu. The
mix hierarchy guide
covers the broader routing logic; in Ren'Py, the mixer
assignment is the routing.
Why does the blip continue through pauses, and how do you fix it?
The looping blip plays from the start of the dialogue to
the moment "slow_done" fires. If the
dialogue contains a comma, a period, or any other
punctuation that creates a natural pause in the reading,
the blip continues through the pause. The player hears
the blip during a moment when no text is being revealed,
which is the opposite of what a voice blip is supposed to
do[reference:3].
The fix is the {w} text tag. Unlike a comma
or a {cps} change, {w} creates
an explicit pause that Ren'Py's event system can detect.
When the dialogue reaches a {w} tag, the
"slow_done" event fires, the blip stops, and
then the dialogue resumes after the specified wait[reference:4].
e "The door was locked... {w=0.4} or so I thought."
The {w=0.4} tag pauses the dialogue for 0.4
seconds after the ellipsis. The blip stops when the tag
is reached and does not play during the pause. When the
dialogue resumes, the "show" event fires
again (because Ren'Py segments the dialogue at the
{w} tag), and the blip starts again[reference:5].
The practical consequence is that every pause in the
dialogue needs to be a {w} tag rather than a
punctuation mark. A comma is a visual pause but not an
event pause; the blip continues through it. A
{w=0.2} after the comma stops the blip and
restarts it after the pause, which matches the rhythm of
speech.
The {p} tag is an alternative for a
player-controlled pause (the dialogue waits for a click).
It also triggers the "slow_done" event, so
the blip stops until the player advances. The
typewriter blip guide
covers the general principle that the blip should track
the reveal, not the line.
How do you give each character a different blip voice?
The callback function can accept additional arguments
through the cb_ prefix. A character defined
with cb_blipfile="audio/blip_high.ogg" will
pass that argument to the callback, which can use it to
play a different sound for that character[reference:6].
init python:
def blip_voice(event, interact=True,
blipfile="audio/blip_default.ogg", **kwargs):
if not interact:
return
if event == "show_done":
renpy.sound.play(blipfile, channel="blip", loop=True)
elif event == "slow_done":
renpy.sound.stop(channel="blip", fadeout=0.05)
define e = Character("Eileen", callback=blip_voice,
cb_blipfile="audio/blip_high.ogg")
define m = Character("Marcus", callback=blip_voice,
cb_blipfile="audio/blip_low.ogg")
The pitch separation between characters is the same as the separation in any dialogue system: at least 150 Hz apart for reliable identification, with the protagonist as the reference. A high character at 1400 Hz and a low character at 900 Hz are distinguishable; two characters at 1100 Hz and 1200 Hz are not. The visual novel blip guide covers the per-character frequency ranges in more detail.
An alternative to separate sound files is a single blip
with pitch variation. The callback can use
renpy.sound.play with a different pitch
argument, or the blip file itself can be generated with
freq_variance and a set of variants. The
offline synthesis approach described by one Ren'Py
developer generates 8 pitch variants per character and
cycles through them, which produces natural variation
without requiring separate files for every character[reference:7].
The simplest implementation for a small cast is separate files. The simplest implementation for a large cast is a single file with runtime pitch shifting. The randomized variation guide covers the distribution question; the Ren'Py-specific part is that the variation happens at the callback level, not within a single playback.
What blip parameters work best for Ren'Py dialogue?
The blip parameters are the same as for any visual novel dialogue, because the perceptual constraints do not change with the engine. The blip should be short (30 to 60 milliseconds), in a mid-range frequency (800 to 1400 Hz), and free of strong harmonics (sine or triangle waveform).
| Parameter | Range | Why |
|---|---|---|
| Duration | 30–60 ms | Short enough to loop without overlapping |
| Frequency | 800–1400 Hz | Mid-range, readable on laptop speakers |
| Waveform | Sine or triangle | Low fatigue over long sessions |
| Pitch variation | ±3% | Breaks repetition without changing identity |
| Gap between blips | Implicit in loop | The loop restarts as the text reveals |
The loop behavior is specific to Ren'Py. Because the blip
plays with loop=True, the sound repeats
continuously until "slow_done". The
effective blip rate is the duration of the blip file, not
the character reveal rate. A 50-millisecond blip file
produces 20 blips per second, which is above the rate the
ear can distinguish. A 100-millisecond blip file produces
10 blips per second, which is in the comfortable range.
This is different from the character-by-character approach used in Unity or custom engines, where the blip fires once per character. In Ren'Py, the blip is a looping sound that the engine starts and stops, and the rate is determined by the file's duration. The Web Audio blip tutorial covers the per-character approach; the Ren'Py approach is the loop-based equivalent.
The loop-based approach has one advantage: it does not depend on the text speed. A player who changes the text speed from 30 to 60 characters per second will hear the same blip rate, because the loop rate does not change. The per-character approach would double the blip rate with the text speed, which is usually the wrong behavior.
The one caveat is that the loop must be seamless. A blip file with a fade-out will produce a gap between repetitions, which the player hears as a stutter. A blip file that starts and ends at full amplitude will loop cleanly. The file size reduction guide covers the trimming and normalization process; for a looping blip, the file should be trimmed so that the end and the start are at the same amplitude.
How do you handle the skip and auto-advance modes?
Ren'Py has a skip mode that advances through dialogue at high speed, and an auto-advance mode that advances after a delay. Both modes interact with the blip callback in ways that need to be handled.
In skip mode, the dialogue is advancing faster than the
blip loop can meaningfully track. The
"slow_done" event fires almost immediately
after "show_done", which means the blip
plays for a few milliseconds and then stops. The result
is a rapid series of blip fragments, which sounds like a
stutter rather than a voice[reference:8].
The fix is to disable the blip when the game is in skip mode. Ren'Py provides a way to check the current skip state, and the callback can return early if the game is skipping:
if renpy.is_skipping():
return
The renpy.is_skipping() function returns
True when the player is holding the skip key or has
enabled skip mode. Checking it at the start of the
callback prevents the blip from playing during skip,
which is the correct behavior because the player is not
reading the text at skip speed.
In auto-advance mode, the dialogue advances after a delay
that depends on the text length. The blip plays normally,
because the text is revealed at the normal rate and the
"slow_done" event fires when the reveal
completes. The only adjustment is that the auto-advance
delay might be longer than the blip loop, which means
the blip stops before the next line begins. That is
correct: the blip should stop when the text stops
revealing, not when the next line starts[reference:9].
The visual novel blip guide
covers the skip-mode problem in more detail; the Ren'Py
implementation is the renpy.is_skipping()
check, which is simpler than the custom state machines
that other engines require.
What happens when the game also has voice acting?
A game that has both voice acting and blips needs to decide which one plays. The two sounds serve the same function, and playing both at once produces a muddled result where the voice is competing with the blip for the player's attention.
The standard approach is to disable the blip when the
line is voiced. The dialogue system knows whether a line
has a voice file, and the callback can check a variable
before playing the blip. In Ren'Py, the
voice statement is used for voice acting, and
the callback can check whether a voice file is currently
playing:
if renpy.music.is_playing(channel="voice"):
return
The alternative is to keep the blips at a reduced volume during voiced lines. The custom channel registered for the blip can be ducked when the voice channel is active, either through a script that monitors the voice channel or through a separate callback that adjusts the blip volume. The ducking guide covers the sidechain approach; in Ren'Py, the implementation is a manual check rather than an automatic sidechain.
The choice between disabling and ducking depends on the game's aesthetic. A visual novel with full voice acting usually disables the blips, because the voice is the primary audio and the blips are redundant. A game with partial voice acting (grunts and exclamations rather than full lines) keeps the blips at full volume and lets the two sounds coexist, because the voice is not trying to be the primary channel.
Ren'Py's callback system is the mechanism that makes
dialogue blips possible, and the {w} tag is
the mechanism that makes them stop at the right moments.
The implementation is small — a callback function, a
character definition, and a blip file — and it does not
require a custom dialogue system or a third-party
plugin. The blip design itself follows the same
principles as any dialogue blip: short, mid-range, and
varied enough to avoid fatigue. The Ren'Py-specific
part is the loop-based playback and the callback
events, and both are documented in the engine's own
reference.
Generate a Looping Blip File for Ren'Py
Open the SfxMaker blip generator and create a blip that is 50 to 100 milliseconds long, in the 800 to 1400 Hz range, with a sine or triangle waveform. Trim the file so that the start and end are at the same amplitude, then export it as an OGG or WAV file. The seamless loop is what allows Ren'Py to play the blip continuously without a gap between repetitions.
Open Blip Sound Generator →