Log In  

I didn't see this documented anywhere, and I had to work it out for my drumbeat cart, so I thought I'd save everyone else the trouble and publish what I know.


A sound effect is stored in 68 bytes. Sfx 0 starts at address 0x3200, sfx 1 at address 0x3244, and so on.

The first 64 bytes (offset 0..63) store the 32 notes of the sound effect as described below.

Offset 64 is a byte that determines the editor mode: 0 for graph mode, nonzero for tracker mode. This has no effect on playback, just on the mode opened in the editor by default.

Offset 65 stores the playback speed. (Higher values correspond to slower playback, so "speed" isn't exactly the right name, but that is what is used in the editor.)

Offsets 66 and 67 store the loop begin and loop end times. A sound effect contains only 32 notes, but these times may be set to any byte value. All times past 32 simply play silence.

Note format:

A note is stored in 16 bits, two bytes little-endian.

Bits 0..5 store the pitch, ranging from 0 (C in octave 0) to 63 (D# in octave 5). The tracker mode editor can only enter pitches up to 60 (C 5), but it can display and play higher pitches.

Bits 6..8 store the instrument; the values are the same as in the tracker mode editor.

Bits 9..11 store the volume. Volume 0 corresponds to silence.

Bits 12..14 store the effect; the values are the same as in the tracker mode editor.

Bit 15 appears to be unused.

Example code:

-- This code is public domain, feel free to copy, use, and modify however you'd like

function make_note(pitch, instr, vol, effect)
  return { pitch + 64*(inst%4) , 16*effect + 2*vol + flr(instr/4) } -- flr may be redundant when this is poke'd into memory

function get_note(sfx, time)
  local addr = 0x3200 + 68*sfx + 2*time
  return { peek(addr) , peek(addr + 1) }

function set_note(sfx, time, note)
  local addr = 0x3200 + 68*sfx + 2*time
  poke(addr, note[1])
  poke(addr+1, note[2])

function get_speed(sfx)
  return peek(0x3200 + 68*sfx + 65)

function set_speed(sfx, speed)
  poke(0x3200 + 68*sfx + 65, speed)

function get_loop_start(sfx)
  return peek(0x3200 + 68*sfx + 66)

function get_loop_end(sfx)
  return peek(0x3200 + 68*sfx + 67)

function set_loop(sfx, start, end)
  local addr = 0x3200 + 68*sfx
  poke(addr + 66, start)
  poke(addr + 67, end)

Music format:

A music pattern consists of four channels, each of which may have a sound effect played on it, plus some playback control flags.

The patterns are stored in four bytes, with pattern 0 at address 0x3100. Each byte describes one channel.

Bits 0..5 are the sfx to be played on the channel.

Bit 6 is mute. If it is 0, the channel is played; if it is 1, the channel is silenced.

Bit 7 contains the playback control flags:
Byte 0 bit 7 is the loop start flag
Byte 1 bit 7 is the loop end flag
Byte 2 bit 7 is the stop flag
Byte 3 bit 7 is unused

Playback control is as follows (I haven't tested all possibilities here):
When a pattern reaches the end, if the stop flag is set on this pattern, playback stops.
Otherwise, if the loop end flag is set on this pattern, move back until a pattern with the loop start flag set is found, and continue playback from there.
Otherwise, continue playback with the next pattern.

P#13179 2015-08-26 18:29 ( Edited 2018-06-04 05:54)

thanks for posting this!

I worked out the sfx layout too and posted in another thread, but it probably wasn't easy to find.

The music docs will be handy =)

P#13180 2015-08-26 21:27 ( Edited 2015-08-27 01:27)

Should we have a wiki?

P#13185 2015-08-27 00:56 ( Edited 2015-08-27 04:56)

A wiki would be amazing.

P#13204 2015-08-27 13:24 ( Edited 2015-08-27 17:24)

Thank you for documenting this (and for your little copy-paste ready API for lazy people like me)

A wiki would definitely be a great idea for stuff like this. This doc could also be added to the cheat sheet in the meantime.

P#13274 2015-08-29 16:24 ( Edited 2015-08-29 20:24)

Thank you as well, especially for the functions. Yes we need a wiki.

P#22503 2016-06-08 07:23 ( Edited 2016-06-08 11:23)
P#22504 2016-06-08 07:56 ( Edited 2016-06-08 11:57)

Thanks matt :) Are there plans to make it more prominent, eg. put it in the menu bar next to "Forum"?

Should I add this to the sound page?

Or the code snippit page?

P#22957 2016-06-15 20:11 ( Edited 2016-06-16 00:11)

It's not my wiki, but @zep has said this is as good as we will get until he adds something on here eventually

P#22962 2016-06-15 20:36 ( Edited 2016-06-16 00:36)

I'm actively building out the wiki at the moment. I have a ToC in progress including a Memory page that would include this stuff. I'll gladly accept help but I'd like to get the bones in place, if that's OK. (I was going to make an announcement when finished, and had assumed nobody was looking at the wiki. :) )

I have complete memory and file format reference docs in the comments of my picotool library here, including sfx and music: https://github.com/dansanderson/picotool/tree/master/pico8

P#22963 2016-06-15 20:59 ( Edited 2016-06-16 01:00)

this will be so useful when designing sfx for games! and dynamic music. extremely appreciated <3

P#22972 2016-06-16 00:53 ( Edited 2016-06-16 04:53)
P#22983 2016-06-16 08:34 ( Edited 2016-06-16 12:34)

You can look at my code here:

which do a quasi real time read of the currently played note (and more) on each channels.

P#23071 2016-06-17 15:10 ( Edited 2016-06-17 19:10)

Bits 0..5 store the pitch, ranging from 0 (C in octave 0) to 63 (D# in octave 5).

Someone correct me if I'm wrong—but isn't the Pico-8 range actually from C in octave 2 (~65.5Hz) to D# in octave 7 (~2489Hz)?

Compare middle C (C4) to this:


for t=0,31 do

--play middle c

function _update()
P#23077 2016-06-17 16:00 ( Edited 2016-06-17 20:09)


This is exactly what i needed for making some generative music for my game.

P#24256 2016-07-02 07:20 ( Edited 2016-07-02 11:20)


(Written since my last post to this thread. :) )

P#24367 2016-07-03 00:11 ( Edited 2016-07-03 04:11)

Is there a way to modify assigned notes with strings instead of one byte at a time? Highly relevant info for my DDR-inspired project, which may require quite a bit of on-the-fly editing.

The song select screen is broken up into 7 song chunks - so each page has a sample set, as well as typical music menu for "Course Mode" (which really, is just playing 4 of the songs in sequence of appearance so far) or "Marathon Mode" (same thing, but now all seven).

After selecting/launching a song though; the idea is to break the song into 4 parts - an "Outro/Intro" chunk (both composed in the first set, and the "begin" flag set to where the intro begins), a verse and a chorus (which I'm planning a different sequence script with, so it can do stuff like "intro, verse, chorus, verse, chorus, outro), and the fourth section is mostly for buildup or some other unique part of the song.

THIS part may also have to be recomposed/arranged mid-stage by an invisible flag hitting the stepzone - hence why I'd much rather use a one-shot string to do so than bytes. Or, at least the composition can be prearranged in the SFX editor, but the assigned SFX would have to be swapped for the fourth part in this case, or in the event of a "Game Over."

As far as the speed thing goes, it's just "programming steps between notes," really. Do you know if it accepts double/decimal values, or just integer ones? Most of the songs I want to make on it are doable, but there's a few icky BPM ranges that don't line up well (most notably, 170~340, 190~380, and 400). The backup plan so far is to compose them in lower BPMs (higher speed), and then artifically multiply the arrow speed with a variable, but this will make them very simplified compositions.

P#24778 2016-07-07 00:10 ( Edited 2016-07-07 04:10)

It sounds like you might prefer editing in the sound/music editor then using memcpy() to copy the sound and music data regions to an unused portion of gfx memory or similar. If you really need to store this data as a string, you can base64-encode it and write a little decoder routine. If you're looking to make your own string representation of sound data such that you can edit songs directly in the source code, that's also possible with a similar technique. (You'd use sub() calls to access chars in the string.)

Note speed is an integer between 1 and 255, a multiple of 1/128ths of a second. Of course, how that translates to BPM depends on how you're using the sound pattern.

P#24791 2016-07-07 02:41 ( Edited 2016-07-07 06:41)

I made a lil cart using this! Thanks for sharing!

P#53260 2018-06-04 01:54 ( Edited 2018-06-04 05:54)

not sure how to build a splitter function

like the inverse of make_note

that'd be handy!

P#82865 2020-10-13 06:22

This is so great - i´m building a sequencer with your help. Thanks!

P#96559 2021-08-28 09:14

Hi everyone. I'm trying to decode this from a .p8 file into a Love2d sound data.

I've already done the work to decode gfx/gff/map data in this thread https://www.lexaloffle.com/bbs/?tid=45366

But sound programming is completely new to me. I don't understand any of the terminology in that page, and how it relates to the code in the beep function example at https://love2d.org/wiki/love.sound.newSoundData

And even once I figure that out, there's data-less terminology on https://pico-8.fandom.com/wiki/P8FileFormat such as where it says waveform=2 is "sawtooth".

How can we work together to figure this out? Does anyone know any of this info and can help?

My goal is to finish writing this love2d lib for .p8 files, so that my son can make love2d games using all the contents in .p8 files that my other kids edit/create. And a game with only graphics and no sound just feels so empty.

I know that this thread has so far been about in-memory data formats of sound/music, and I'm asking about .p8 file format. But that's not the hard part. I can easily decode the .p8 file data according to the wiki page I linked to above. The problem is understanding it well enough to port it to love2d SoundData. That's what I need help with.

P#100390 2021-11-19 00:25 ( Edited 2021-11-19 00:27)

@catholic I remember telling you on discord and I'll say it again but this page (or at least the very top) is just how the data is stored and read by pico to get the info needed to play the music/sfx.
If you want to actually turn that data into sound you'll need the synth, which is part of pico so you'll either need to crack pico open which I have no idea how to, to see or reverse-engineer it.
Hope that helps clear that up.

(So you'll need to read this data but also put it through Pico's synth and then que that to the qsource.)

P#100396 2021-11-19 04:11 ( Edited 2021-11-19 04:55)

First of all I've never been on the discord, you must be thinking of someone else.

But surely there is another way to do this without "cracking" anything? Maybe experimenting with different patterns and trying to see what sounds right?

P#100408 2021-11-19 12:39

The code at the top has some small syntax errors. This is a corrected version.

function make_note(pitch,instr,vol,effect)
 return {pitch+64*(instr%4),16*effect+2*vol+flr(instr/4)}

function get_note(sfx_index,time_index)
 local addr=0x3200+68*sfx_index+2*time_index
 return {peek(addr),peek(addr+1)}

function set_note(sfx_index,time_index,note)
 local addr=0x3200+68*sfx_index+2*time_index

function get_speed(sfx_index)
 return peek(0x3200+68*sfx_index+65)

function set_speed(sfx_index,speed)

function get_loop_start(sfx_index)
 return peek(0x3200+68*sfx_index+66)

function get_loop_end(sfx_index)
 return peek(0x3200+68*sfx_index+67)

function set_loop(sfx_index,start,ending)
 local addr=0x3200+68*sfx_index
P#141623 2024-02-18 10:37

This is how you can set the global fx (noiz, buzz, detune, reverb and dampen).

--Values of fx: noiz(0,1),buzz(0,1),detune(0,1,2),reverb(0,1,2),dampen(0,1,2)
function make_globalfx(noiz,buzz,detune,reverb,dampen)
 local effect = 0
 effect |= noiz and 2 or 0
 effect |= buzz and 4 or 0
 effect += detune * 8
 effect += reverb * 24
 effect += dampen * 72
 return effect

function set_globalfx(sfx_index,effect)
 local addr=0x3200+68*sfx_index
P#141624 2024-02-18 11:12 ( Edited 2024-02-18 11:13)

[Please log in to post a comment]