$ cargo new make_tone
Should result in a standard "Hello, World!" source file:
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.
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:
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.
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.
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:
And then we use the "build a new sine wave" code in rodio to build a new sine wave, which we'll name "source".
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.
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.)
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:
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