singe/docs/it/lessons/10-sound.adoc

610 lines
22 KiB
Text

[#_lesson_10_sound]
== Lezione 10: Suono
image::learn/10-sound.png[La lezione finita, 480]
Un gioco senza suono sembra rotto, in un modo su cui è difficile mettere il
dito. I colpi partono e non succede niente. Le rocce si spezzano e non succede
niente. Metti un rumore di mezzo secondo su ognuna di queste cose e lo stesso
gioco all'improvviso sembra fatto di qualcosa.
Questa lezione dà al tuo gioco uno sparo, un'esplosione e una musica sotto.
Strada facendo ti presenta la tua quinta callback: quella che il motore usa per
dirti che un suono ha finito di suonare.
[#_one_noise]
=== Un solo rumore
Il kit di grafica che hai copiato nella nona lezione contiene due suoni,
`shoot.wav` e `boom.wav`, e dei due l'esplosione è quello che si sente meglio.
Crea un nuovo file accanto alla tua cartella `art` e scrivi questo.
[source,lua]
----
dofile("Singe/Framework.singe")
local boom = soundLoad(DIR .. "art/boom.wav")
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
soundPlay(boom)
end
end
function onOverlayUpdate()
overlayClear()
overlayPrint(2, 2, "Press space.")
return OVERLAY_UPDATED
end
function onShutdown()
soundUnload(boom)
end
----
Eseguilo e premi la barra spaziatrice tre o quattro volte, in fretta. I botti
si ammucchiano uno sull'altro invece di interrompersi a vicenda.
Tenere premuta la barra spaziatrice ti dà esattamente un botto, non una
raffica. È lo stesso comportamento che hai incontrato nella terza lezione: un
tasto tenuto premuto è una sola pressione, segnalata una volta, e il motore
butta via le ripetizioni che manda la tastiera. È per questo che per sterzare
servono `onInputReleased` e una variabile, invece di contare le pressioni.
[#_what_just_happened_6]
=== Cos'è appena successo
La forma è la stessa della nona lezione: carica una volta in cima, usa
l'handle, restituiscilo alla fine.
[source,lua]
----
local boom = soundLoad(DIR .. "art/boom.wav")
----
`soundLoad` legge un file audio e lo tiene in memoria, e ti restituisce un
handle -- la stessa idea dell'handle di uno sprite, un numero che per il
motore vuol dire «quella clip». `DIR` c'è per lo stesso motivo per cui c'era
nella nona lezione: il suono sta nella cartella del tuo gioco, e il gioco deve
riuscire a trovarlo da qualunque posto sia stato avviato Singe.
Tutto quello che la nona lezione diceva sul caricamento vale qui parola per
parola. Carica in cima al file, una volta. Un `soundLoad` dentro
`onOverlayUpdate` carica la clip sessanta volte al secondo e non ne restituisce
mai nessuna, e qualche minuto dopo il gioco ne muore.
[source,lua]
----
soundPlay(boom)
----
Riproducila. Riprodurre un effetto sonoro è tutto qui, e al resto pensa il
motore: la clip parte, finisce, e non intralcia mai il tuo programma.
Ma anche `soundPlay` restituisce qualcosa, e *non* è lo stesso numero che gli
hai dato. Cambia la riga e guarda:
[source,lua]
----
local channel = soundPlay(boom)
debugPrint("clip " .. boom .. " on channel " .. channel)
----
Premi in fretta la barra spaziatrice una mezza dozzina di volte e osserva i due
numeri. Il numero della clip non cambia mai. Il numero del canale sale -- 0,
poi 1, poi 2 -- e una volta finiti i botti precedenti torna giù e ricomincia a
riusare i numeri bassi.
Questa è l'unica cosa del suono che frega tutti, quindi vale la pena dirla
lentamente. *In gioco ci sono due tipi diversi di numero.*
L'*handle della clip* che viene da `soundLoad` è la registrazione. Ce n'è uno
solo. Non cambia, non sta né suonando né non suonando: è il suono che se ne sta
in memoria in attesa di essere usato.
Il *canale* che viene da `soundPlay` è un'esecuzione di quella registrazione.
Singe ne ha sedici, numerati da `0` a `15`, e ogni suono che si sente in questo
momento ne usa uno. Ecco perché i botti si sovrappongono: la seconda pressione
non ha interrotto la prima, ha avviato una seconda esecuzione sul canale libero
successivo.
Tutto ciò che agisce su un *suono che senti* prende un canale: `soundStop`,
`soundIsPlaying`, `soundPause`, `soundResume`. Tutto ciò che agisce sulla
*registrazione* prende l'handle: `soundPlay` e `soundUnload`. Passa l'uno dove
va l'altro e non sempre otterrai un errore, perché `3` è un numero di canale
valido e anche un handle di clip perfettamente buono -- otterrai silenzio, o il
suono sbagliato che si ferma, che è molto più difficile da trovare.
Sedici sono tanti, ma non sono infiniti. Quando tutti i canali sono occupati
non suona niente, e `soundPlay` restituisce `-1` invece di un numero di canale.
Un gioco che fa partire moltissimi suoni insieme dovrebbe aspettarselo, e tra
un minuto ne scriverai uno.
[#_volume]
=== Volume
Ci sono tre volumi, e si moltiplicano tra loro.
Il volume *generale* degli effetti copre tutto. `soundGetVolume` lo legge e
`soundSetVolume` lo imposta, su una scala da `0` a `63`.
[source,lua]
----
soundSetVolume(40)
----
Resisti. Quel numero è del giocatore, non tuo: parte da quello che ha chiesto
quando ha avviato il gioco, e un gioco che lo sovrascrive è un gioco troppo
forte nella stanza silenziosa di qualcuno. Cambia il volume generale solo
quando te lo chiede il giocatore, da una schermata delle opzioni.
`soundGetVolume` c'è perché una schermata delle opzioni possa mostrare quanto
vale adesso invece di tirare a indovinare.
Quello che dovresti usare è il volume del *canale*, che sta sotto quello
generale. Dai a `soundPlay` un terzo argomento e questa singola esecuzione
suona più piano:
[source,lua]
----
soundPlay(shootClip, 0, 35)
----
Lo `0` in mezzo è quante volte ripetere, e devi darlo perché il volume viene
dopo. `0` vuol dire suonala una volta. `-1` vuol dire ripetila finché qualcosa
non la ferma, ed è così che faresti il rombo di un motore o il vento. La voce
del Riferimento per `soundPlay` elenca tutti e tre gli argomenti.
Il terzo volume è la distanza, per i suoni collocati in un mondo 3D. Quello è
per la quarta parte, quando il gioco passa alle tre dimensioni.
La musica ha un volume tutto suo, ed è su una scala diversa. Ne riparliamo tra
un momento.
[#_stopping_and_not_stopping]
=== Fermare, e non fermare
`soundStop` prende un canale e lo interrompe lì dov'è:
[source,lua]
----
soundStop(engineChannel)
engineChannel = -1
----
Nota la seconda riga. Appena un canale si ferma, il motore è libero di dare
quello stesso numero al `soundPlay` successivo, quindi un numero di canale che
tieni ancora potrebbe ora appartenere a un suono completamente diverso. Buttalo
via nel momento stesso in cui lo fermi. `-1` è la cosa utile da metterci,
perché è lo stesso valore che `soundPlay` ti dà quando non è riuscito a suonare
affatto, quindi un solo controllo copre tutti e due i casi:
`if channel >= 0 then`.
`soundStop` è per un suono che adesso è sbagliato -- il rumore del motore
quando il motore muore, l'allarme quando qualcuno ha risposto all'allarme. È lo
strumento sbagliato per un effetto breve. Un'esplosione di mezzo secondo
troncata dopo un decimo di secondo sembra un errore, perché lo è.
Quindi la maggior parte delle volte non fare proprio niente. Lascialo finire.
Un effetto sonoro che avvii e a cui non pensi più finisce da solo e libera il
suo canale da solo, che è esattamente quello che vuoi.
Questo vale anche alla fine. `soundUnload` non interrompe una clip che sta
ancora suonando: il motore tiene vivo l'audio finché non si è fermato l'ultimo
canale che lo usa. Puoi scaricare in `onShutdown` senza preoccuparti di aver
zittito qualcosa.
`soundIsPlaying(channel)` risponde se un canale sta facendo rumore in questo
momento. Il suo uso principale non è la curiosità ma la moderazione: un allarme
che deve suonare una volta, e non ripartire ogni volta che il gioco si accorge
che il pericolo è ancora lì.
[source,lua]
----
function raiseAlarm()
if alarmChannel < 0 or not soundIsPlaying(alarmChannel) then
alarmChannel = soundPlay(alarmClip)
end
end
----
Leggi la condizione ad alta voce: suona l'allarme se non c'è un canale
dell'allarme, o se quello che hai si è fermato. Altrimenti lascialo stare.
[#_the_sound_that_stutters]
=== Il suono che balbetta
Ecco l'errore. Lo scrivono tutti, di solito entro una settimana da quando
scoprono il suono.
Supponi di volere che la nave continui a sparare finché il pulsante resta
premuto, invece di sparare una volta per pressione. Lo faresti come sterzi:
una variabile `firing` messa a `true` in `onInputPressed` e di nuovo a `false`
in `onInputReleased`, e il colpo creato in `onOverlayUpdate` finché è vera. È
una cosa perfettamente sensata da volere, e il rumore sembra stare proprio lì,
insieme al colpo:
[source,lua]
----
function onOverlayUpdate()
if firing then
shots[#shots + 1] = { x = shipX, y = shipY }
soundPlay(shootClip)
end
...
----
Quello che esce non è uno sparo. È un ronzio gracchiante, o un tono duro e
piatto, o un suono come di una piccola macchina che si rompe.
`onOverlayUpdate` viene eseguita circa sessanta volte al secondo. Tieni premuto
il pulsante per un secondo e avrai chiesto sessanta esecuzioni di una clip che
dura circa un quinto di secondo, ognuna che parte sopra la precedente. In ogni
momento ne stanno suonando circa undici, ognuna un fotogramma dietro quella
prima, e quello che senti sono sessanta copie dello stesso attacco ammucchiate l'una sull'altra.
La soluzione non è un suono più furbo. È riprodurre il suono dove avviene
l'*evento* invece che dove lo *stato* è vero:
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
soundPlay(shootClip, 0, 35)
end
end
----
`onInputPressed` avviene una volta, quando il pulsante va giù. Una pressione,
un colpo, un rumore.
Se vuoi davvero il fuoco continuo, la risposta è la stessa in un'altra forma:
fai diventare un evento lo sparo stesso. Conta i fotogrammi, esattamente come
li contava il ciclo della camminata nella nona lezione, e crea un colpo solo
ogni otto fotogrammi. Il rumore va con il colpo, quindi suona sette o otto
volte al secondo invece di sessanta, e quella è una mitragliatrice e non un
gracchiare.
Questa è la regola, e vale la pena portarsela via da questa lezione: *i suoni
vanno con gli eventi, non con le condizioni*. Una pressione, un colpo a segno,
una morte, un oggetto raccolto -- ognuna di queste cose avviene una volta e
merita un suono. «Il pulsante è premuto» e «la nave si sta muovendo» non sono
eventi; sono veri per centinaia di fotogrammi di fila, e qualunque cosa
riproduci a partire da loro viene riprodotta centinaia di volte.
Quando un suono appartiene davvero a uno stato che dura -- un motore, il vento,
una sirena -- avvialo una volta con `-1` come numero di ripetizioni, tieni il
canale e fermalo quando lo stato finisce.
[#_the_fifth_callback]
=== La quinta callback
A volte ti serve proprio sapere quando un suono è finito. Scrivi questa
funzione e il motore la chiamerà:
[source,lua]
----
function onSoundCompleted(channel)
end
----
È la tua quinta callback, dopo `onOverlayUpdate` nella prima lezione, le due
callback dei comandi nella terza e `onShutdown` nella nona, e funziona come tutte le altre: tu la scrivi,
non la chiami mai, il motore chiama te. Il `channel` che ti passa è il canale
che si è appena zittito -- il numero che `soundPlay` ti ha dato quando l'hai
avviato.
Scatta quando una clip finisce, quando termina l'ultima ripetizione di un loop,
*e* quando fermi tu stesso un canale con `soundStop`. Quest'ultimo caso
sorprende chi usa la callback per concatenare un suono al successivo: fermare
un canale apposta lo segnala comunque come completato, e se non stai attento il
suono successivo della catena parte lo stesso.
Il gioco sta per usarla per qualcosa che vale la pena fare. Quando se ne va
l'ultima vita, suona l'esplosione della nave e il gioco resta immobile --
niente GAME OVER, niente musica che sfuma -- finché quell'esplosione non è
davvero finita. Poi si ferma tutto insieme. Bastano poche righe, ed è la
differenza tra un finale e un taglio.
[#_music_is_not_a_sound_effect]
=== La musica non è un effetto sonoro
Potresti caricare un motivo di tre minuti con `soundLoad` e riprodurlo con
`soundPlay`. Non farlo. La musica ha una famiglia di chiamate tutta sua, per
motivi che contano:
* La musica ha un volume suo, separato da quello degli effetti, così un
giocatore può abbassare la musica e sentire ancora i colpi.
* Non c'è il limite dei sedici canali e non c'è un numero di canale. Un handle
è un brano, e lo fermi, lo metti in pausa e lo riprendi tramite quell'handle.
* `musicStop` può far sfumare un brano in un secondo o due invece di troncarlo.
Le chiamate rispecchiano quelle che conosci già:
[source,lua]
----
local theme = musicLoad("Singe/menuIntro.flac")
musicSetVolume(50)
musicPlay(theme, -1)
----
`musicLoad` dà un handle. `musicPlay` lo avvia, con lo stesso numero di
ripetizioni di `soundPlay`: `-1` per sempre. `musicStop(theme)` lo termina, e
`musicStop(theme, 1500)` impiega un secondo e mezzo a farlo sfumare.
`musicUnload` lo restituisce.
Ora la trappola. *Il volume della musica va da 0 a 128, non da 0 a 63.* Gli
effetti sonori usano una scala e la musica un'altra, lunga il doppio, per
motivi storici che non ti consolano affatto quando la tua musica esce a metà
del volume che volevi. Quando lavori con la musica, raddoppia il numero che
avevi in mente per un effetto.
Il brano caricato lì è la musica del menu del motore stesso, presa dalla
cartella `Singe` che il motore ha estratto nella tua cartella di lavoro al primo
avvio. Dura dieci secondi ed è stato scritto per sfumare alla fine, quindi
ripeterlo in loop suona esattamente come un loop di dieci secondi che continua
a sfumare. Va bene per imparare le chiamate e non va bene per niente in un
gioco finito, che porta con sé un brano più lungo tutto suo. Va bene uno
qualunque dei formati soliti: la sezione Musica del Riferimento li elenca, e
Ogg Vorbis o MP3 sono le scelte sensate, perché qualche minuto di musica in WAV
è un file enorme.
[#_the_game_gets_a_soundtrack]
=== Il gioco ha una colonna sonora
Apri il gioco della nona lezione. Tutto quello che c'è qui gli si aggiunge; non
si toglie niente.
Quattro cose da caricare, in cima, insieme agli sprite:
[source,lua]
----
local shootClip = soundLoad(DIR .. "art/shoot.wav")
local boomClip = soundLoad(DIR .. "art/boom.wav")
local clickClip = soundLoad("Singe/click.wav")
local theme = musicLoad("Singe/menuIntro.flac")
----
I primi due hanno `DIR` e gli ultimi due no, ed è voluto. `DIR` vuol dire la
cartella del *tuo* gioco. Un nome che comincia con `Singe/` vuol dire invece la
cartella del motore, dovunque si trovi -- ed è per questo che
`dofile("Singe/Framework.singe")` funziona dalla settima lezione anche lui
senza `DIR`. I tuoi file hanno bisogno di `DIR`; quelli del motore mai.
Due nuove variabili, accanto alle altre:
[source,lua]
----
local dying = false
local deathChannel = -1
----
`dying` è il momento tra la perdita dell'ultima vita e la fine della partita:
l'esplosione si sente ancora, e sullo schermo non si muove niente.
Il colpo prende il suo rumore in `onInputPressed`, proprio dove viene creato il
colpo stesso, e la ripartenza prende un clic, così premere `1` dà l'impressione
di aver fatto qualcosa:
[source,lua]
----
function onInputPressed(what)
if over then
if what == SWITCH_START1 then
soundPlay(clickClip)
startGame()
end
return
end
if what == SWITCH_LEFT then
goLeft = true
elseif what == SWITCH_RIGHT then
goRight = true
elseif what == SWITCH_BUTTON1 then
shots[#shots + 1] = { x = shipX + shipWidth / 2 - shotWidth / 2, y = shipY }
soundPlay(shootClip, 0, 35)
end
end
----
Una roccia colpita fa un botto, e così una roccia che colpisce te. In
`updateShots`, accanto al punteggio:
[source,lua]
----
newRock(rock)
score = score + HIT_SCORE
gone = true
soundPlay(boomClip, 0, 45)
----
E in `updateRocks`, dove si perde la vita, la parte interessante:
[source,lua]
----
if overlapping(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then
local channel = soundPlay(boomClip)
newRock(rock)
lives = lives - 1
if lives <= 0 then
dying = true
deathChannel = channel
if channel < 0 then
endGame()
end
end
end
----
Quando se ne va l'ultima vita, l'esplosione suona e il suo canale viene
ricordato. Il gioco ora è `dying`, cosa che ferma ogni movimento ma non dice
ancora GAME OVER.
Quel `if channel < 0` è il limite dei sedici canali, gestito. Se proprio in
quell'istante tutti i canali fossero occupati, non ci sarebbero né
un'esplosione né un canale da aspettare, e senza quelle due righe il gioco
resterebbe in `dying` per sempre, ad aspettare un suono mai partito. Aspettare
qualcosa che non può arrivare è uno dei modi più facili per bloccare un
programma, e il controllo costa sempre meno del bug.
Ora la nuova callback, e la piccola funzione che condivide con quel controllo:
[source,lua]
----
function endGame()
dying = false
deathChannel = -1
over = true
musicStop(theme, 1500)
end
function onSoundCompleted(channel)
if channel == deathChannel then
endGame()
end
end
----
Il motore chiama `onSoundCompleted` per *ogni* canale che si ferma, quindi la
prima cosa che fa è controllare se questo è il canale che le interessa. Tutto
il resto -- ogni colpo, ogni roccia -- passa oltre e viene ignorato.
`onOverlayUpdate` deve restare immobile mentre l'esplosione va avanti, e basta
una parola:
[source,lua]
----
if not over and not dying then
updateShip()
updateShots()
updateRocks()
end
----
`startGame` azzera le due nuove variabili e fa ripartire la musica da capo:
[source,lua]
----
dying = false
deathChannel = -1
musicSetVolume(50)
musicPlay(theme, -1)
----
E `onShutdown` restituisce quello che era stato caricato:
[source,lua]
----
function onShutdown()
spriteUnload(shipSprite)
spriteUnload(rockSprite)
spriteUnload(shotSprite)
spriteUnload(starSprite)
soundUnload(shootClip)
soundUnload(boomClip)
soundUnload(clickClip)
musicUnload(theme)
end
----
Eseguilo. Spara alle rocce, fatti colpire tre volte e ascolta la fine: l'ultimo
botto suona fino in fondo, e solo quando è finito la musica comincia a sfumare
e compaiono le parole.
Lo script finito si trova nella cartella `learn` come `10-sound.singe`, se vuoi
fare un confronto.
[#_try_it_10]
=== Prova tu
. *Rendi il colpo ancora più piano.* Cambia il `35` nel `soundPlay` del colpo
in `10`, poi in `63`. Trova il numero in cui sta sotto le esplosioni invece
che sopra.
. *Abbassa la musica senza toccare gli effetti.* Cambia `musicSetVolume(50)` in
`musicSetVolume(15)`. Verifica che i botti siano forti esattamente quanto
prima, che è tutto il motivo per cui la musica è un sistema a parte.
. *Taglia il finale.* In `endGame`, cambia `musicStop(theme, 1500)` in
`musicStop(theme)`. Ascolta entrambi i finali due volte. La differenza è un
numero in un argomento, e non è una piccola differenza.
. *Dai un motore alla nave.* Avvia `soundPlay(clickClip, -1, 12)` in
`startGame`, tieni il canale che ti dà in una variabile tutta sua e fermalo
in `endGame`. Il clic dura un cinquantesimo di secondo, quindi in loop
diventa un ronzio, che è quanto di più vicino a un motore offra questo kit.
Poi togli di nuovo il `soundStop`, gioca tre partite di fila e ascolta che
cosa hai costruito.
. *Conta i canali.* Stampa il numero del canale ogni volta che una roccia
esplode. Poi aumenta `ROCK_COUNT` finché non ne esplodono parecchie insieme,
e continua ad aumentarlo finché non vedi passare un `-1`.
[#_break_it_on_purpose_9]
=== Rompilo apposta
Ormai hai in testa due scale di volume, e prima o poi dalle dita esce quella
sbagliata. Aggiungi questa riga da sola, subito sotto le quattro righe
`soundLoad` e `musicLoad`:
[source,lua]
----
soundSetVolume(100)
----
Il gioco si rifiuta di partire:
----
20:soundSetVolume: Invalid sound volume value: 100
----
Il numero di riga che ottieni è la riga in cui l'hai scritta davvero, quindi il
tuo potrebbe non essere 20. Poi la funzione che ha protestato, e il valore
contro cui ha protestato. Il messaggio non ti dice qual è l'intervallo -- a
quello serve la voce di `soundSetVolume` nel Riferimento -- ma ti dice il
numero che non ha voluto accettare, e una volta che sai che gli effetti si
fermano a 63 la correzione è un numero solo.
`100` è un numero sospetto da trovare in un volume, e vale la pena sapere
perché l'hai scritto. Quasi di sicuro hai pensato al volume come a una
percentuale, perché ogni cursore del volume che tu abbia mai usato arriva a
100. Quello di Singe no. Gli effetti vanno da `0` a `63` e la musica da `0` a
`128`, e nessuno dei due è una percentuale di qualcosa.
Questa è la forma generale di un errore del motore, a differenza degli errori
di Lua della prima lezione: un numero di riga, il nome della funzione del
motore che hai chiamato e una lamentela sui valori che le hai passato. Quando ne
vedi uno, la voce di quella funzione nel Riferimento è la prossima cosa da
leggere.
[#_what_you_learned_10]
=== Cosa hai imparato
* `soundLoad` legge una clip e restituisce un handle. Carica una volta, in cima
al file.
* `soundPlay` avvia una clip e restituisce un *canale*, che è un tipo di numero
diverso dall'handle.
* Singe mescola sedici canali. Quando sono tutti occupati, `soundPlay`
restituisce `-1` e non suona niente.
* I numeri dei canali vengono riusati. Dimenticane uno appena l'hai fermato.
* Gli effetti brevi vanno lasciati finire da soli; `soundStop` è per i suoni
che sono diventati sbagliati.
* `soundUnload` non interrompe una clip che sta ancora suonando.
* Riproduci un suono dove avviene l'evento, non dove una condizione è vera,
altrimenti suona sessanta volte al secondo.
* `onSoundCompleted(channel)` è una callback che il motore chiama ogni volta
che un canale si zittisce, per qualunque motivo.
* La musica è un sistema separato, con i suoi handle, il suo volume e nessun
limite di canali.
* Il volume degli effetti va da `0` a `63`. Il volume della musica va da `0` a
`128`. Non sono percentuali.
[#_next_time_10]
=== La prossima volta
Le tue rocce vengono colpite quando i loro rettangoli si sovrappongono, ed è
per questo che un colpo che passa per un angolo di cielo conta lo stesso.
Nell'undicesima lezione ottieni i controlli di collisione del motore, impari
che cos'è una hitbox e scopri perché il rettangolo attorno a una roccia
rotonda ha la forma sbagliata.