[#_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.