Best way to store and reproduce a sample array

In the project I’m working on currently, I’d like a vinyl crackle sound and thought that storing this as a sampled array of floats (about 12 seconds @ 48kHz or so) would be the best way to be able to recall this sound whenever needed.
My question is what would be the best/easiest way to get a sound into an array of the samples? Maybe this would be called a wavetable?

I originally tried the Sample Data Export in Audacity, which gave me a .txt document of the samples (16bit @ 48kHz), as an example, the first 6 samples:
0.00323 0.00281 0.00104 -0.00082 -0.00140 -0.00079.
I then turned the data from ^that doc into a float array in a separate header file that I included in the project. I was then able to recall elements of that array but it was not a recreation of the original sound (kind of a white noise).
Something like this method would work best for me, somehow being able to spit out the sampled values into a text file.
I’ve also attempted to simply print the samples directly from the Daisy so I could copy/paste them into a document, but wasn’t able to get either the Arduino Serial or the VS Code Serial Port Helper extension working (both of these issues have been present for a while even though I was able to before).

(Theoretically, I should be able to sample the values to an array on the QSPI section of memory, but I’d rather not have to do this every time I want to build this project on a different unit. I’d like it to be something in a header file that I can just include in the project and any other projects I’d like to use this sound on in the future.)

TLDR- How do I get the sampled values of a file or the Daisy’s output floats into a text file?

Hi! Just my 5 cents.

Seems lika a good method. Are you using the right Measurement scale? Stereo/mono? No mistake in how you convert it into the array and play it back?

That is how I would do it, sample using the Daisy and then output the values using the Logger() class, and capture the output. Then convert into code.

I am on Linux and program through the CLI and the Logger() method has worked great for other applications for me.

I may be missing something but my settings for the Data Export are- Measurement scale: Linear, Channel layout for stereo: L Channel First (but I’m only exporting mono files). I don’t believe there is a mistake in the conversion. I have a python script just inserting commas after every float (I double check by making sure the values of random lines throughout match) and then I copy/paste that to a header file that looks something like this.

// vinyl_crackle_array.h
namespace VinylCrackle
{
    float DSY_SDRAM_BSS crackle_samples[576000]  =
        { 
            // sample data parsed with commas
        };
}

Maybe I’ll give the Logger() another go but I haven’t been able to see the output of that or open the Arduino Serial monitor when connected to a daisy for a while now. (I believe there’s a bug with the Serial Port View extension in VS Code and can’t even get a list of my connections currently).

Sounds like you do things the right way, @three. And the rest of the code, for playback, is working?

I did a sampler for my OscPocketD/Base project (OscPocketD/Base 1+2/MIDI - fx unit, synth, sampler, modulator), where I started by filling the sample buffer with a triangle wave:

void fillBuffer()
{
	Oscillator osc;

    osc.Init(sysSampleRate);
    osc.SetWaveform(osc.WAVE_TRI);
	osc.SetFreq(440.0f);
	osc.SetAmp(0.1f);

	for (uint32_t i = 0; i < BUFFER_MAX; i++)
	{
		sBufferR[i] = osc.Process();
		sBufferL[i] = sBufferR[i];	
	}
}

Just for checking the playback.

Does the playback work ok with something like this? Sorry if I am rambling, just trying to help and I don’t have your complete code! :slight_smile:

@three: I’m getting the exact same problem with Serial Port View. I’ve just posted a question on this topic in Daisy’s forum yesterday. I just thought that I’m the only one with this problem.

Vinyl crackle sounds are more like sparks or impulses on wave data. Normally it is a spike up and a spike down. If you’re getting noise (white noise) most probable cause is quantization, that you get when print some data with less accuracy than the measurement.

Yes, I have playback working other than this. I can playback a loop, the dry signal, or an oscillator, like your code. I appreciate the help!
It may help if I post the relevant code:

#include vinyl_crackle.h

...

for (size_t i = 0; i < size; i++)
{
    ...

    static size_t v = 0;
    out[1][i] = VinylCrackle::crackle_samples[v];
    v++;
    if (v >= 576000) vi = 0;
    // 576000: 12 secs of floats @ 48kHz
}

This is what I was using to test the output of the vinyl_crackle array.

It’s “good” to know that I’m not the only one having this issue. I’ll watch that post for any solutions.

What exactly do you mean by “sparks or impulses”? It sounds like I could maybe just artificially generate this type of sound with a function. Could I just generate arbitrary, momentary additions/subtractions to the sampled values I’m processing?
Would you be able to explain what those sparks/impulses would look like on data?

I think you may be right about the quantization, that seems like a probable cause given what I know.

Yes, I think you can artificially generate the crackles. Some years ago I was trying to manually remove crackles of a digital record from an old vinyl and noticed that they appear on wave as a very fast pulse going from zero to a maximum then inverting down to a negative minimum, and then returning to zero. Maybe something like c exp(-a t^2)*tanh(b t) could do the job, with a, b and c constants and t the time. I think one has to adjust both the amplitude c and the number (or frequency) of crackles to be inserted in the wave. The amplitude should be equal to the inverse of frequency: the higher the number of the crackles per unit of time, the smaller their amplitude.

1 Like

Thank you! I don’t yet quite understand the details of the math here, but I get the idea and this gives me a point where I can start googling.

@three, sorry for late reply. I will try to get some time to work on an example of my own and get back to you.

Thank you! Let me know what you come up with.

1 Like

After trying out code similar to yours with data exported from Audacity, I only got noise too.

Then I remembered something.

You cannot init an array that is placed in DSY_SDRAM_BSS at compile time. If your sample is small enough, and you remove the “DSY_SDRAM_BSS”, it works for me. So, the export from Audacity is ok and compatible.

So, your original question remains, with larger samples, how to do it? I have to think about that one. You are using the basic Seed, right? I think the solution lies in using an SD card.

Ok that explains a lot. Would you be able to explain why this is? I assume this would be the same for the QSPI section of memory.

Yes, I’m using the Seed. I’m leaning towards generating vinyl crackle-type noise if I’m able to figure out how to do so.

I think it is because this is external RAM and it can’t be directly loaded in the same way as you load a firmware into RAM.

Here is an article on how to generate these kinds of sounds:

https://www.researchgate.net/publication/287114601_Digital_audio_antiquing_-_Signal_processing_methods_for_imitating_the_sound_quality_of_historical_recordings

1 Like

From that article, I’ve started creating a class to generate clicks. The main issue I’m running into is trying to use a gamma_distribution object to randomize the gap between clicks, however the parameters of the object can only be set on construction, so the gap can never be changed. I’m trying to come up with some way to work around this because the gap variation produced by the gamma object is less “artificial” sounding than randomized gaps using rand()
_randomized_gap = rand() % (gap_duration * 2) + (gap_duration / 4);

Theoretically, click_duration can be extended to something like 30 to create some lower frequency disturbances (more like “thumps” than “clicks”) but this is just about right for my purposes.

#include <random>

class VinylClick
{
private:
    uint16_t click_duration; 
    uint16_t gap_duration;
    std::default_random_engine rand_eng;
    std::weibull_distribution<double> weibull;
    // std::gamma_distribution<double> gamma;
    
public:
    VinylClick()
      : click_duration(10),
        gap_duration(14000), // 14000
        weibull(1, click_duration)
        // gamma(1, gap_duration)
    {}

    void Process(float &sample)
    {
        static uint16_t _randomized_duration = click_duration;
        static uint16_t _randomized_gap = gap_duration;
        static bool positive_amplitude = true;
        static uint32_t sample_clock = 0;
        static bool produce_click = false;
        static float click_operation = 1.f / _randomized_duration;
        static float click = sample;
        static bool reverse_direction = false;

        if (produce_click)
        {
            if (positive_amplitude)
            {
                if (click >= 1.f) reverse_direction = true;

                if (reverse_direction)
                {
                    if (click <= 0.f)
                    {
                        sample = click = 0.f;
                        produce_click = false;
                        reverse_direction = false;
                    }
                    else
                    {
                        click -= click_operation;
                        sample = click;
                    }
                }
                else
                {
                    click += click_operation;
                    sample = click;
                }
                
            }
            else
            {
                if (click <= -1.f) reverse_direction = true;
                
                if (reverse_direction)
                {
                    if (click >= 0.f)
                    {
                        sample = click = 0.f;
                        produce_click = false;
                        reverse_direction = false;
                    }
                    else
                    {
                        click += click_operation;
                        sample = click;
                    }
                }
                else
                {
                    click -= click_operation;
                    sample = click;
                }
            }
        }
        else
        {
            if (sample_clock++ > _randomized_gap)
            {
                // generate values for new click
                _randomized_duration = int(weibull(rand_eng)) + 1;
                click_operation = 1.f / _randomized_duration;

                // _randomized_gap = int(gamma(rand_eng));
                _randomized_gap = rand() % (gap_duration * 2) + (gap_duration / 4);

                positive_amplitude = rand() % 2;

                produce_click = true;
                reverse_direction = false;
                sample_clock = 0;
                click = sample;
            }
        }
    }

    // returns current average gap between clicks
    inline uint16_t getGap() { return gap_duration; }

    // set gap between clicks, ranging from 4000 to 40000
    void setGap(uint16_t gap) 
    {
        if (gap >= 4000 && gap <= 40000)
        {
            gap_duration = gap;
            // gamma.beta = gap_duration ???
        }
    }
};