Ratatui Audio with Rodio: Sound FX for Rust Text-based UI
Tags: rust, gamedev, tui
Published: 2024-06-26 17:47:13

---
title: "Ratatui Audio with Rodio: Sound FX for Rust Text-based UI"
published: "true"
description: "Ratatui audio with Rodio 🔊 adding sound effects to a 🦀 Rust Text-based user interface or Terminal app using the Rodio crate."
tags: "rust, gamedev, tui"
canonical_url: "https://rodneylab.com/ratatui-audio-with-rodio/"
cover_image: "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ysihz6f822dpc2zr7f1x.png"
---

## 🔊 Adding Sound FX to a Ratatui Game

In this post, we look at Ratatui audio with Rodio.  I have been building a text-based User Interface (**TUI**) game in Ratatui.  Ratatui is Rust tooling for **Terminal** apps.  This is the third post in the series. I wrote the initial post once I had put together a minimal viable product, and outlined some next steps in that post.  In the last post, I added fireworks to the victory screen by painting in a Ratatui canvas widget.

As well as fireworks, another enhancement that I identified (in the first post) was sound effects.  I have built Rust games with sound before, but relied on the game&rsquo;s tooling to add manage loading the audio assets and playing them (<a href="https://rodneylab.com/rust-for-gaming/#rust-for-gaming-macroquad">Macroquad</a> and <a href="https://rodneylab.com/rust-for-gaming/#rust-for-gaming-bevy">Bevy</a>, for example, make this seamless).  So, the first step was going to be finding a crate to play **MP3 audio**.  I discovered Rodio, which worked well, and was quick to get going with.

In the rest of this post, I talk about the Rodio integration, and some next steps for the game.  There is a link to the latest project repo, with full code further down.

## 🧱 Ratatui Audio with Rodio: What I Built

![Ratatui Audio with Rodio: Screen capture shows game running in the Terminal.  The main title reads “How did you do?”.  Below, text reads You nailed it. “You hit the target!”, and below that, taking up more than half the screen, are a number of colourful dots in the shape of a recently ignited firework.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/prsy8uxl974crx0hwkya.png)

I didn&rsquo;t really want background music, just sound effects to play when the player starts the challenge, and then to provide audio feedback if their latest solution attempt was good or perfect.   Finally, I wanted to play some audio when each firework on the victory screen ignited.  I found some quite small wave files for each of these, and converted them to MP3s.

## About Rodio

Rodio uses a number of lower level crates under the hood, simplifying adding audio via their single higher level API.  These, lower-level, crates include:

- `cpal` for playback;
- `symphonia` for MP4 and AAC playback;
- `hound` for WAV playback;
- `lewton` for Vorbis playback; and
- `claxon` for FLAC playback.

I just needed MP3s, so disabled default features, and only added `symphonia-mp3` back in the project `Cargo.toml`, to keep the binary size in check:

```toml
[package]
name = "countdown-numbers"
version = "0.1.0"
edition = "2021"
license = "BSD-3-Clause"
repository = "https://github.com/rodneylab/countdown-numbers"
# ratatui v0.26.3 requires 1.74.0 or newer
rust-version = "1.74"
description = "Trying Ratatui TUI 🧑🏽‍🍳 building a text-based UI number game in the Terminal 🖥️ in Rust with Ratatui immediate mode rendering."
 
[dependencies]
num_parser = "1.0.2"
rand = "0.8.5"
ratatui = "0.27.0"
rodio = { version = "0.18.1", default-features = false, features = ["symphonia-mp3"] }
```

## 🐎 Adding Rodio

The <a href="https://docs.rs/rodio/latest/rodio/">Rodio docs</a> give a couple of examples for getting going.  Those examples read and decode an ogg file from a local folder within the project.  This makes uses of the Rodio decoder struct, either appending it to a Rodio sink, or playing decoder output directly on an output stream.  Either way, the audio is decoded and played straight away.

That setup works well for longer files, played once.  For the game, I have a few sound effects that might be played dozens of times.  It seemed a little extravagant to read from a file and decode each time I needed to play the same sound effect.  Luckily, I found a <a href="https://stackoverflow.com/questions/67988070/how-to-read-a-file-from-within-a-move-fnmut-closure-that-runs-multiple-times">Stack Overflow post with an alternative approach</a>, letting you buffer the decoder output.  Since the audio files were no bigger that `10 KB`, I was happy to buffer them as the app starts up and keep them in memory.

### Rust Code
I created a `SoundEffects` struct for holding all the sound effects, as there are only five of them.  For an app with more, I would attempt a cleaner solution, but this approach keeps things simple for what I have.

```rust
use std::{fs::File, path::Path};

use rodio::{
    source::{Buffered, Source},
    Decoder,
};

pub struct SoundEffects {
    pub start: Buffered<Decoder<File>>,
    pub end: Buffered<Decoder<File>>,
    pub perfect: Buffered<Decoder<File>>,
    pub valid: Buffered<Decoder<File>>,
    pub firework: Buffered<Decoder<File>>,
}

fn buffer_sound_effect<P: AsRef<Path>>(path: P) -> Buffered<Decoder<File>> {
    let sound_file = File::open(&path)
        .unwrap_or_else(|_| panic!("Should be able to load `{}`", path.as_ref().display()));
    let source = Decoder::new(sound_file).unwrap_or_else(|_| {
        panic!(
            "Should be able to decode audio file `{}`",
            path.as_ref().display()
        )
    });

    source.buffered()
}

impl Default for SoundEffects {
    fn default() -> Self {
        SoundEffects {
            start: buffer_sound_effect("./assets/start.mp3"),
            end: buffer_sound_effect("./assets/end.mp3"),
            perfect: buffer_sound_effect("./assets/perfect.mp3"),
            valid: buffer_sound_effect("./assets/valid.mp3"),
            firework: buffer_sound_effect("./assets/firework.mp3"),
        }
    }
}
```

The default initializer for the `SoundEffects` struct just creates a buffered decoding for each of the effects, which can be called later from the app code.

In app code, I can then clone one of these buffers and add it to a sink to play it.  For example in the main game loop:

```rust
fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
    // ...TRUNCATED   
 
    let (_stream, stream_handle) = OutputStream::try_default().unwrap();
    let sink = Sink::try_new(&stream_handle).unwrap();
    let sound_effects = SoundEffects::default();
 
    loop {
        if event::poll(timeout)? {
            if let Event::Key(key) = event::read()? {
              // ...TRUNCATED
 
                match app.current_screen {
                  // ...TRUNCATED
                    CurrentScreen::PickingNumbers => match key.code {
                        KeyCode::Enter => {
                            if app.is_number_selection_complete() {
                                app.current_screen = CurrentScreen::Playing;
                                sink.append(sound_effects.start.clone());
                            }
                        }
                    }
                }
            }
        }
        // TRUNCATED...
    }
}
```

We initialize the sink ahead of the main loop, then play the buffered sound from within it (line `19`), by calling `sink.append()`.  `sink.append()` pushes the buffer into a queue and plays it immediately (if nothing is already playing), or waits until the last sound has finished before starting.  That setup works here, and if you need to play sounds simultaneously, you can create multiple sinks.  This setup also avoids borrow checker issues with consuming the `File` struct, which might arise when decoding the `File` within the main loop.

## 🙌🏽 Ratatui Audio with Rodio: Wrapping Up

In this Ratatui audio with Rodio post, I briefly ran through how I added audio to the Ratatui Countdown game.  In particular, I talked about:

- why I **added Rodio**;
- why **you might buffer Rodio to avoid borrow checker issues in a game loop**; and
- how I **buffered MP3 sound FX with Rodio**.

I hope you found this useful.  As promised, you can <a href="https://github.com/rodneylab/countdown-numbers">get the full project code on the Rodney Lab GitHub repo</a>.  I would love to hear from you, if you are also new to Rust game development.  Do you have alternative resources you found useful? How will you use this code in your own projects?

## 🙏🏽 Ratatui Audio with Rodio: Feedback

If you have found this post useful, see links below for further related content on this site.  Let me know if there are any ways I can improve on it. I hope you will use the code or starter in your own projects. Be sure to share your work on X, giving me a mention, so I can see what you did. Finally, be sure to let me know ideas for other short videos you would like to see. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please <a aria-label="Support Rodney Lab via Buy me a Coffee" href="https://rodneylab.com/giving/">consider supporting me through Buy me a Coffee</a>.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via <a href="https://twitter.com/messages/compose?recipient_id=1323579817258831875">@askRodney</a> on X (previously Twitter) and also, join the <a href="https://matrix.to/#/%23rodney:matrix.org">#rodney</a> Element Matrix room. Also, see <a aria-label="Get in touch with Rodney Lab" href="https://rodneylab.com/contact/">further ways to get in touch with Rodney Lab</a>. I post regularly on <a href="https://rodneylab.com/tags/gaming/">Game Dev</a> as well as <a href="https://rodneylab.com/tags/rust/">Rust</a> and <a href="https://rodneylab.com/tags/c++/">C++</a> (among other topics). Also, <a aria-label="Subscribe to the Rodney Lab newsletter" href="https://newsletter.rodneylab.com/issue/latest-issue">subscribe to the newsletter to keep up-to-date</a> with our latest projects.