Log In  


Now in stereo!

Cart #lazy_procedural_acid_picotron-3 | 2024-07-25 | Embed ▽ | License: CC4-BY-NC-SA
14

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
14


1

Spectacular 💯



[Please log in to post a comment]