Kent West - kent.west@{that mail that swore to do no evil}
Assumptions
You are running Debian GNU/Linux, about version 12.
You have Rust installed.
You know how to create/edit/run a simple Rust program such as "Hello, World!".
Let's create "cw_keyer"
$ cargo new cw_keyer
Should result in a standard "Hello, World!" source file:
Create a single tone for starters
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:
Creating the tone is a multi-part process.
First, we have to create a "handle" on the default audio device, 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.
Second, we need to generate a sine wave. 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.
The third thing we need to do is to play the sine wave through our output handler:
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!
Make a 'dit' and a 'dah', and a character-separating silence
Let's convert our sound-code into a 'dit'.
This code should function exactly as it did before.
Let's get rid of "magic numbers"
However, the length of the 'dit' seems a little long. Also, "magic numbers" in our code, such as the 0.5 and the 500 and the 800,0, should usually be avoided. So let's swap out those "magic numbers" with meaningful constant names. So that the consts can be known throughout the program, we'll define them outside of any functions:
Let's copy/paste 'dit' into a 'dah'
Let's create a silent space to separate characters
Let's play a 'dit', followed by a character space, followed by a 'dah'
I'm showing the main() function only, not the entire program.
The timing may not be quite right after a fresh compile, so you may need to run it a second time after a fresh compile to hear it all.
Let's create a message
In reality, we're ready to start working on the code to listen for the keys from the keyboard that will represent our 'dit' and 'dah', but before we do that, just for fun, let's create a short Morse code message, "CQ".
Now if you cargo run your program, you should hear a Morse "C" followed by a "Q".
I'm going to add "CQ CQ de KC5ENO KC5ENO K".
When you cargo run this, you shuld get a pretty good approximation of "CQ CQ CQ de KC5ENO KC5ENO K". You may notice the timing is not perfect. I'm unsure why that is, and don't know how to fix it, but it's close enough for my purposes. Before moving on, for kicks, try changing the DIT_LENGTH to 75.
Let's move that "CQ" code into its own function.
Let's also make the code more generic, so it can easily play other messages. Notice I only coded the characters needed for my message; if you want a different message, you'll have to add the code for those characters you need.
Now we're ready to move on and add the key portion to our project.
Add the key
It's a simple keyer. We'll just loop forever listening for one of three keys to be pressed: a left-arrow for a 'dah'; a right-arrow for a 'dit', and a 'q' (or 'Q') to quit the program.
And rather than building all the code ourselves, reinventing the wheel, we can use a library already written for creating Text User Interfaces (TUIs). There are several, but termion is a good one:
$ cargo add termion
We'll add some use statements:
And we'll add a use_keyer function.
And that should be it. Enjoy your Morse Code keyer app!