singe/docs/it/lessons/27-midi.adoc

515 lines
22 KiB
Text

[#_lesson_27_music_and_midi]
== Lezione 27: Musica e MIDI
image::learn/27-midi.png[La lezione finita, 480]
Questa lezione è fatta di due lezioni che per caso abitano insieme.
La prima metà è la musica: come un gioco riproduce come si deve un brano
musicale, perché non è la stessa cosa che riprodurre un effetto sonoro, e che
cosa fare quando la musica deve restare a tempo con il video.
La seconda metà è il MIDI, che quasi nessuno ha mai incontrato e che dà a un
gioco qualcosa che nient'altro gli dà. Alla fine, una tastiera collegata al
tuo computer suonerà il tuo gioco.
[#_a_sound_effect_and_a_piece_of_music_are_different_things]
=== Un effetto sonoro e un brano musicale sono cose diverse
Sai già come riprodurre un suono. Nella decima lezione hai caricato un `.wav`
con `soundLoad` e l'hai fatto partire con `soundPlay`, e se punti quelle due
chiamate su una canzone di quattro minuti la suoneranno.
Non farlo. Singe tiene separati gli effetti sonori e la musica, e la
separazione conviene per quattro motivi.
* *Hanno volumi propri.* `soundSetVolume` non tocca mai la musica e
`musicSetVolume` non tocca mai gli spari. Ogni gioco mai uscito ne ha avuto
bisogno, perché i giocatori abbassano la musica e alzano gli effetti.
* *La musica non ha un limite di canali.* Gli effetti sonori si dividono sedici
canali, e quando sono occupati tutti e sedici il diciassettesimo non suona.
Un brano musicale non sta su un canale, quindi niente può scalzarlo.
* *Un handle è un brano*, e si mette in pausa, riprende, si ferma e sfuma per
conto suo senza disturbare nient'altro.
* *La musica va in pausa con il gioco.* Premi il tasto di pausa e resta dov'è.
Torna e riprende. Non devi scrivere niente perché succeda.
Una cosa che *non* è diversa vale la pena saperla, perché si tende a pensare il
contrario: Singe decodifica un file musicale in memoria fin dall'inizio,
esattamente come fa con un effetto sonoro. Un brano lungo costa memoria. Non
viene letto dal disco un pezzo alla volta. Cinque minuti di musica sono
qualche decina di megabyte di memoria finché restano caricati, quindi carica il
brano per il livello in cui sei e scaricalo con `musicUnload` quando lo lasci.
[#_playing_a_track]
=== Riprodurre un brano
Ogni lettore di questo libro ha già un brano musicale, perché il motore ne ha
estratto uno nella tua cartella di lavoro la prima volta che hai eseguito
qualcosa. Comincia un file con queste tre righe:
[source,lua]
----
local theme = musicLoad("Singe/menuIntro.flac")
musicSetVolume(64)
musicPlay(theme, -1)
function onOverlayUpdate()
overlayClear()
overlayPrint(2, 2, "Music: " .. (musicIsPlaying(theme) and "playing" or "stopped"))
return OVERLAY_UPDATED
end
----
Eseguilo con `Singe -R music`. Dieci secondi di musica, poi ricomincia, e
continua a ricominciare.
È questo che fa il `-1`. `musicPlay` prende un numero di *ripetizioni*:
omettilo o passa `0` e il brano suona una volta, passa `3` e suona quattro
volte in tutto, passa `-1` e si ripete finché qualcosa non lo ferma. Un menu,
una schermata del titolo o il sottofondo di un livello sono quasi sempre `-1`.
In quel piccolo script ci sono due dettagli che meritano due parole.
`musicLoad` prende un percorso normale e non un percorso con `DIR ..`, perché
`Singe/menuIntro.flac` è uno dei file del motore e sta nella cartella che il
motore ha estratto nella tua cartella di lavoro. `musicIsPlaying` risponde
`true` o `false`, e la coppia `and ... or ...` in mezzo a quel `overlayPrint`
è la scorciatoia di Lua vista nella terza lezione per scegliere uno tra due valori.
[#_the_volume_scale_is_not_the_one_you_know]
=== La scala del volume non è quella che conosci
`musicSetVolume(64)` lo imposta più o meno a metà. Gli effetti sonori vanno da
`0` a `63`; la musica va da `0` a `128`, cioè il doppio.
Non è un errore e non è uno scherzo. Singe può eseguire giochi scritti per
Hypseus, un motore il cui volume della musica è sempre stato su una scala da
`0` a `128`, e cambiare i numeri renderebbe in silenzio la musica di quei
giochi due volte più forte di quanto volevano i loro autori. Così Singe tiene
tutte e due le scale, ognuna legata alla famiglia di chiamate a cui appartiene.
Effetti: da `0` a `63`. Musica: da `0` a `128`. Dai a `musicSetVolume` un
numero fuori dal suo intervallo e lo script si ferma, come vedrai tu stesso
alla fine di questa lezione.
[#_what_singe_will_play]
=== Che cosa riproduce Singe
Quasi tutto. WAV, Ogg Vorbis, Opus, MP3, FLAC, WavPack, AAC e un lungo elenco
oltre a questi, tutti con lo stesso `musicLoad`.
Due famiglie sono più interessanti di quelle normali, ed esistono tutte e due
perché sono minuscole.
I *moduli tracker* -- `.mod`, `.s3m`, `.xm`, `.it` e una cinquantina di
parenti -- contengono una manciata di brevi strumenti registrati e un elenco
delle note su cui suonarli. Un motivo di quattro minuti può stare sotto i
cento kilobyte.
Le *chiptune* -- `.nsf`, `.spc`, `.vgm`, `.gbs` e simili -- vanno oltre:
contengono le *scritture nei registri* per il vero chip sonoro di un Nintendo,
di un Mega Drive o di uno Spectrum, e Singe emula il chip per suonarle. Se vuoi
che un gioco suoni come il 1989 e non come una registrazione del 1989, si fa
così.
Le une e gli altri si caricano con `musicLoad` come qualsiasi altra cosa. La
sezione *Formati video, audio e dei contenitori* del Riferimento ha l'elenco
completo, compreso quale contenitore va con quale codec.
[#_stopping_pausing_and_fading]
=== Fermare, mettere in pausa e sfumare
[source,lua]
----
musicPause(theme)
musicResume(theme)
musicStop(theme)
musicStop(theme, 2000)
----
`musicPause` ferma il brano dov'è e `musicResume` riprende da quel punto.
`musicStop` lo termina, e la volta dopo che lo riproduci il brano riparte
dall'inizio. Con un secondo numero, `musicStop` sfuma il brano in uscita in
quel numero di millisecondi invece di troncarlo di netto -- qui due secondi.
Quella sfumatura è in assoluto la cosa più facile che puoi fare per dare a un
gioco un'aria finita. Una musica che si interrompe a metà battuta quando il
giocatore attraversa una porta sembra un bug, anche a chi non saprebbe dire
perché.
Chiama una qualsiasi delle quattro senza nessun handle e agisce su tutta la
musica insieme, che è quello che vuole una schermata di pausa.
Ora il limite, detto onestamente, perché lo cercherai e non c'è: *Singe non sa
fare una dissolvenza incrociata tra due brani.* `musicSetVolume` è un unico
guadagno su tutta la musica, non un volume per brano, quindi non c'è modo di
alzare un brano mentre un altro si abbassa. Quello che puoi fare è sfumare in
uscita quello vecchio e far partire quello nuovo mentre se ne va, il che basta
per un cambio di livello e, a orecchio, non è la stessa cosa per un tema di
battaglia che scivola sotto un tema di esplorazione. Se oggi ti serve una vera
dissolvenza incrociata, il modo per ottenerla è fare dei due pezzi un solo file
e saltare al suo interno, o accettare la sovrapposizione.
[#_music_video_and_staying_in_step]
=== Musica, video e restare a tempo
Se il tuo gioco riproduce video -- l'argomento della terza parte -- c'è una
cosa da sapere sul suono, e non è affatto una chiamata della musica.
Singe mostra un fotogramma video quando si sente il suono che gli appartiene,
non a colpi di timer. L'audio è l'orologio e l'immagine lo segue. È il verso
giusto, perché un orecchio si accorge di un quinto di secondo di scarto in una
voce, e un occhio non si accorge dello stesso scarto in una bocca.
Il motore misura il proprio percorso audio quando parte. Quello che non può
misurare è tutto ciò che viene dopo che il sistema operativo ha consegnato il
suono: un amplificatore, un televisore, un ricevitore HDMI, delle cuffie
Bluetooth. Ognuno aggiunge un ritardo suo, diverso su ogni macchina, e si nota
come un suono che arriva un attimo dopo la cosa che l'ha prodotto.
La soluzione è un numero:
[source,lua]
----
singeSetAudioDelay(45)
----
Quarantacinque millisecondi di «l'audio è in ritardo; trattieni l'immagine per
andargli dietro». Puoi anche passarlo come `--audiodelay=45` sulla riga di
comando o metterlo nella voce del gioco in `games.dat`. `singeGetAudioDelay`
rilegge il valore in vigore, ed è quello che un menu di servizio mostra
accanto al suo cursore.
Non devi tirare a indovinare il numero. Il menu di Singe ha una schermata di
calibrazione: fa un clic una volta al secondo e fa lampeggiare lo schermo di
bianco, tu regoli finché il lampo e il clic non avvengono insieme, e lei salva
il risultato per tutti i giochi di quella macchina. La sezione
*Sincronizzazione dell'audio* del Riferimento la descrive e spiega perché
funziona. Se costruisci un cabinato, eseguila una volta dopo aver collegato
gli altoparlanti, e non pensarci mai più.
[#_the_other_thing_called_midi]
=== L'altra cosa che si chiama MIDI
MIDI è il nome di due cose che non hanno niente a che fare l'una con l'altra,
e tenerle separate è quasi tutto quello che serve per capirlo.
Ecco la prima, in un paragrafo. Una registrazione -- un `.wav`, un `.mp3` -- è
una misura di un suono, presa quarantaquattromila volte al secondo. Il MIDI non
è questo. Un file MIDI contiene *che cosa è stato suonato*: in questo momento,
su questo canale, premi il tasto sessanta con questa forza; poco dopo,
lascialo andare. È più vicino a uno spartito, o al rullo perforato di una
pianola, che a una registrazione. Ecco perché un file MIDI pesa pochi kilobyte
quando la stessa musica come audio pesa qualche megabyte, ed è anche il motivo
per cui un file MIDI non contiene proprio nessun suono. Qualcosa deve suonare
le note.
Un file MIDI è musica, quindi si riproduce come la musica:
[source,lua]
----
local tune = musicLoad("music/overworld.mid")
musicPlay(tune, -1)
----
Ma la cosa che suona le note deve venire da qualche parte, e quella cosa è un
*soundfont*: un file pieno di strumenti registrati -- un pianoforte, una
tromba, un rullante -- con le note e i punti di loop segnati, così il motore
può costruirci qualsiasi nota di qualsiasi strumento.
Singe non ne include uno. Un buon soundfont pesa decine di megabyte, e
sarebbero decine di megabyte portate in giro da ogni copia del motore per un
formato che la maggior parte dei giochi non tocca mai. Quindi ne indichi uno
con `--soundfont`, o ne metti uno in `Singe/soundfont.sf2` così che un gioco
impacchettato si porti il suo, o lasci che Singe trovi quello installato dalla
tua distribuzione Linux. Se non c'è un soundfont da nessuna parte, caricare un
`.mid` fallisce e lo dice.
Ecco perché lo stesso file MIDI suona diverso su due macchine, e perché
suonava così male sui computer degli anni novanta. Il file è un elenco di
note. Quello che senti è il soundfont che si è trovato a suonarle: uno buono è
una stanza piena di strumenti, uno scadente è una stanza piena di giocattoli,
e le note sono identiche in entrambi. Se nel tuo gioco va un file MIDI,
distribuisci il soundfont insieme. Altrimenti non hai scelto come suona la tua
musica; l'hai lasciato a qualunque cosa sia installata sulla macchina del
giocatore.
[#_midi_as_a_wire]
=== Il MIDI come cavo
Ora la seconda cosa, che è quella interessante.
Il MIDI è anche un cavo, e i messaggi che ci viaggiano sopra. Collega una
tastiera musicale al tuo computer e ogni tasto che premi manda un messaggio:
*note on, canale 1, tasto 60, velocity 90*. Gira una manopola su una superficie
di controllo e manda *il controller 7 sul canale 1 adesso vale 40*. Questi
messaggi esistono che ci sia o no qualcosa che suona musica. Sono un altro
dispositivo di input, come un joystick con ottantotto pulsanti che sanno
quanto forte li colpisci.
E funziona anche al contrario. Il tuo gioco può mandare fuori quei messaggi, a
un sintetizzatore, a un modulo sonoro, a un controller per luci da palco o a
qualsiasi altra cosa sul cavo.
Le porte sono numerate da zero, e Singe non apre niente finché non glielo
chiedi -- un gioco che non nomina mai il MIDI non paga mai per il MIDI. Possono
essere aperte una porta di ingresso e una di uscita alla volta. Fai
l'inventario della macchina così:
[source,lua]
----
local inPort = "none"
local outPort = "none"
if midiInputCount() > 0 and midiOpenInput(0) then
inPort = midiInputName(0)
end
if midiOutputCount() > 0 and midiOpenOutput(0) then
outPort = midiOutputName(0)
end
----
Su una macchina senza nessun MIDI i conteggi sono zero, le chiamate di
apertura rispondono `false` e niente fallisce. Questo conta: vuol dire che puoi
scrivere questo codice in un vero gioco e distribuirlo, e i novantanove
giocatori su cento senza una tastiera MIDI non sapranno mai che c'è. I nomi
servono per una schermata delle impostazioni che lasci scegliere al giocatore
quale porta, perché la porta zero di rado è quella che vuole. `midiRescan`
cerca di nuovo qualcosa che è stato collegato mentre il gioco è in esecuzione.
[#_listening]
=== Ascoltare
Ogni messaggio che arriva va a una callback, così come ogni pressione di un
tasto va a `onInputPressed`:
[source,lua]
----
local MIDI_NOTE_ON = 0x90
function onMidiMessage(status, data1, data2, bytes)
if (status & 0xF0) ~= MIDI_NOTE_ON or data2 == 0 then
return
end
heard = noteName(data1)
end
----
La maggior parte dei messaggi MIDI è lunga tre byte, e i tre numeri che ricevi
sono quei byte esattamente come li ha mandati il dispositivo. Un messaggio con
meno byte ti passa uno `0` al posto di ognuno che non ha.
`status` dice di che tipo di messaggio si tratta e da quale canale è arrivato,
mescolati insieme: il tipo sono i quattro bit alti e il canale i quattro
bassi. `0x90` è note on, e una nota sul canale 1 arriva come `0x90` mentre la
stessa nota sul canale 5 arriva come `0x94`. `status & 0xF0` tiene la metà alta
e butta via il canale, che è quello che vuoi a meno che non ti importi da quale
canale veniva. (`&` è l'and bit a bit di Lua, e `0x90` è un numero scritto in
esadecimale -- base sedici -- perché è così che li scrive ogni documento MIDI
del mondo.)
`data1` e `data2` sono gli altri due byte, e che cosa vogliono dire dipende dal
tipo. Per un note on sono il tasto e la velocity: il tasto `60` è il Do
centrale, e la velocity è quanto forte è stato colpito, da `1` a `127`.
Il controllo `data2 == 0` non è pignoleria. Un note on con velocity zero vuol
dire *note off*: è il modo in cui quasi ogni tastiera mai costruita dice «ho
lasciato andare», e se non lo filtri il tuo gioco scatterà due volte per ogni
tasto.
`bytes` è l'intero messaggio come stringa, ed è così che leggi un messaggio più
lungo di tre byte.
[#_playing]
=== Suonare
Mandare è più facile che ricevere.
[source,lua]
----
midiNoteOn(1, 60, 100)
midiNoteOff(1, 60)
----
Do centrale sul canale 1, colpito a cento, e poi rilasciato. Qui i canali vanno
da `1` a `16` come li conta una persona, non da `0` a `15` come fa il cavo: è
Singe che è gentile con te.
Ogni `midiNoteOn` ha bisogno di un `midiNoteOff`. Una nota che non viene mai
rilasciata suona finché qualcosa non la ferma, e il classico primo bug MIDI è
un gioco che va in crash o si chiude con tre note premute e lascia un accordo
sospeso nella stanza. Quindi abbinale, e abbinale con un timer invece che a
mano:
[source,lua]
----
function playTarget()
if not midiIsOutputOpen() then
return
end
local key = target
midiNoteOn(1, key, 100)
timerAfter(HOLD_MS, function()
midiNoteOff(1, key)
end)
end
----
`local key = target` fa un lavoro vero. `timerAfter` esegue la sua funzione
quattro decimi di secondo dopo, e a quel punto `target` potrebbe essere
passato a un'altra nota; copiarlo in una variabile locale vuol dire che il
timer rilascia la nota che è stata davvero premuta. Copia tutto ciò che servirà
più tardi a un timer.
Le altre chiamate di invio fanno il resto di quello che è il MIDI:
`midiProgramChange` sceglie quale strumento suona un canale,
`midiControlChange` muove un controller (il volume è 7, il pan è 10, il pedale
di sustain è 64), `midiPitchBend` piega l'intonazione di un canale, e
`midiSend` manda qualsiasi messaggio come byte grezzi per le cose che le
chiamate con un nome non coprono.
[#_a_reason_to_care]
=== Un motivo per interessarsene
Tutto questo è meccanica. Ecco a che cosa serve.
Metti insieme le due metà e il gioco chiede una nota, il giocatore la suona su
un vero strumento, e il gioco sa se l'ha azzeccata:
[source,lua]
----
function onMidiMessage(status, data1, data2, bytes)
if (status & 0xF0) ~= MIDI_NOTE_ON or data2 == 0 then
return
end
heard = noteName(data1)
if data1 == target then
score = score + 1
pickTarget()
playTarget()
end
end
----
Undici righe, e hai lo scheletro di un gioco per imparare la musica. Aggiungi
un orologio e hai un gioco ritmico che dà punti a chi suona davvero invece che
a quattro pulsanti di plastica. Aggiungi `data2` alla verifica e al gioco
importa quanto forte colpisci la nota. Confronta il momento in cui è arrivata
la nota con quello in cui era attesa, e stai valutando la tempistica al
millisecondo.
E siccome il MIDI esce oltre che entrare, un cabinato può fare cose che
nient'altro può fare. Un vero strumento collegato alla macchina. Un modulo
sonoro esterno che suona la partitura, così il motivo ha una profondità che
nessun banco di campioni gli dà. Luci da palco sul cabinato che lampeggiano a
tempo perché il gioco ha mandato loro il tempo. Se sei il tipo di persona che
costruisce cabinati, questa è la chiamata che permette al cabinato di essere
una cosa e non una scatola attorno a un computer.
Lo script finito è `learn/27-midi.singe`: i comandi della musica, l'elenco
delle porte e il gioco a indovinare, in un centinaio di righe. La barra
spaziatrice manda fuori la nota, Alt sinistro mette in pausa la musica, Maiusc
sinistro la sfuma, e le frecce su e giù cambiano il volume. Eseguilo senza
hardware MIDI e la metà della musica funziona, mentre la metà MIDI dice `none`
due volte, che è proprio come dovrebbe comportarsi sulla macchina di un
giocatore.
[#_cleaning_up]
=== Fare pulizia
[source,lua]
----
function onShutdown()
midiCloseInput()
midiCloseOutput()
musicUnload(theme)
end
----
`onShutdown` viene chiamata una volta quando il gioco finisce, ed è il punto in
cui uno script restituisce ciò che ha preso. Chiudere una porta che non è
aperta non fa niente, quindi non c'è niente da controllare prima.
[#_try_it_27]
=== Prova tu
. *Fallo ripetere quattro volte invece che all'infinito.* Cambia
`musicPlay(theme, -1)` in `musicPlay(theme, 3)` e conta. Il numero è quante
volte si *ripete*, non quante volte suona.
. *Sfuma più a lungo.* Cambia la dissolvenza del pulsante che ferma la musica
in `musicStop(theme, 4000)` e ascolta la differenza che fanno quattro secondi.
Poi prova `musicStop(theme, 100)`.
. *Mostra il canale.* In `onMidiMessage`, ricava il canale con
`(status & 0x0F) + 1` e stampalo. Suona la tua tastiera e guarda su quale
canale è -- poi trova le sue impostazioni e cambia quel canale, per
dimostrare a te stesso che il numero è vero.
. *Dai punti alla velocity.* Dai un punto per la nota giusta, e tre punti se
`data2` è sopra cento, così il gioco premia chi suona con convinzione.
. *Suona la risposta.* Quando il giocatore azzecca una nota, fai suonare al
gioco un piccolo accordo di risposta con tre chiamate a `midiNoteOn` a un
paio di note di distanza e tre timer per rilasciarle. Per questo ti serve una
vera porta di uscita o un sintetizzatore software, e su Linux va bene
qualsiasi programma che si registri con ALSA.
[#_break_it_on_purpose_26]
=== Rompilo apposta
Le due scale del volume sono facili da confondere, e Singe preferisce fermarsi
piuttosto che suonare la tua musica a un livello che non intendevi. Aggiungi
una riga sotto `musicPlay(theme, -1)`, usando il numero che sarebbe il volume
massimo di un effetto sonoro se lo raddoppiassi a mente e sbagliassi il conto:
[source,lua]
----
musicSetVolume(200)
----
Singe si ferma prima che il gioco parta, indicando la riga del tuo file in
cui si trova quella chiamata:
----
107:musicSetVolume: Invalid music volume value: 200
----
La riga, la chiamata che ha protestato e la lamentela, che qui ti ripete il
numero, così non c'è discussione su che cosa è arrivato. La musica va da `0` a
`128`. Gli effetti sonori vanno da `0` a `63`. Quando una chiamata del volume
ferma il tuo gioco, la domanda da farti è a quale delle due famiglie stai
parlando.
[#_what_you_learned_27]
=== Cosa hai imparato
* In Singe la musica e gli effetti sonori sono separati: volumi propri, nessun
limite di canali per la musica e una pausa che segue la pausa del gioco.
* Un file musicale viene decodificato in memoria fin dall'inizio, quindi un
brano lungo costa memoria e va scaricato quando lasci il livello.
* `musicPlay(track, -1)` si ripete all'infinito; un numero positivo è quante
volte si ripete.
* Il volume della musica va da `0` a `128` e quello degli effetti da `0` a
`63`, e la differenza è per compatibilità con Hypseus, non un errore.
* `musicStop(track, milliseconds)` sfuma in uscita, e una sfumatura è la
rifinitura più economica che ci sia. Non c'è la dissolvenza incrociata,
perché un unico guadagno copre tutta la musica.
* Singe mostra il video seguendo l'orologio dell'audio, e `singeSetAudioDelay`
corregge il ritardo di qualunque cosa da cui esca il suono.
* Un file MIDI contiene note, non suono, e ha bisogno di un soundfont per farsi
sentire. Singe non ne include nessuno, quindi distribuisci il tuo, o la tua
musica sarà qualunque cosa la macchina del giocatore si trovi ad avere.
* Le porte MIDI sono tutt'altra cosa: messaggi dal vivo in entrata e in uscita,
su un cavo, che non hanno niente a che fare con i file MIDI.
* `onMidiMessage` ti passa i byte grezzi. I quattro bit alti di `status` sono
il tipo, i quattro bassi sono il canale, e un note on con velocity zero vuol
dire note off.
* Ogni `midiNoteOn` ha bisogno di un `midiNoteOff`, e un timer è il modo per
abbinarli.
* Su una macchina senza MIDI ogni chiamata risponde `false` o zero e niente
fallisce, quindi il supporto MIDI si può distribuire tranquillamente.
[#_next_time_27]
=== La prossima volta
Il tuo gioco ha la musica, e un punteggio che una tastiera MIDI può far salire.
La ventottesima lezione porta quel punteggio fuori dalla macchina: account,
classifiche dei punteggi record che possono vedere anche gli altri, e il
catalogo che fa conoscere il tuo gioco al mondo.