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

608 lines
22 KiB
Text

[#_lesson_10_sound]
== Lección 10: Sonido
image::learn/10-sound.png[La lección terminada, 480]
Un juego sin sonido parece roto de una forma difícil de señalar. Los disparos
salen y no pasa nada. Las rocas se rompen y no pasa nada. Ponle a cada una de
esas cosas un ruido de medio segundo y, de repente, el mismo juego parece hecho
de algo.
Esta lección le da a tu juego un disparo, una explosión y música de fondo. Por
el camino te presenta tu quinto callback: el que usa el motor para decirte que
un sonido ha terminado de sonar.
[#_one_noise]
=== Un ruido
El kit de arte que copiaste en la lección nueve trae dos sonidos, `shoot.wav` y
`boom.wav`, y de los dos la explosión es la que mejor se oye. Crea un archivo
nuevo junto a tu carpeta `art` y escribe esto.
[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
----
Ejecútalo y pulsa la barra espaciadora tres o cuatro veces, rápido. Los
estallidos se amontonan unos encima de otros en lugar de cortarse entre sí.
Mantener pulsada la barra espaciadora te da exactamente un estallido, no un
chorro de ellos. Es el mismo comportamiento que viste en la lección tres: una
tecla mantenida es una sola pulsación, que se avisa una vez, y el motor tira las
repeticiones que manda tu teclado. Por eso, para mover la nave, hace falta
`onInputReleased` y una variable en lugar de contar pulsaciones.
[#_what_just_happened_6]
=== Qué acaba de pasar
La forma es la misma que en la lección nueve: cargar una vez arriba, usar el
handle y devolverlo al final.
[source,lua]
----
local boom = soundLoad(DIR .. "art/boom.wav")
----
`soundLoad` lee un archivo de audio, lo guarda en memoria y te devuelve un
handle -- la misma idea que el handle de un sprite, un número que para el motor
significa «ese clip». `DIR` está ahí por la misma razón que en la lección nueve:
el sonido vive en la carpeta de tu juego, y el juego tiene que poder encontrarlo
desde dondequiera que se haya arrancado Singe.
Todo lo que dijo la lección nueve sobre cargar vale aquí palabra por palabra.
Carga arriba del todo del archivo, una vez. Un `soundLoad` dentro de
`onOverlayUpdate` carga el clip sesenta veces por segundo y no devuelve ninguno,
y el juego se muere de eso unos minutos después.
[source,lua]
----
soundPlay(boom)
----
Reprodúcelo. Eso es todo lo que hace falta para reproducir un efecto de sonido,
y el motor se encarga del resto: el clip empieza, termina y nunca le estorba a
tu programa.
Pero `soundPlay` también devuelve algo, y *no* es el mismo número que le diste.
Cambia la línea y mira:
[source,lua]
----
local channel = soundPlay(boom)
debugPrint("clip " .. boom .. " on channel " .. channel)
----
Pulsa la barra espaciadora rápido media docena de veces y mira los dos números.
El número del clip no cambia nunca. El número del canal sube -- 0, luego 1,
luego 2 -- y en cuanto terminan los estallidos anteriores vuelve a bajar y
empieza a reutilizar los números bajos.
Esto es lo único del sonido que pilla a todo el mundo, así que vale la pena
decirlo despacio. *Hay dos tipos distintos de número en juego.*
El *handle del clip* que da `soundLoad` es la grabación. Solo hay uno. No
cambia, no está ni sonando ni callado: es el sonido guardado en memoria,
esperando a que lo uses.
El *canal* que da `soundPlay` es una reproducción de esa grabación. Singe tiene
dieciséis, numerados del `0` al `15`, y cada sonido que se oye en este momento
está usando uno. Por eso los estallidos se solapan: la segunda pulsación no
interrumpió la primera, sino que empezó una segunda reproducción en el siguiente
canal libre.
Todo lo que actúa sobre un *sonido que puedes oír* recibe un canal:
`soundStop`, `soundIsPlaying`, `soundPause`, `soundResume`. Todo lo que actúa
sobre la *grabación* recibe el handle: `soundPlay` y `soundUnload`. Si pasas uno
donde va el otro, no siempre te saldrá un error, porque `3` es un número de
canal válido y también un handle de clip perfectamente bueno -- te saldrá
silencio, o se parará el sonido equivocado, y eso cuesta mucho más encontrarlo.
Dieciséis son muchos, pero no infinitos. Cuando todos los canales están
ocupados, no suena nada y `soundPlay` devuelve `-1` en lugar de un número de
canal. Un juego que lanza muchísimos sonidos a la vez debería contar con ello, y
vas a escribir uno dentro de un momento.
[#_volume]
=== Volumen
Hay tres volúmenes, y se multiplican entre sí.
El volumen *general* de los efectos lo abarca todo. `soundGetVolume` lo lee y
`soundSetVolume` lo cambia, en una escala de `0` a `63`.
[source,lua]
----
soundSetVolume(40)
----
Resiste. Ese número es del jugador, no tuyo: empieza en lo que pidiera al lanzar
el juego, y un juego que lo sobrescribe es un juego que suena demasiado alto en
la habitación tranquila de alguien. Cambia el volumen general solo cuando te lo
pida el jugador, desde una pantalla de opciones. `soundGetVolume` existe para
que una pantalla de opciones pueda mostrar cuál es ahora en lugar de adivinarlo.
Lo que deberías usar es el volumen del *canal*, que está por debajo del general.
Dale a `soundPlay` un tercer argumento y solo esa reproducción sonará más baja:
[source,lua]
----
soundPlay(shootClip, 0, 35)
----
El `0` del medio es cuántas veces repetirlo, y tienes que ponerlo porque el
volumen va detrás. `0` significa reproducirlo una vez. `-1` significa repetirlo
en bucle hasta que algo lo pare, que es como harías el zumbido de un motor o el
viento. La entrada de `soundPlay` en la referencia enumera los tres argumentos.
El tercer volumen es la distancia, para sonidos colocados en un mundo 3D. Eso
queda para la cuarta parte, cuando el juego pase a tres dimensiones.
La música tiene su propio volumen, y va en otra escala. Enseguida hablamos de
eso.
[#_stopping_and_not_stopping]
=== Parar y no parar
`soundStop` recibe un canal y lo corta allí donde esté:
[source,lua]
----
soundStop(engineChannel)
engineChannel = -1
----
Fíjate en la segunda línea. En cuanto un canal se para, el motor puede darle ese
mismo número al siguiente `soundPlay`, así que un número de canal que todavía
guardas puede pertenecer ahora a un sonido completamente distinto. Tíralo en el
momento en que lo pares. `-1` es lo útil que poner ahí, porque es el mismo valor
que te da `soundPlay` cuando no ha podido reproducir nada, así que una sola
comprobación cubre los dos casos: `if channel >= 0 then`.
`soundStop` es para un sonido que ya no debería sonar -- el ruido del motor
cuando el motor se rompe, la alarma cuando alguien la atiende. Es la herramienta
equivocada para un efecto corto. Una explosión de medio segundo cortada a la
décima de segundo suena a error, porque lo es.
Así que, casi siempre, no hagas nada. Deja que termine. Un efecto de sonido que
empiezas y en el que no vuelves a pensar termina solo y libera su canal solo,
que es exactamente lo que quieres.
Esto vale incluso al final. `soundUnload` no corta un clip que todavía está
sonando: el motor mantiene vivo el audio hasta que se para el último canal que
lo usa. Puedes descargar en `onShutdown` sin preocuparte de haber silenciado
algo.
`soundIsPlaying(channel)` responde si un canal está sonando ahora mismo. Su uso
principal no es la curiosidad sino la contención: una alarma que debe sonar una
vez, y no volver a empezar cada vez que el juego nota que el peligro sigue ahí.
[source,lua]
----
function raiseAlarm()
if alarmChannel < 0 or not soundIsPlaying(alarmChannel) then
alarmChannel = soundPlay(alarmClip)
end
end
----
Lee la condición en voz alta: reproduce la alarma si no hay canal de alarma, o
si el que tienes se ha parado. Si no, déjala en paz.
[#_the_sound_that_stutters]
=== El sonido que tartamudea
Aquí está el error. Todo el mundo lo escribe, normalmente en la primera semana
después de descubrir el sonido.
Supón que quisieras que la nave siguiera disparando mientras el botón está
pulsado, en lugar de una vez por pulsación. Lo harías igual que la dirección:
una variable `firing` que se pone a `true` en `onInputPressed` y vuelve a
`false` en `onInputReleased`, y el disparo se crea en `onOverlayUpdate` mientras
sea verdadera. Es algo perfectamente razonable de querer, y el ruido parece que
va justo ahí, con el disparo:
[source,lua]
----
function onOverlayUpdate()
if firing then
shots[#shots + 1] = { x = shipX, y = shipY }
soundPlay(shootClip)
end
...
----
Lo que sale no es un disparo. Es un traqueteo zumbón, o un tono plano y duro, o
un sonido como de una maquinita rompiéndose.
`onOverlayUpdate` se ejecuta unas sesenta veces por segundo. Mantén el botón un
segundo y habrás pedido sesenta reproducciones de un clip que dura más o menos
una quinta parte de segundo, cada una empezando encima de la anterior. En
cualquier momento hay unas once sonando, cada una un fotograma por detrás de la
anterior, y lo que oyes son sesenta copias del mismo ataque amontonadas unas
sobre otras.
El arreglo no es un sonido más ingenioso. Es reproducir el sonido donde ocurre
el *evento* y no donde el *estado* es verdadero:
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
soundPlay(shootClip, 0, 35)
end
end
----
`onInputPressed` ocurre una vez, cuando se pulsa el botón. Una pulsación, un
disparo, un ruido.
Si de verdad quieres fuego continuo, la respuesta es la misma con otra forma:
convierte el propio disparo en un evento. Cuenta fotogramas, exactamente como
los contaba el ciclo de caminar en la lección nueve, y crea un disparo solo cada
ocho fotogramas. El ruido va con el disparo, así que suena siete u ocho veces
por segundo en lugar de sesenta, y eso es una ametralladora y no un traqueteo.
Esa es la regla, y vale la pena llevártela de esta lección: *los sonidos van con
los eventos, no con las condiciones*. Una pulsación, un impacto, una muerte,
recoger algo -- cada una de esas cosas ocurre una vez y merece un sonido. «El
botón está pulsado» y «la nave se mueve» no son eventos; son verdad durante
cientos de fotogramas seguidos, y todo lo que reproduzcas a partir de ellos se
reproduce cientos de veces.
Cuando un sonido sí pertenece a un estado que dura -- un motor, el viento, una
sirena -- empiézalo una vez con `-1` como número de repeticiones, guarda el
canal y páralo cuando termine el estado.
[#_the_fifth_callback]
=== El quinto callback
A veces sí necesitas saber cuándo ha terminado un sonido. Escribe esta función y
el motor la llamará:
[source,lua]
----
function onSoundCompleted(channel)
end
----
Ese es tu quinto callback, después de `onOverlayUpdate` en la lección uno, los
dos callbacks de entrada de la lección tres y `onShutdown` en la lección nueve,
y funciona igual que todos ellos:
lo escribes tú, nunca lo llamas, y el motor te llama a ti. El `channel` que te
pasa es el canal que acaba de quedarse en silencio -- el número que te dio
`soundPlay` cuando lo empezaste.
Salta cuando un clip se acaba, cuando termina la última repetición de un bucle,
*y* cuando paras tú mismo un canal con `soundStop`. Esto último sorprende a
quien usa el callback para encadenar un sonido con el siguiente: parar un canal
a propósito también lo da por completado, y si no tienes cuidado el siguiente
sonido de la cadena empieza de todos modos.
El juego está a punto de usarlo para algo que merece la pena. Cuando se pierde
la última vida, suena la explosión de la nave y el juego se queda quieto -- sin
GAME OVER, sin que la música se desvanezca -- hasta que esa explosión ha
terminado de verdad. Entonces todo se para a la vez. Son un puñado de líneas y
es la diferencia entre un final y un corte.
[#_music_is_not_a_sound_effect]
=== La música no es un efecto de sonido
Podrías cargar una canción de tres minutos con `soundLoad` y reproducirla con
`soundPlay`. No lo hagas. La música tiene su propia familia de llamadas, por
razones que importan:
* La música tiene su propio volumen, separado del de los efectos, para que un
jugador pueda bajar la música y seguir oyendo los disparos.
* No hay límite de dieciséis canales ni número de canal. Un handle es una pieza
de música, y la paras, la pausas y la reanudas con ese handle.
* `musicStop` puede desvanecer una pieza durante un segundo o dos en lugar de
cortarla.
Las llamadas son un reflejo de las que ya conoces:
[source,lua]
----
local theme = musicLoad("Singe/menuIntro.flac")
musicSetVolume(50)
musicPlay(theme, -1)
----
`musicLoad` da un handle. `musicPlay` la empieza, con el mismo número de
repeticiones que `soundPlay`: `-1` para siempre. `musicStop(theme)` la termina,
y `musicStop(theme, 1500)` tarda un segundo y medio en desvanecerla.
`musicUnload` la devuelve.
Ahora, la trampa. *El volumen de la música va de 0 a 128, no de 0 a 63.* Los
efectos de sonido usan una escala y la música otra, el doble de larga, por
razones históricas que no consuelan nada cuando tu música sale a la mitad del
volumen que querías. Cuando trabajes con música, dobla el número que tenías en
mente para un efecto.
La pieza que se carga ahí es la música del menú del propio motor, sacada de la
carpeta `Singe` que el motor desempaquetó en tu carpeta de trabajo la primera
vez que lo ejecutaste. Dura diez segundos y se compuso para desvanecerse al
final, así que en bucle suena exactamente como un bucle de diez segundos que no
para de desvanecerse. Eso está bien para aprender las llamadas y no sirve de
nada en un juego terminado, que trae una pieza propia más larga. Cualquiera de
los formatos habituales sirve: la sección Música de la referencia los enumera, y
Ogg Vorbis o MP3 son las opciones sensatas, porque unos minutos de música en WAV
son un archivo enorme.
[#_the_game_gets_a_soundtrack]
=== El juego tiene banda sonora
Abre el juego de la lección nueve. Todo lo de aquí se le añade; no se quita
nada.
Cuatro cosas que cargar, arriba, con los sprites:
[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")
----
Las dos primeras llevan `DIR` y las dos últimas no, y es a propósito. `DIR`
significa la carpeta de *tu* juego. Un nombre que empieza por `Singe/` significa,
en cambio, la carpeta del propio motor, esté donde esté -- y por eso
`dofile("Singe/Framework.singe")` ha funcionado desde la lección siete sin `DIR`
tampoco. Tus propios archivos necesitan `DIR`; los del motor, nunca.
Dos variables nuevas, junto a las demás:
[source,lua]
----
local dying = false
local deathChannel = -1
----
`dying` es el momento entre que se pierde la última vida y que termina la
partida: la explosión aún suena y nada se mueve en pantalla.
El disparo recibe su ruido en `onInputPressed`, justo donde se crea el propio
disparo, y el reinicio recibe un clic para que pulsar `1` parezca que ha hecho
algo:
[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 roca alcanzada por un disparo estalla, y también una roca que te golpea. En
`updateShots`, junto a la puntuación:
[source,lua]
----
newRock(rock)
score = score + HIT_SCORE
gone = true
soundPlay(boomClip, 0, 45)
----
Y en `updateRocks`, donde se pierde la vida, la parte interesante:
[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
----
Cuando se pierde la última vida, suena la explosión y se guarda su canal. Ahora
el juego está en `dying`, que para todo lo que se mueve pero todavía no dice
GAME OVER.
El `if channel < 0` es el límite de dieciséis canales, resuelto. Si justo en ese
momento todos los canales estuvieran ocupados, no hay explosión ni canal que
esperar, y sin esas dos líneas el juego se quedaría en `dying` para siempre,
esperando un sonido que nunca sonó. Esperar algo que no puede llegar es una de
las formas más fáciles de colgar un programa, y la protección siempre sale más
barata que el fallo.
Ahora el callback nuevo, y la pequeña función que comparte con esa protección:
[source,lua]
----
function endGame()
dying = false
deathChannel = -1
over = true
musicStop(theme, 1500)
end
function onSoundCompleted(channel)
if channel == deathChannel then
endGame()
end
end
----
El motor llama a `onSoundCompleted` por *cada* canal que se para, así que lo
primero que hace es comprobar si este es el canal que le importa. Todo lo demás
-- cada disparo, cada roca -- pasa de largo y se ignora.
`onOverlayUpdate` tiene que quedarse quieto mientras dura la explosión, y eso es
una palabra:
[source,lua]
----
if not over and not dying then
updateShip()
updateShots()
updateRocks()
end
----
`startGame` reinicia las dos variables nuevas y vuelve a empezar la música:
[source,lua]
----
dying = false
deathChannel = -1
musicSetVolume(50)
musicPlay(theme, -1)
----
Y `onShutdown` devuelve lo que se cargó:
[source,lua]
----
function onShutdown()
spriteUnload(shipSprite)
spriteUnload(rockSprite)
spriteUnload(shotSprite)
spriteUnload(starSprite)
soundUnload(shootClip)
soundUnload(boomClip)
soundUnload(clickClip)
musicUnload(theme)
end
----
Ejecútalo. Dispara a las rocas, deja que una te golpee tres veces y escucha el
final: el último estallido suena entero, y solo cuando se ha ido empieza la
música a desvanecerse y aparecen las palabras.
El script terminado está en la carpeta `learn` como `10-sound.singe`, por si
quieres comparar.
[#_try_it_10]
=== Pruébalo
. *Baja todavía más el disparo.* Cambia el `35` del `soundPlay` del disparo por
`10`, y luego por `63`. Encuentra el número con el que queda por debajo de las
explosiones en lugar de por encima.
. *Baja la música sin tocar los efectos.* Cambia `musicSetVolume(50)` por
`musicSetVolume(15)`. Comprueba que los estallidos suenan exactamente igual de
fuerte que antes, que es toda la razón de que la música sea un sistema aparte.
. *Corta el final.* En `endGame`, cambia `musicStop(theme, 1500)` por
`musicStop(theme)`. Escucha los dos finales dos veces. La diferencia es un
número en un argumento, y no es una diferencia pequeña.
. *Ponle un motor a la nave.* Arranca `soundPlay(clickClip, -1, 12)` en
`startGame`, guarda el canal que te da en una variable propia y páralo en
`endGame`. El clic dura una cincuentava parte de segundo, así que en bucle es
un zumbido, que es lo más parecido a un motor que da de sí este kit. Luego
vuelve a quitar el `soundStop`, juega tres partidas seguidas y escucha lo que
has construido.
. *Cuenta los canales.* Imprime el número de canal cada vez que explota una
roca. Luego sube `ROCK_COUNT` hasta que exploten varias a la vez, y sigue
subiéndolo hasta que veas pasar un `-1`.
[#_break_it_on_purpose_9]
=== Rómpelo a propósito
Ahora tienes dos escalas de volumen en la cabeza, y tarde o temprano te saldrá
de los dedos la equivocada. Añade esta línea suelta, justo debajo de las cuatro
líneas de `soundLoad` y `musicLoad`:
[source,lua]
----
soundSetVolume(100)
----
El juego se niega a arrancar:
----
20:soundSetVolume: Invalid sound volume value: 100
----
El número de línea que te sale es la línea en la que de verdad la escribiste,
así que puede que el tuyo no sea 20. Luego viene la función que se ha quejado, y
el valor del que se ha quejado. El mensaje no te dice cuál es el rango -- para
eso está la entrada de `soundSetVolume` en la referencia -- pero sí te dice el
número que no ha aceptado, y en cuanto sabes que los efectos llegan hasta 63, el
arreglo es un número.
`100` es un número sospechoso para un volumen, y vale la pena saber por qué lo
escribiste. Casi seguro que pensaste en el volumen como un porcentaje, porque
todos los controles de volumen que has usado en tu vida llegan a 100. Los de
Singe no. Los efectos van de `0` a `63` y la música de `0` a `128`, y ninguno es
un porcentaje de nada.
Esta es la forma general de un error del motor, a diferencia de los errores de
Lua de la lección uno: un número de línea, el nombre de la función del motor que
llamaste y una queja sobre los valores que le pasaste. Cuando veas uno, lo
siguiente que hay que leer es la entrada de esa función en la referencia.
[#_what_you_learned_10]
=== Lo que has aprendido
* `soundLoad` lee un clip y devuelve un handle. Carga una vez, arriba del todo
del archivo.
* `soundPlay` empieza un clip y devuelve un *canal*, que es un tipo de número
distinto del handle.
* Singe mezcla dieciséis canales. Cuando están todos ocupados, `soundPlay`
devuelve `-1` y no suena nada.
* Los números de canal se reutilizan. Olvida uno en cuanto lo hayas parado.
* A los efectos cortos hay que dejarlos terminar en paz; `soundStop` es para
sonidos que ya no deberían sonar.
* `soundUnload` no corta un clip que todavía está sonando.
* Reproduce un sonido donde ocurre el evento, no donde una condición es
verdadera, o sonará sesenta veces por segundo.
* `onSoundCompleted(channel)` es un callback que el motor llama cada vez que un
canal se queda en silencio, por el motivo que sea.
* La música es un sistema aparte, con sus propios handles, su propio volumen y
sin límite de canales.
* El volumen de los efectos va de `0` a `63`. El de la música, de `0` a `128`.
No son porcentajes.
[#_next_time_10]
=== La próxima vez
Tus rocas reciben un impacto cuando sus rectángulos se solapan, y por eso cuenta
incluso un disparo que pasa por una esquina de cielo. En la lección once tendrás
las comprobaciones de colisión del propio motor, aprenderás qué es una zona de
impacto y descubrirás por qué el rectángulo que rodea una roca redonda tiene la
forma equivocada.