kentwest.neocities.org

Rust - Generate An Audio Sine-wave Tone

Kent West - kent.west@{that mail that swore to do no evil}

  1. Assumptions

    1. You are running Debian GNU/Linux, about version 12.
    2. You have Rust installed.
    3. You know how to create/edit/run a simple Rust program such as "Hello, World!".
  2. Let's create "make_tone"

  3. $ cargo new make_tone

    Should result in a standard "Hello, World!" source file:

    src/main.rs
    fn main() {
        println!("Hello, world!");
    }
    
  4. Create a single tone

    1. Tell Cargo that we'll be using the Rodio crate

    2. Rather than reinvent the wheel creating our own sound routines from scratch, let's use someone else's work. There are several possibilities, but a good choice for us is to use the rodio crate. If you want to read up on it, mosey on over to crates.io and search for "rodio".

      $ cargo add rodio should add the rodio crate to our project. You can verify that by looking at your Cargo.toml file before and after this command.

    3. Tell our program what paths to search for the needed Rodio pieces

    4. Now that Cargo knows about rodio, we'll need to tell our Rust program what pieces of this new crate we'll be using, and we can get rid of the "Hello, World" line:

      src/main.rs
      use rodio::source::{SineWave, Source};
      use rodio::OutputStream;
       
      fn main() {
          println!("Hello, world!");
      }
      
    5. Creating the tone is a four-step process.

      1. First, we have to create a "handle" on the default audio device (our computer speakers, or Bluetooth earbuds, etc), so we can "grab" hold of it and put sound into it, etc.

      2. src/main.rs
        use rodio::source::{SineWave, Source};
        use rodio::OutputStream;
        
        fn main() {
          // Create an output stream
          let (_stream, stream_handle) = OutputStream::try_default().expect("Could not open default audio device.");
        } // main()
        

        The OutputStream::try_default() method (or you might think of it as a function or a procedure or a "thing that the rodio crate knows how to do") returns two values: an "Output stream", and a "handle" for that Output stream. To be honest, I don't entirely know what is meant by "Output stream" in this context, but I do know we don't need it, so we tell Rust that even though we're collecting the value, we won't be using it in our program. We do this by placing an underscore at the front of our receiving variable, "_stream". We are assigning two variables at once in this line, one to recieve the returned stream (which as mentioned just now, we're ignoring), and the other to receive the handle for the stream, "stream_handle". (A single "package" that holds multiple variables like this is called a "tuple" (no relation to "two"; it could be three or thirty or three thousand variables).)

        We tell this command to try and grab the default sound device in the system (::try_default()), and that if we don't get what we expect (.expect("Could not open default audio device.");), to print out a message and to "crash" the program at that point. We could optionally just crash without the message (.unwrap();), or handle the error more gracefully, which often takes more coding than it's worth.

      3. Second, we need to generate a sine wave.

      4. You may know already that an audio tone is just a repeating sine wave. We have to define the sine wave's frequency and duration. 800 hertz is a fairly comfortable frequency, and let's just make it half a second long.

        A lot of things in Rust don't understand things like "3 seconds"; it wants this information defined in a special type of variable called a "Duration" type of variable. The "build a new sine wave" function of rodio is one of these things that wants its time-units in the "Duration" format. Although the definition of and manipulation of the "Duration" type is part of the Rust standard coding, it's not an "immediately standard" part; we'll have to tell our Rust program where to find the "Duration" code:

        src/main.rs
        use rodio::source::{SineWave, Source};
        use rodio::OutputStream;
        use std::time::Duration;
        
        fn main() {
          // Create an output stream
          let (_stream, stream_handle) = OutputStream::try_default().expect("Could not open default audio device.");
        } // main()
        

        And then we use the "build a new sine wave" code in rodio to build a new sine wave, which we'll name "source".

        src/main.rs
        use rodio::source::{SineWave, Source};
        use rodio::OutputStream;
        use std::time::Duration;
        
        fn main() {
          // Create an output stream
          let (_stream, stream_handle) = OutputStream::try_default().expect("Could not open default audio device.");
         
          // Generate a sine wave
          let source = SineWave::new(800.0).take_duration(Duration::from_secs_f32(0.5));
        } // main()
        

        The new definition for our sine wave will be stored in a variable called "source". The SineWave::new(800.0) uses rodio to build a new sine wave of 800 hertz; this code expects the frequency in hertz, and for it to be a "float" type of value (containing a decimal point) instead of an "integer" (which doesn't contain decimal points, as they are whole, or complete, numbers, not fractional).

        The .take_duration(Duration::from_secs_f32(0.5)); piece tells the "build a new sine wave" method that the duration of the sine wave will be 0.5 seconds. We could have also written this as .take_duration(Duration::from_millis(500));, because 500 milliseconds is just another way to describe half a second.

      5. The third thing we need to do is to send the sine wave to our output handler, telling it to play the sine wave:

      6. src/main.rs
        use rodio::source::{SineWave, Source};
        use rodio::OutputStream;
        use std::time::Duration;
        
        fn main() {
          // Create an output stream
          let (_stream, stream_handle) = OutputStream::try_default().expect("Could not open default audio device.");
        
          // Generate a sine wave
          let source = SineWave::new(800.0).take_duration(Duration::from_secs_f32(0.5));
         
          // Play the tone
          let _ = stream_handle.play_raw(source.convert_samples()).expect("Unable to play sine wave.");
          
        } // main()
        

        Because the "play_raw" method of an OutputStream handler returns a value, we have to receive that value, but again, since we won't be using that value, we just assign it to "_", which both tells Rust we won't be using this value and to not even bother giving it a name, unlike before, when we named our ignored variable as "_stream". (For reasons, the former value needs a name; try it without and see what happens.)

      7. Pause the program long enough for the audio to play itself out.

      8. You'd think that at this point, we'd be able to hear a tone, but surprisingly, the program doesn't wait the half-second to let the tone play before it continues to the next piece of the program, which in this case is the end of the program. So the program quits before the tone plays.

        The fix is to tell the program to pause for as long as the tone is defined to play:

        src/main.rs
        use rodio::source::{SineWave, Source};
        use rodio::OutputStream;
        use std::time::Duration;
        
        fn main() {
          // Create an output stream
          let (_stream, stream_handle) = OutputStream::try_default().expect("Could not open default audio device.");
        
          // Generate a sine wave
          let source = SineWave::new(800.0).take_duration(Duration::from_secs_f32(0.5));
        
          // Play the tone
          let _ = stream_handle.play_raw(source.convert_samples()).expect("Unable to play sine wave.");
         
          // Sleep for the duration of the tone
          std::thread::sleep(Duration::from_millis(500));
          
        } // main()
        

        Now we're ready to save our code, compile it, and run it to see if we're successful.

        $ cargo run

        Success! Yay!

To see more along these lines, Build a Morse Code Keyer app.

Go to top of page