Projects · Lesson 25.16

Melody

Source
Play the opening of Ode to Joy through the console speaker by calling the Windows Beep function directly.
You'll need: Parts 1–24 — this project is the checkpoint for Platform, and leans on Extern, Target and Compile error.

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

PieceIts jobLessons it uses
when #target.osDeclares Beep on Windows, stops the build elsewhereWhen, Target, Compile error
#Link and externBinds Beep to Kernel32Extern
C4 … G4Note frequencies as named constantsConst
Note and the tune arrayThe music, as dataStruct, Array
MainPlays each note, stops at the first failureFor, 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?" --> main

Windows 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.

Src/Main.rux
// 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

Calling a foreign function as if it could not fail.
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.
A 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.
Untyped note constants.
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

  1. Add rests: a Note with frequency 0 that pauses instead of beeping. Beep only plays frequencies from 37 to 32767 hertz, so the loop has to tell rests apart — Duration and SleepFor provide the pause.
  2. Add a tempo: a constant that scales every duration, so the whole tune can be played faster or slower by changing one number.
  3. 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?
  4. 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 when to choose the body of a PlayNote function.

Learn more