type Msg { Tick }
type Car {
seat: Int
x: Float
z: Float
speed: Float
knock: Bool
}
// A func like every other shape-maker, and worth noticing that a sound is
// exactly as much of a value as a box is: sixty of these are built a second
// and thrown away, while the voice on the other end of the name goes on
// running the whole time.
func noises(cars: Car[]): ui.Shape[dyn] {
mut ui.Shape[dyn] out = []
for each car in cars {
// Sustained: it lasts as long as the scene goes on naming it, and
// bends with the speed rather than being restarted at a new one.
append(out, ui.sound([
ui.at(car.x, 0.6, car.z),
ui.voice(ui.Voice.Engine()),
ui.pitch(0.7 + car.speed / 90.0),
ui.volume(0.6),
], "engine-{car.seat}"))
// Struck: it fires when the name arrives and is deaf to everything
// after it. Letting the name go and bringing it back is a second one.
if car.knock {
append(out, ui.sound([
ui.at(car.x, 0.6, car.z),
ui.voice(ui.Voice.Impact()),
ui.pitch(1.0),
ui.volume(0.8),
], "hit-{car.seat}"))
}
}
return out
}
func view(cars: Car[dyn]): ui.View {
solids := [ui.ground([ui.paint(ui.Tone.HEX("#3a3f45"))], 400.0, 400.0)]
return ui.scene([
ui.grow(1),
ui.eye(0.0, 1.4, 6.0),
ui.aim(0.0, 0.0),
ui.background(ui.Tone.HEX("#8ec5ff")),
], solids + noises(cars))
}
proc update(cars: mut Car[dyn], msg: Msg): Msg {
return msg
}
proc main(): void {
Car[dyn] grid = [Car(1, -2.0, 0.0, 40.0, false), Car(2, 2.0, -6.0, 62.0, true)]
spawn(ui.window("Sound", view, update), grid)
}sound(attrs, name) is a scene's tenth shape, and the one heard from where it stands instead of drawn there. It takes at like every other shape, and four attributes of its own: voice(Voice) is which noise it is, pitch(Float) how high and volume(Float) how loud — each a multiple of that voice at rest rather than hertz or decibels — and track(Str), a recording under the program's own assets/.
It is a shape because a noise in a world has a place in it: positioned by the same at, written into the same frame, and heard from the scene's own eye and aim, so what is heard on the left is what is drawn on the left, and it fades as the listener walks away.
The payload is a name, and the name is the sound's identity between frames. A box is matched to whatever was in its slot last frame; an oscillator rebuilt that way would click sixty times a second. So a name that was in the last frame carries on, one that has just arrived starts, and one that has gone fades out. What it then does belongs to the voice:
Engine, Tyres, Brakes, Wind, Crowd and Music are sustained: they last as long as the scene goes on naming them.Impact, Chime and Talk are struck: they fire once, when the name arrives. Naming "hit-3" for as long as a car knows it was hit is one impact; letting the name go and bringing it back is a second.Music and Talk have nowhere to stand, so neither is panned.Every voice is made of oscillators and filtered noise unless track names a file. A sustained voice loops its recording, pitch being its playback speed; a struck one plays it once, and is not heard if the recording is still decoding when the sound arrives. A missing or undecodable file falls back to the made voice. A browser plays no sound before the first click or key, and the runtime tries again on every one.