Melody
This program plays the opening of Beethoven's Ode to Joy, one note at a time, through the console speaker. There is no portable way to make a sound and no standard package that wraps one, so it does what a systems language is for: it calls the operating system directly.
The function it calls is Beep in Windows' Kernel32 library, which plays a tone of a given frequency for a given number of milliseconds — exactly the shape of a musical note. That makes this the checkpoint for Part 24: Platform, and the last project in the course. It builds and runs only on Windows, and it says so clearly everywhere else.
How it is put together
| Piece | Its job | Lessons it uses |
|---|---|---|
when #target.os | Declares Beep on Windows, stops the build elsewhere | When, Target, Compile error |
#Link and extern | Binds Beep to Kernel32 | Extern |
C4 … G4 | Note frequencies as named constants | Const |
Note and the tune array | The music, as data | Struct, Array |
Main | Plays each note, stops at the first failure | For, Boolean |
When Main calls Beep, nothing in Rux sits in between — the call goes straight to the system library:
flowchart LR
main(["Main<br/>for note in tune"]) -- "Beep(frequency, duration)" --> decl["extern Beep<br/>declared in Rux"]
decl -- "linked by #Link" --> dll["Kernel32.dll"]
dll --> spk["console speaker"]
dll -- "bool32: did it play?" --> mainWindows only, on purpose
The declaration of Beep sits inside a compile-time when, which picks a branch while compiling rather than while running:
when #target.os {
.Windows => {
#Link("Kernel32.dll")
extern {
func Beep(frequency: uint32, duration: uint32) -> bool32;
}
},
else => #Error("Melody plays through the Windows Beep function, so it builds only for Windows")
}
On Windows the first branch declares Beep and says which library holds it. On any other system there is no Beep to call. Without the else branch the build would still stop there, but with error: no arm of this 'when' matches .Linux — accurate, and no help to someone who only wanted to hear a tune. #Error stops it with a sentence a person can act on instead. Checking for Linux shows it:
rux check --target linux-x86_64
error: Melody plays through the Windows Beep function, so it builds only for Windows
The branches when skips are never even type-checked, which is why the Windows-only declaration costs other systems nothing.
The tune as data
Raw frequencies such as 330 and 392 mean nothing on the page, so each note gets a name. Then the tune is an array of Note structs that reads almost like a score:
const C4: uint32 = 262;
const D4: uint32 = 294;
const E4: uint32 = 330;
const F4: uint32 = 349;
const G4: uint32 = 392;
Note { name: "E", frequency: E4, milliseconds: 400 },
Note { name: "E", frequency: E4, milliseconds: 400 },
Note { name: "F", frequency: F4, milliseconds: 400 },
Note { name: "G", frequency: G4, milliseconds: 400 },
The constants are typed uint32, because that is what Beep takes and what the frequency field holds. An untyped constant would be an int, and every note would be refused — see Common mistakes.
A foreign failure, checked by hand
A foreign function does not report failure through a Rux fallible. Beep returns a bool32 that is false when no tone could be played — and the compiler will not insist that you look at it. The program looks anyway, and stops at the first note that fails rather than printing notes nobody heard:
for note in tune {
Print("{} ", note.name);
if !Beep(note.frequency, note.milliseconds) {
PrintLine();
PrintLine("Windows could not play a tone, so the tune stops here.");
return 1;
}
}
Beep also blocks until the note has finished, so the loop needs no pauses of its own: the durations in the tune are the rhythm. The whole tune lasts about six seconds.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// Playing a tune by asking the operating system to sound one note at a time.
//
// There is no portable way to make a sound, and no standard package wraps one, so this program
// calls Windows directly: `Beep(frequency, duration)` in Kernel32 plays a tone of that many
// hertz for that many milliseconds, which is exactly the shape of a note. It is the extern
// declaration and `#Link` from the Extern lesson, put to work.
//
// That makes the program Windows-only, and it says so where it matters: on any other system the
// `when` below stops the build with an `#Error`, instead of letting it fail to link a function
// that does not exist there.
//
// A foreign function reports failure its own way, not through a Rux fallible. `Beep` returns a
// `bool32` that is false when no tone could be played, so every call is checked, and the tune
// stops at the first note that fails rather than printing notes nobody heard.
import Core::{ #Error, #target };
import Io::{ Print, PrintLine };
when #target.os {
.Windows => {
#Link("Kernel32.dll")
extern {
func Beep(frequency: uint32, duration: uint32) -> bool32;
}
},
else => #Error("Melody plays through the Windows Beep function, so it builds only for Windows")
}
// Note frequencies in hertz, near the middle of a piano. Naming them keeps the tune readable as
// music rather than as numbers.
const C4: uint32 = 262;
const D4: uint32 = 294;
const E4: uint32 = 330;
const F4: uint32 = 349;
const G4: uint32 = 392;
struct Note {
name: char8[..];
frequency: uint32;
milliseconds: uint32;
}
func Main() -> int {
// The opening of Beethoven's Ode to Joy.
let tune: Note[15] = [
Note { name: "E", frequency: E4, milliseconds: 400 },
Note { name: "E", frequency: E4, milliseconds: 400 },
Note { name: "F", frequency: F4, milliseconds: 400 },
Note { name: "G", frequency: G4, milliseconds: 400 },
Note { name: "G", frequency: G4, milliseconds: 400 },
Note { name: "F", frequency: F4, milliseconds: 400 },
Note { name: "E", frequency: E4, milliseconds: 400 },
Note { name: "D", frequency: D4, milliseconds: 400 },
Note { name: "C", frequency: C4, milliseconds: 400 },
Note { name: "C", frequency: C4, milliseconds: 400 },
Note { name: "D", frequency: D4, milliseconds: 400 },
Note { name: "E", frequency: E4, milliseconds: 400 },
Note { name: "E", frequency: E4, milliseconds: 600 },
Note { name: "D", frequency: D4, milliseconds: 200 },
Note { name: "D", frequency: D4, milliseconds: 800 }
];
PrintLine("Ode to Joy, through the console speaker:");
for note in tune {
Print("{} ", note.name);
if !Beep(note.frequency, note.milliseconds) {
PrintLine();
PrintLine("Windows could not play a tone, so the tune stops here.");
return 1;
}
}
PrintLine();
return 0;
}
Besides Io, its Rux.toml lists Core under [Dependencies].
Run it
cd Examples/Projects/Melody
rux run
Ode to Joy, through the console speaker:
E E F G G F E D C C D E E D D
Run it on Windows with the sound turned up. Each note name is printed as it plays. On a machine with no sound device Beep may fail, and then the program says so and exits with status 1. On Linux, macOS or FreeBSD the build stops with the #Error message shown above.
Common mistakes
Beep(C4, 100); on its own compiles without a word: a bool32 result is not a fallible, so nothing makes you check it. The safety a Rux fallible gives you ends at the extern boundary, and the checking becomes your job.when with no answer for other systems.Without the
else => #Error(…) branch, a build for Linux stops with error: no arm of this 'when' matches .Linux. That is correct but unhelpful. Whenever code is platform-specific, say why in an #Error of your own.With
const E4 = 330; the constant is an int, and every note that uses it fails: error: field 'frequency' in initializer for 'Note' has type 'int', but its declaration requires 'uint32'. Give the constants the type the foreign function expects, once, where they are declared.Try it yourself
- Add rests: a
Notewith frequency 0 that pauses instead of beeping.Beeponly plays frequencies from 37 to 32767 hertz, so the loop has to tell rests apart — Duration andSleepForprovide the pause. - Add a tempo: a constant that scales every duration, so the whole tune can be played faster or slower by changing one number.
- Play the next phrase of Ode to Joy (E E F G G F E D C C D E D C C). Which constants and array length have to change?
- Make the program portable in a different way: on systems other than Windows, instead of stopping the build, print the note names with a pause between them so the rhythm still shows. Use
whento choose the body of aPlayNotefunction.
Learn more
- Extern, Target and Compile error
- Beep in the API reference
- External functions and Conditional compilation in the Rux Reference
- Back to the Projects overview for the full list