Page 1 of 2

Setting large amounts of SoundData at a time

Posted: Sun Aug 18, 2024 2:13 am
by DTmg
Hello :)
I'm attempting to parse a file that contains raw sound data as part of it. I've managed to get individual samples out of it by creating a DataView from the ByteData object that hold all the sample data, and wanted to test to see if I was pulling the right section out by playing back one of the samples, however I can't figure out how to do that without manually setting each individual sample through a loop. Is there a way, either through some function I don't know about or ffi trickery with the data pointers to copy large chunks of data directly into a SoundData?

Re: Setting large amounts of SoundData at a time

Posted: Sun Aug 18, 2024 2:28 pm
by DTmg
I've figured out a roundabout solution, which is to take the chunk of SoundData and put a .wav file header in front of it, then put that in a FileData and put *that* into a SoundData, however for some reason the sample is not looping when I play it back with looping enabled. It plays back the audio just fine though, and I'm able to use it mostly like a normal audio source, but isPlaying() doesn't ever return false unless I stop it manually. This makes me worried that the header hasn't been set up correctly (I've been following this: http://soundfile.sapp.org/doc/WaveFormat/ to set it up), so ideally it would be great to just copy the sample data directly into a SoundData if that is possible. I don't have time right now, but later after I'm done moving I'll compare the generated wav "file" with an actual .wav file to see if maybe I'm missing some piece of data to make it loop properly.

Re: Setting large amounts of SoundData at a time

Posted: Sun Aug 18, 2024 7:01 pm
by RNavega
DTmg wrote: Sun Aug 18, 2024 2:13 am Is there a way, either through some function I don't know about or ffi trickery with the data pointers to copy large chunks of data directly into a SoundData?
Hi, I think it's something like this (untested):

Code: Select all

local ffi = require('ffi')

local all_samples = love.filesystem.read(path)
local all_samples_buf = love.data.newByteData(all_samples)

-- Create a blank SoundData based on settings you already know.
local my_sounddata = love.sound.newSoundData(TOTAL_SAMPLES, SAMPLE_RATE, 
                                             BIT_DEPTH, CHANNELS)

-- Zero-based index into the byte to start copying from.
local byte_offset = 3316

-- How many bytes per sample (assuming the platform byte is 8 bits long).
local bytes_per_sample = BIT_DEPTH / 8

local destination_ptr = my_sounddata:getFFIPointer()
local source_ptr = all_samples_buf:getFFIPointer()
ffi.copy(destination_ptr,
         source_ptr + byte_offset,
         TOTAL_SAMPLES * bytes_per_sample)
The API function is this:
https://luajit.org/ext_ffi_api.html#ffi_copy

Also note there's an extension included in LuaJIT, string.buffer, which is helpful when parsing files or buffer-like things. The functions buffer:set() and buffer:ref() are the most useful IMO.

Re: Setting large amounts of SoundData at a time

Posted: Mon Aug 19, 2024 10:24 pm
by DTmg
Attempting to add directly to a pointer like that gives me

Code: Select all

attempt to perform arithmetic on 'void *' and 'number'
but casting to a uint64_t and back to a void * worked, thanks!

Re: Setting large amounts of SoundData at a time

Posted: Tue Aug 20, 2024 12:16 am
by RNavega
Ah of course, thanks for the follow-up.

LuaJIT does support pointer arithmetic, so I think if you cast (or rather, construct) the void* into a byte pointer (uint8_t*) then it should work, without having to convert it back.

That is:

Code: Select all

local TYPEOF_UINT8_T = ffi.typeof("uint8_t *")

(...)

local source_ptr = all_samples_buf:getFFIPointer()
source_ptr = TYPEOF_UINT8_T(source_ptr)
...with the rest as in the original, including that add with "byte_offset".

Re: Setting large amounts of SoundData at a time

Posted: Tue Aug 20, 2024 6:19 am
by zorg
DTmg wrote: Sun Aug 18, 2024 2:13 am I've managed to get individual samples out of it by creating a DataView from the ByteData object that hold all the sample data, and wanted to test to see if I was pulling the right section out by playing back one of the samples, however I can't figure out how to do that without manually setting each individual sample through a loop.
I might be wrong about this, but can't you just create a SoundData object directly from the DataView? (FileData should work, not sure about ByteData nor DataView objects)
Of course, you'd still need to pass in some extra info (sampling rate, bit depth, channel count), but that's literally just a constructor in a one-liner.

Re: Setting large amounts of SoundData at a time

Posted: Thu Sep 05, 2024 6:19 am
by DTmg
I tried from just a DataView/ByteData and it wasn't working right, it seemed to only want FileData. In any case I did get it working!

Re: Setting large amounts of SoundData at a time

Posted: Fri Sep 06, 2024 8:23 pm
by SiENcE
I am currently writing an MOD and HSC player and therefore had to deal with a similar problem.

You can use setSample to insert generated raw data into SoundData.

Here is the code for continuous buffered playback. But it only has 1 channel.

Code: Select all

-- Create a QueueableSource with the desired sample rate, bit depth, and channel count
local queueSource = love.audio.newQueueableSource(44100, 16, 1)

-- TEST SOUND
function createSound(samplerate, frequency)
    local samples = {}
    local amplitude = 0.5
    for i = 1, samplerate do
        samples[i] = amplitude * math.sin((i / 44100) * frequency * 2 * math.pi)
    end
	return samples
end

function generateRandomSound()
    -- Generate random samples
	local samples = createSound(44100, 385)
    local audioData = love.sound.newSoundData(#samples, 44100, 16, 1)

    for i, v in ipairs(samples) do
        audioData:setSample(i - 1, v) -- i - 1 because SoundData is 0-indexed
    end

    -- Queue the generated audio data
    queueSource:queue(audioData)
end

function love.update(dt)
    -- Continuously generate and queue sound while maintaining playback
    if queueSource:getFreeBufferCount() > 0 then
        generateRandomSound()
    end

    -- Start playing if not already
    if not queueSource:isPlaying() then
        queueSource:play()
    end
end

function love.draw()
    love.graphics.print("Playing continuous random sound ...", 10, 10)
end
Now when you want to mix channels it works like this. I setup 9 channels.

Code: Select all

-- Create 9 QueueableSources with the desired sample rate, bit depth, and channel count
local channels = {}
local numChannels = 9
local sampleRate = 44100
local bitDepth = 16

for i = 1, numChannels do
    channels[i] = love.audio.newQueueableSource(sampleRate, bitDepth, 1)
end

-- Function to create a sine wave sound for a specific channel
function createSound(samplerate, frequency)
    local samples = {}
    local amplitude = 0.5
    for i = 1, samplerate do
        samples[i] = amplitude * math.sin((i / 44100) * frequency * 2 * math.pi)
    end
    return samples
end

-- Function to generate sounds for each channel and mix them together
function generateMixedSound()
    local mixedSamples = {}
    for i = 1, sampleRate do
        mixedSamples[i] = 0 -- Initialize the mixed sample to 0
    end

    -- Generate and mix sounds from each channel
    for ch = 1, numChannels do
        local frequency = love.math.random(1,200) + ch * 50 -- Vary the frequency for each channel
        local samples = createSound(sampleRate, frequency)

        for i = 1, #samples do
            mixedSamples[i] = mixedSamples[i] + (samples[i] / numChannels) -- Mix the sound, normalize by number of channels
        end
    end

    -- Create SoundData for the mixed samples
    local audioData = love.sound.newSoundData(#mixedSamples, sampleRate, bitDepth, 1)
    for i, v in ipairs(mixedSamples) do
        audioData:setSample(i - 1, v) -- SoundData is 0-indexed
    end

    -- Queue the mixed audio data in each channel
    for ch = 1, numChannels do
        if channels[ch]:getFreeBufferCount() > 0 then
            channels[ch]:queue(audioData)
        end
    end
end

function love.update(dt)
    -- Continuously generate and queue mixed sound while maintaining playback
    generateMixedSound()

    -- Start playing each channel if not already playing
    for ch = 1, numChannels do
        if not channels[ch]:isPlaying() then
            channels[ch]:play()
        end
    end
end

function love.draw()
    love.graphics.print("Playing mixed sound from 9 channels...", 10, 10)
end

Re: Setting large amounts of SoundData at a time

Posted: Sat Sep 07, 2024 1:46 pm
by zorg
That sine wave implementation will pop at the ends unless you taper it to zero, or keep a continuous phase value that doesn't reset at the end of each sounddata.

Re: Setting large amounts of SoundData at a time

Posted: Tue Sep 10, 2024 8:19 pm
by SiENcE
Ah thanks zorg! It was just a sample to show how continous sampling works.