Defy Audio Player
The Defy Audio Player plays Defy formatted audio files via PICO-8's PCM channel. It's a PCM boombox! You can also use it to create binary strings of audio data for playback in your own cartridges.
Convert almost any audio file format to .defy here: https://bikibird.itch.io/defy.
This cart is intended to run in PICO-8, not play on the BBS, which is limited to a maximum files size of 250K. Instead, open PICO-8 and enter load #defy
. Then enter run
.
Controls
- Drag and drop your .defy file onto PICO-8 to play it. You do not have to wait for a file to finish before loading another.
- Press 🅾️ (z key) to pause playback. Press it again to resume.
- Press ⬅️ or ➡️ to switch visualizers.
- Press ⬆️ to eject and stop playback.
- Press ⬇️ to display title and format.
- Press ❎ to record binary string. If you press record prior to playing the file, Defy will start recording it as soon as the file is dropped. The binary string is copied to your computer's clipboard.
Audio String Player
The library below plays Defy formatted audio strings. For support for @luchak's QPA formatted audio strings see https://www.lexaloffle.com/bbs/?tid=53933 and https://github.com/luchak/qpa-format/.
Warning: Be careful testing new audio strings. Playback with a mismatched playback mode sounds terrible. Do not test with headphones. Protect your hearing.
When record is pressed, the first 32,000 bytes of audio data are copied to the clipboard as a binary string during playback. Use the code below in your own carts to play the binary string. Be aware that only a few seconds of audio will fit in a cart. If the captured string contains too much data, you may truncate it. One second of audio equates to approximately 5500, 2250, 1840, 1125, and 690 characters for 8, 4, 2.6, 2, and 1 bit formats respectively.
- Paste the library below into a new cart.
- To save tokens, delete defy_play functions for any unused bit formats
do --defy audio string library by bikibird local buffer=0x8000 -- required all formats local clips={} -- required all formats local cued -- required all formats -- locals required for 4, 2.6, 2, and 1 bit formats below local step, new_sample, ad_index,c,direction local index_table = {[0]=-1,-1,-1,-1,2,4,6,8,-1,-1,-1,-1,2,4,6,8} local step_table ={7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, 73, 80, 88, 97, 107, 118,130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767} local adpcm=function(sample,bits) --adapted from http://www.cs.columbia.edu/~hgs/audio/dvi/IMA_ADPCM.pdf if bits >1 then local delta=0 local temp_step =step local sign = sample &(1<<(bits-1)) --hi bit of sample convert to 4 bit local magnitude =(sample & (sign-1))<<(4-bits) -- convert sample to 4 bit sign <<=(4-bits) -- convert sign to 4 bit 8==negative 0== positive local mask = 4 for i=1,3 do if (magnitude & mask >0) then delta+=temp_step end mask >>>= 1 temp_step >>>= 1 end if sign> 0 then -- negative magnitude if new_sample < -32768+delta then new_sample = -32768 else new_sample-=delta end else -- positive magnitude if new_sample >32767-delta then new_sample =32767 else new_sample +=delta end end ad_index += index_table[sign+magnitude] else --1-bit if sample==1 then -- negative if new_sample < -32768+step then new_sample = -32768 else new_sample-=step end else -- positive if new_sample >32767-step then new_sample =32767 else new_sample +=step end end if sample==direction then --if direction same, try larger step. if changed, try smaller step ad_index+=1 else ad_index-=1 direction =sample end end if ad_index < 1 then ad_index = 1 elseif (ad_index > #step_table) then ad_index = #step_table end step = step_table[ad_index] return new_sample\256+128 end defy_load=function(clip) -- required all formats add(clips,{clip=clip,start=1,endpoint=#clip, index=1, loop=false, done=false}) end local cued=false -- required all formats defy_cue=function(clip_number,start,endpoint,looping) --required all formats clips[clip_number].start=start or clips[clip_number].start clips[clip_number].index=clips[clip_number].start clips[clip_number].endpoint=endpoint or #clips[clip_number].clip clips[clip_number].loop=looping or false clips[clip_number].done=false step, new_sample, ad_index,delta,direction=7,0,0,0,0 cued=clip_number end defy_play= { [8]=function() -- 8 bit format if cued and not clips[cued].done then while stat(108)<1536 do for i=0,511 do poke (buffer+i,ord(clips[cued].clip,clips[cued].index)) clips[cued].index+=1 if (clips[cued].index>clips[cued].endpoint) then if (clips[cued].loop) then clips[cued].index=clips[cued].start else serial(0x808,buffer,i+1) clips[cued].done=true return end end end serial(0x808,buffer,512) end end end, [4]=function() -- 4 bit format if cued and not clips[cued].done then while stat(108)<1536 do for i=0,255 do c=ord(clips[cued].clip,clips[cued].index) poke (buffer+i*2,adpcm((c&0xf0)>>>4,4),adpcm(c&0x0f,4)) clips[cued].index+=1 if (clips[cued].index>clips[cued].endpoint) then if (clips[cued].loop) then clips[cued].index=clips[cued].start else serial(0x808,buffer,i+1) clips[cued].done=true return end end end serial(0x808,buffer,512) end end end, [3]=function() -- 3 bit format if cued and not clips[cued].done then while stat(108)<1536 do for i=0,170 do c=ord(clips[cued].clip,clips[cued].index) poke(buffer+i*3, adpcm((c>>>5)&0x07,3), adpcm((c>>>2)&0x07,3), adpcm(c&0x03,2,true)) clips[cued].index+=1 if (clips[cued].index>clips[cued].endpoint) then if (clips[cued].loop) then clips[cued].index=clips[cued].start else serial(0x808,buffer,i+1) clips[cued].done=true return end end end serial(0x808,buffer,510) end end end, [2]=function() -- 2 bit format if cued and not clips[cued].done then while stat(108)<1536 do for i=0,128 do c=ord(clips[cued].clip,clips[cued].index) poke(buffer+i*4, adpcm((c>>>6)&0x03,2), adpcm((c>>>4)&0x03,2), adpcm((c>>>2)&0x03,2), adpcm(c&0x03,2)) clips[cued].index+=1 if (clips[cued].index>clips[cued].endpoint) then if (clips[cued].loop) then clips[cued].index=clips[cued].start else serial(0x808,buffer,i+1) clips[cued].done=true return end end end serial(0x808,buffer,512) end end end, [1]=function() -- 1 bit format if cued and not clips[cued].done then while stat(108)<1536 do for i=0,64 do c=ord(clips[cued].clip,clips[cued].index) poke(buffer+i*8, adpcm((c>>>7)&1,1), adpcm((c>>>6)&1,1), adpcm((c>>>5)&1,1), adpcm((c>>>4)&1,1), adpcm((c>>>3)&1,1), adpcm((c>>>2)&1,1), adpcm((c>>>1)&1,1),adpcm(c&1,1)) clips[cued].index+=1 if (clips[cued].index>clips[cued].endpoint) then if (clips[cued].loop) then clips[cued].index=clips[cued].start else serial(0x808,buffer,i+1) clips[cued].done=true return end end end serial(0x808,buffer,512) end end end } function eject() -- required all formats clips[cued].done=true end end |
- Record some audio in the Defy Player. Once the binary string is created, playback will pause while the binary string is copied to the clipboard and then resume
- In
_init()
add one or more load statements to add your audio clips to the clip table. Turn on puny font by pressing ctrl-p before pasting your string. Failing to do so will corrupt the string and distort the audio.
function _init() defy_load"my audio string" --Turn on puny font (ctrl-p) before pasting!!! end |
- The
cue
function must be called before your audio string may be played. Cue your clip in the_update
function. Use whatever logic you would use when usingsfx()
. Cue may also be called with additional parameters:defy_cue(clip_number,start,endpoint,looping).
start
andendpoint
are indexes into the string.looping
is a boolean. - Add a playback mode in
_update
,defy_play[format]()
. Where format is 8, 4, 3, 2, or 1, corresponding to 8-bit, 4-bit, 2.6-bit, 2-bit, or 1-bit.
function _update() if (btnp(4)) then defy_cue(1) -- cues clip 1 for play from beginning to end, no looping. end defy_play[4]() -- play back of 4-bit audio string. called unconditionally in _update. end |
- Use
defy_eject(clip_number)
to end a clip early or stop a looping clip.
Acknowledgements
Thank you, @luchak and @packbat, for all your sound advice. Thank you, @luchak for the antialiasing filter. Thank you, @Gabe_8_bit for feedback and your fancy oscilloscope code. It formed the basis of the simpler oscilloscope that was ultimately included. Thank you, @LazarevGaming, et al, for testing and feedback.
Thanks again for making this happen! It's a really cool project.
@packbat, you're welcome. Looking forward to seeing what people do with audio strings in their own carts. Still can't believe I got down to 1-bit compression as an option and the quality is actually usable.
I need help. How do you import the PCM? I've been trying this whole time to add the PCM I made but with no results, Please help.
Not sure where you are getting hung up. Quick summary is:
- Go to https://bikibird.itch.io/defy and create a data file using the .defy format.
- Open PICO- 8 and type
load #defy
- Type run.
- Press the z key key to start recording.
- Drag and drop the .defy file on top of PICO-8 to copy your sample to the clipboard
- In your cartridge, press ctrl-p to enable puny font and add
defy_load"pasted your string here"
in the _init() function. - Add cue and play in the _update function as described above.
The number one problem most folks have is forgetting to turn on puny font before pasting the string.
Here's a game that uses the defy library. https://www.lexaloffle.com/bbs/?tid=47097 You may find their code helpful.
Let me know how it goes. Happy to look at your cartridge if needed.
Oh wow, this is amazing! Super cool project thanks for making/posting it.
...I took a break for a few months from Pico-8 after I jerry-rigged a .wav to Pico-8 DAC converter in a Google Colab. I came back to it today and you implemented this SO much better than I did?? Thanks SO much for making this tool.
@bikibird I used this in my game too!
https://www.lexaloffle.com/bbs/?tid=48795
i opened a defy file in notepad and it was (probably binary data that is translated into) chinese and korean characters and other stuff
Are you curious why?
Defy consumes string representations of raw bytestreams where each byte, represented by a single character in your picture, holds 1, 2, 2.5, 4, or 8 audio samples (not to be confused with a long series of these samples, which is also referred to as a "sample" by musicians), depending on if you use 8-bit, 4-bit, 3-bit, 2-bit, or 1-bit encoding. Notepad is probably interpreting the bytestream as UTF-8, while Pico-8 will show it as its own variant of ASCII, named P8SCII. Also see chr() and ord().
Here's a small cart to transform Defy files into ready-to-play binary strings. To use it, drop a .defy file onto the cart. It will put the audio data from the .defy file into the clipboard into a form that you can paste directly into a string in your PICO-8 code.
You can also press X to preview the file.
Limitations:
- Only 8-bit Defy and the QPA formats are supported.
- Samples more than about 5.9 seconds long may be truncated.
(Note: you can also use the main Defy cart for 8-bit Defy files, and I'll add proper QPA-to-binary-string support to that cart as well in the near future.)
Suddenly remembered this exists. would this work with picotron by chance? 😲 Because I could maybe use the speako8 to record sounds then maybe import them with this, maybe
EDIT!!!!
No wait this is PCM too. shakes fist at sky
@Nbrother1607,
defy_cue(clip_number,start,endpoint,looping)
. start
and endpoint
are indexes into the string. looping
is a boolean. Set it to true to loop.
@NazarFloppaLovesP8, see this cart from @luchak: https://www.lexaloffle.com/bbs/?tid=53933 for info on QPA.
[Please log in to post a comment]