Now in stereo!
Just a short little proof-of-concept for streaming PCM output in Picotron. Could stand to be streamlined and cleaned up a fair bit (and commented better!), but hopefully this gets the idea across. The idea is to set up a few different sfx instruments playing different wavetables on adjacent rows of a pattern, use stat()
calls to track the currently playing row, and update non-playing rows with new samples. There are a few gotchas to work around - setting up the instruments is a little fiddly, and Picotron crossfades instruments between rows so some samples need to be written to multiple instruments.
Use the left and right arrows to change the audio buffer size. Values >=8 seem to work well on web, and it looks like I can go as low as 3 on desktop Picotron on my machine.
IF YOU WANT TO USE PCM IN YOUR OWN CODE, here is a version of pcm.lua
you can use (tested as of v0.1.1c, if it's not working for you please let me know):
-- Picotron PCM driver -- -- -- 8-bit 44.1 kHz stereo PCM for Picotron -- -- Entrypoint is pcm_init(), call it in update to set up. -- -- You can pass {n_bufs = 4} to pcm_init for lower latency, but this will probably result in audio breakups in the web -- player. -- -- Uses these resources: -- * memory from 0xe00000-0xe08000 (or more, if you want more than 8 buffers) -- * instruments 56-63 (or more instruments, going lower as you add more buffers) -- * sfx 63 -- * output channel 15 -- -- -- EXAMPLE -- ======= --[[ include 'pcm.lua' function gen_sine(n_samples) local sound_left = userdata('f64', n_samples) local sound_right = userdata('f64', n_samples) for i = 0, n_samples - 1 do sound_left[i] = sin(sine_phase_left) sound_right[i] = sin(sine_phase_right) sine_phase_left += 0.01 sine_phase_right += 0.015 end -- output range is -128 to 127 return sound_left * 32, sound_right * 32 end function _init() sine_phase_left = 0 sine_phase_right = 0 pcm_callback, pcm_start, pcm_stop = pcm_init() pcm_start() end function _update() -- call pcm_callback() every frame until you call pcm_stop() pcm_callback(gen_sine) end --]] -- -- LICENSE -- ======= -- -- Copyright (C) 2024 by luchak <[email protected]> -- -- Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby -- granted. -- -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL -- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN -- AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -- PERFORMANCE OF THIS SOFTWARE. function pcm_init(options) options = options or {} local n_bufs = options.n_bufs or 8 local base_addr = options.base_addr or 0xe00000 local sfx_id = 63 local channel_id = 15 local sfx_spd = 2 local buf_size = 10 local buf_len = 2<<buf_size local first_instrument=64-n_bufs local play_queue={} local buf_addrs_left={} local buf_addrs_right={} for i=0,n_bufs-1 do local buf_addr_left = base_addr + 2*i*buf_len local buf_addr_right = buf_addr_left + buf_len add(buf_addrs_left, buf_addr_left) add(buf_addrs_right, buf_addr_right) -- fill output buffer with enough 0s to avoid startup glitches add(play_queue, userdata('f64',buf_len)) add(play_queue, userdata('f64',buf_len)) _pcm_make_stream_instrument(first_instrument+i, buf_addr_left, buf_addr_right, buf_size) end _pcm_setup_sfx(sfx_id, sfx_spd, first_instrument, n_bufs) local next_row=0 local function pcm_callback(gen_chunk) local row = stat(400+channel_id, 9) local tick_len = stat(400+channel_id, 21) if #play_queue<n_bufs*2 then local chunk_left, chunk_right = gen_chunk(sfx_spd*tick_len) add(play_queue, chunk_left) add(play_queue, chunk_right or chunk_left) end while next_row ~= row and #play_queue>0 do local chunk_left = deli(play_queue, 1) local chunk_right = deli(play_queue, 1) local prev_idx = (next_row + n_bufs - 1) % n_bufs + 1 local this_idx = next_row + 1 local prev_base_left = buf_addrs_left[prev_idx] local prev_base_right = buf_addrs_right[prev_idx] local base_left = buf_addrs_left[this_idx] local base_right = buf_addrs_right[this_idx] local chunk_size = #chunk_left poke(prev_base_left + chunk_size, chunk_left:get(0, chunk_size)) poke(prev_base_right + chunk_size, chunk_right:get(0, chunk_size)) poke(base_left, chunk_left:get(0, chunk_size)) poke(base_right, chunk_right:get(0, chunk_size)) next_row = (next_row + 1) % n_bufs end end local function pcm_start() sfx(sfx_id, channel_id) end local function pcm_stop(channel) sfx(-1, channel_id) end return pcm_callback, pcm_start, pcm_stop end function _pcm_make_stream_instrument(instrument_id, wave_addr_left, wave_addr_right, wave_size) -- instr comments here mostly copied from sfx.p64/data.lua poke(0x40000 + (instrument_id * 512) + 0x1df, 2) -- wide -- node 0: root poke(0x40000 + (instrument_id * 512) + (0 * 32), -- instrument 63, node 0 0, -- parent (0x7) op (0xf0) 1, -- kind (0x0f): 1 root kind_p (0xf0): 0 -- wavetable_index 0, -- flags 0, -- unused extra -- MVALs: kind/flags, val0, val1, envelope_index 0x0,0x40,0,0, -- volume: mult. 0x40 is max (-0x40 to invert, 0x7f to overamp) 0x1,0,0,0, -- pan: add. center 0x1,0,0,0, -- tune: +0 -- 0,48,0,0 absolute for middle c (c4) 261.6 Hz 0x1,0,0,0, -- bend: none -- following shouldn't be in root 0x0,0,0,0, -- wave: use wave 0 0x0,0,0,0 -- phase ) -- node 1: custom wt (left) poke(0x40000 + (instrument_id * 512) + (1 * 32), -- instrument 63, node 1 0, -- parent (0x7) op (0xf0) 0x20|2, -- kind (0x0f): 2 osc kind_p (0xf0): 2 -- wavetable_index 0x0, -- flags 0, -- unused extra -- MVALs: kind/flags, val0, val1, envelope_index 0x0,0x40,0,0, -- volume: mult. 0x40 is max (-0x40 to invert, 0x7f to overamp) 0x0,-128,0,0, -- pan: left 0x0,77,0,0x20|0x80, -- tune: +0 -- 0,5,0,0 absolute for f0 -- tune is quantized to semitones with 0x20 0x0,0,0,0x20|0x80, -- can fine-tune here but unsure of exact setting 0x0,0,0,0, -- wave: 0 0x0,-128,0,0 -- phase ) -- node 2: custom wt (right) poke(0x40000 + (instrument_id * 512) + (2 * 32), -- instrument 63, node 2 0, -- parent (0x7) op (0xf0) 0x30|2, -- kind (0x0f): 2 osc kind_p (0xf0): 3 -- wavetable_index 0x0, -- flags 0, -- unused extra -- MVALs: kind/flags, val0, val1, envelope_index 0x0,0x40,0,0, -- volume: mult. 0x40 is max (-0x40 to invert, 0x7f to overamp) 0x0,127,0,0, -- pan: right 0x0,77,0,0x20|0x80, -- tune: +0 -- 0,5,0,0 absolute for f0 -- tune is quantized to semitones with 0x20 0x0,0,0,0x20|0x80, -- can fine-tune here but unsure of exact setting 0x0,0,0,0, -- wave: 0 0x0,-128,0,0 -- phase ) -- wavetable 2 (node 1) poke(0x40000 + 0x200*instrument_id + 0x1e0 + 4*2, (wave_addr_left >> 8) & 255, -- address (low) in 256 byte increments (wave_addr_left >> 16) & 255, -- address (high) in 64k increments wave_size, -- samples (1 << n) 0x1 -- wt_height 256(0); wave mval points at one of the entries ) -- wavetable 3 (node 2) poke(0x40000 + 0x200*instrument_id + 0x1e0 + 4*3, (wave_addr_right >> 8) & 255, -- address (low) in 256 byte increments (wave_addr_right >> 16) & 255, -- address (high) in 64k increments wave_size, -- samples (1 << n) 0x1 -- wt_height 256(0); wave mval points at one of the entries ) memset(wave_addr_left, 2 << wave_size, 0) memset(wave_addr_right, 2 << wave_size, 0) end function _pcm_setup_sfx(sfx_id, sfx_spd, first_instrument, n_bufs) local base = 0x50000 + 328*sfx_id poke2(base, 64) -- track length poke(base + 2, sfx_spd) -- spd poke(base + 3, 0) -- loop to 0 poke(base + 4, n_bufs) -- loop len n_bufs poke(base + 5, 0) poke(base + 6, 0) for i=0,n_bufs-1 do poke(base + 8 + i,0) -- note poke(base + 8 + 64 + i, first_instrument + i) -- instrument poke(base + 8 + 128 + i, 64) -- volume end end |
[Please log in to post a comment]