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