Slide 69 de 74

Um som é uma forma

Sustentada e percutida, lado a lado
type Msg { Tick }

type Car {
	seat:  Int
	x:     Float
	z:     Float
	speed: Float
	knock: Bool
}

// Um func como todo outro construtor de formas deste arquivo, e vale notar
// que um som é exatamente tão valor quanto uma box: sessenta destes são
// construídos por segundo e jogados fora, enquanto a voz do outro lado do
// nome segue rodando o tempo todo.
func noises(cars: Car[]): ui.Shape[dyn] {
	mut ui.Shape[dyn] out = []
	for each car in cars {
		// Sustentada: dura enquanto a scene continuar a nomeando, e dobra
		// com a velocidade em vez de ser reiniciada em uma nova.
		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}"))

		// Percutida: dispara quando o nome chega e fica surda a tudo depois
		// disso. Soltar o nome e trazê-lo de volta é um segundo impacto.
		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("Som", view, update), grid)
}

sound(attrs, name) é a décima forma de uma cena, e a que é ouvida de onde está em vez de desenhada ali. Ela recebe at como toda outra forma, e quatro atributos próprios: voice(Voice) é que ruído ela é, pitch(Float) o quão agudo e volume(Float) o quão alto — cada um um múltiplo daquela voz em repouso, e não hertz ou decibéis — e track(Str), uma gravação nos assets/ do próprio programa.

Ela é uma forma porque um ruído num mundo tem um lugar nele: posicionado pelo mesmo at, escrito no mesmo quadro, e ouvido do próprio eye e aim da cena, então o que se ouve à esquerda é o que se desenha à esquerda, e ele diminui conforme quem ouve se afasta.

A carga é um nome, e o nome é a identidade do som entre quadros. Uma caixa é casada com o que estava no seu lugar no quadro anterior; um oscilador reconstruído assim estalaria sessenta vezes por segundo. Então um nome que estava no último quadro continua, um que acabou de chegar começa, e um que sumiu vai se apagando. O que ele faz então é da voz:

  • Engine, Tyres, Brakes, Wind, Crowd e Music são sustentadas: duram enquanto a cena continuar nomeando-as.
  • Impact, Chime e Talk são percutidas: disparam uma vez, quando o nome chega. Nomear "hit-3" enquanto um carro souber que foi atingido é um impacto; deixar o nome sumir e trazê-lo de volta é um segundo.
  • Music e Talk não têm onde ficar, então nenhuma das duas é posicionada entre os lados.

Toda voz é feita de osciladores e ruído filtrado, a menos que track nomeie um arquivo. Uma voz sustentada repete sua gravação, sendo pitch a velocidade de reprodução; uma percutida a toca uma vez, e não é ouvida se a gravação ainda estiver sendo decodificada quando o som chegar. Um arquivo ausente ou que não se consegue decodificar volta para a voz feita. Um navegador não toca som antes do primeiro clique ou tecla, e o runtime tenta de novo a cada um.