Log In  

Is it possible to pause or mute music in Pico-8 from script?

I want to halt the music for a short period when the player dies, and resume it when they respawn. Pause or Mute would both work in this case; I just don't want to restart the whole song every time they die.


This is what I currently do:

-- Player Spawns...

--Player dies

-- Player Spawns...
P#35493 2017-01-11 20:59 ( Edited 2017-01-12 01:59)

Hey, @mhughson, have you figured out any other workaround for pausing (or muting music)? This would be super useful to me too.

P#70323 2019-11-28 17:08

Sorry @alanxoc3, but I don't even remember which project this was for!

P#70324 2019-11-28 17:10

This works sort of. It will record the music position where you are and continue it. Unfortunately there does not seem to be a way to play back according to location in the music position either peek or poke, I checked.

Now while STAT() does contain information about what is playing and where, you cannot reverse to send STAT back to the best of my knowledge.

Cart #totafohese-0 | 2019-11-28 | Code ▽ | Embed ▽ | No License


P#70331 2019-11-28 19:33

A bit brutal (stops sfx as well) but that undocument poke was found some time ago: https://twitter.com/bone_volt/status/1173584954359517184

P#70334 2019-11-28 20:06

Ok, @dw817, I didn't know stat could tell you music information. But not exactly what I was looking for.

I did figure something out though. One way pauses the music with a specific memory location with poke. The other way will mute the music, while still playing it. Here is an example cartridge.

Cart #pause_music-0 | 2019-11-28 | Code ▽ | Embed ▽ | No License

The downside to using poke is that it pauses all sound for the cartridge, so you can't play any sound effects while the sound is paused. The other way plays a sound effect over the music. If the sound effect is empty and looping, it can act as "muting" the music.

EDIT: Note that playing an empty sound effect over the music also lets you play sound effects while the music is muted!

P#70335 2019-11-28 20:07 ( Edited 2019-11-28 20:10)

@freds72 I did find out about that just before you commented :), but yeah, it is a bit too brutal for me.

P#70336 2019-11-28 20:09

Pretty nifty poke there, @freds72. Adding to my notes ... thanks.

@alanxoc3, glad to help out !

P#70339 2019-11-28 20:37 ( Edited 2019-11-28 20:57)

Ehe, I was peeking at the API and discovered that sfx does take an offset argument to decide from which note to start. But not music. We can access the note currently played via stat(20) through stat(23), which is more precise than the pattern number returned by stat(24) (although possibly with some advance, esp. on web player, see https://pico-8.fandom.com/wiki/Stat#.7B16.E2.80.A626.7D_Sound_and_music_status). But how to resume full music?

My solution would be to write a custom music manager, that uses a custom music data format stored at runtime, plays many SFX at once to simulate multi-channel music, and use either stats or custom frame count to guess when we should play the next SFX. The main issue (besides extra code needed) is synchronizing: note stats may not be precise depending on user audio system, while frames may be off due to music running on a separate thread (another thread on this forum shows how music and code counting frames can be offset over time: https://www.lexaloffle.com/bbs/?tid=3969). But that may be worth trying.

I won't be going that route, however, as I found two (and a half) workaround for my needs:

a. I know how long I need to pause the music, and that duration was half of the duration of a music pattern (I was playing a jingle during that time). Instead of resuming the music at the beginning of the last pattern, I resume it at the next pattern. Since it is approximately where the music would be if it had continued playing at "volume 0", the player just feels like the jingle "covered" the music for some time, like SFX when there are too many for the number of channels and they cover part of the music (e.g. the first Pokemon games and old-school games on 4-channel hardware in general). I tried this approach and it works well, you don't really notice that you don't always skip the same amount of time and that the music always start at the beginning of a pattern (rather, starting at the beginning of a pattern guarantees it not to clash when it comes back).

a'. By the way, if you want to exactly resuming the music at the point it should if it had played continuously, you can push the idea further by playing 4 blank SFX for the needed duration of muting, to cover all channels. If you don't know how long muting will happen in advance (e.g. dynamic event controlled by player), you may want to play short blank SFX regularly until some condition is false. I didn't try this approach. Note that if you want an actual pause menu that pauses the music (but not other sounds, otherwise we already have the solution to stop all sounds), then the user will probably notice that the music skipped forward...

b. Reduce volume of all music tracks temporarily. This allowed me fancier results like only reducing volume by half instead of muting it completely. It may work or not for you, depending on your needs.

Below, the code snippet to reduce volume on a range of tracks (use this on all tracks used by your music; or at least the one you expect to be played during the volume decrease range). You can set decrease_value to 7 or adapt the function so it effectively sets volume to 0 and mute the music (but you cannot reuse those tracks for something else while music is running!)

local volume = {}

-- decrease volume for all tracks between `from` and `to` by `decrease_value`
-- use this to reduce the volume of music while keeping it active
--  so you can restore volume later (with a cartridge reload) without interruption
function volume.decrease_volume_for_track_range(from, to, decrease_value)
  for track = from, to do
    volume.decrease_volume_for_track(track, decrease_value)

-- decrease volume for sound effect #track by `decrease_value`
function volume.decrease_volume_for_track(track, decrease_value)
  for i = 0, 31 do
    -- 32 sounds per track
    volume.decrease_volume_for_sound(track, i, decrease_value)

-- decrease volume for note #note (index starting at 0) of sound effect #track by `decrease_value`
function volume.decrease_volume_for_sound(track, note, decrease_value)
  -- https://pico-8.fandom.com/wiki/Memory > Sound effects
  -- sound effects memory starts at 0x3200
  -- a track takes 68 bytes
  -- a sound takes 2 bytes, and volume is located in 2nd byte
  local note_higher_byte_addr = 0x3200 + 68 * track + 2 * note + 1
  local new_higher_byte = volume.compute_sound_higher_byte_with_decreased_volume(peek(note_higher_byte_addr), decrease_value)
  poke(note_higher_byte_addr, new_higher_byte)

-- return higher byte of sound in memory after modifying volume bits so that
--  the volume is decreased by `decrease_value` (clamped to 0)
function volume.compute_sound_higher_byte_with_decreased_volume(higher_byte, decrease_value)
  -- https://pico-8.fandom.com/wiki/Memory > Sound effects
  -- the higher byte is compounded like this:
  -- Higher bit          Lower bit
  -- c  e   e   e   v   v   v   w
  -- volume is contained in the 3 'v' bits, under mask 0b00001110 = 0xe = 14

  -- first, we extract volume (mask + shift right by 1 bit to cover offset of 'w')
  -- we use band instead of & to be compatible with picotool and busted,
  --  but instead of >> 1 we can divide by 0b10 = 2 instead of using shr
  -- second, decrease volume down to 0
  local volume = max(0, band(higher_byte, 14) / 2 - decrease_value)

  -- third, clear the volume bits in the temp higher byte
  --  by applying complementary mask
  -- fourth, re-add decremented volume shifted back by 1 to the left (* 0b10 = 2)
  return bor(band(higher_byte, bnot(14)), volume * 2)

return volume

(repo: https://github.com/hsandt/pico-boots/blob/develop/src/engine/audio/volume.lua)

After that, you may want to restore the original volume by reloading the sfx used by the music entirely.

reload(0x3200, 0x3200, 0xd48, "bgm.p8")

But it will stop for a moment to reload (unless you hack PICO-8 so it doesn't) so it can work for a pause menu or cinematics but not something in the middle of the action.
As an alternative, you can store the original sound effects memory in general memory with memcpy to paste it later back onto your sound effects memory without lag.
Finally, you can also store the list of original volumes in a custom Lua table to restore the volumes later (you'll have to adapt my function to set sound volumes to arbitrary values).

P#85138 2020-12-08 00:02 ( Edited 2020-12-08 00:03)

[Please log in to post a comment]

Follow Lexaloffle:          
Generated 2023-02-03 07:17:14 | 0.031s | Q:33