Pysound sound parameters

Martin McBride
2021-09-15

In Pysound, a sound buffer is simply a NumPy array that contains the sampled values of a sound. It is always a 1-dimensional array of float values.

For example, if you use the sine_wave function to create a sine wave signal, the result will be stored as a NumPy array of values that vary as a sine wave.

In this article we will look:

  • How the sound buffer's sample rate is stored.
  • How we can use sound buffers as inputs to create time varying sounds.
  • How to apply mathematical operators directly to sound buffers.

Sample rate

A sound buffer just contains a list of values. In order to interpret those values as a sound, we need to know the sample rate of the sound. For example, if we have an array of 10000 values, this could represent:

  • 1 second of sound with a sample rate of 10000 samples per second.
  • 0.5 seconds of sound with a sample rate of 20000 samples per second.
  • And so on.

Pysound stores the sample rate separately, in a BufferParams object.

This was a design decision taken because it is far easier to work with simple NumPy arrays in the code, which makes it easier to experiment with new code. It also allows us to apply NumPy mathematical operators to manipulate sounds, which is very useful.

In most cases, if you are working on a sound design, every buffer will have the same length and sample rate, so you will only need one BufferParams object. If you are working with different sample rates, you need to take care to use the correct parameters. This will be covered in later examples.

Sound buffers as inputs

Here is a simple example of creating a sine wave with a fixed frequency of 400Hz:

params = BufferParams()
out = sine_wave(params, frequency=400)
soundfile.save(params, '/tmp/sine-400.wav', out)

The default BufferParams has a length of 1 second and a sample rate of 44100, so this will create a 1 second sound file.

Here is an example of creating a sine wave with a frequency that changes from 200 Hz to 600 Hz over time:

params = BufferParams()
freq = linseg(params, start=200, end=600)
out = sine_wave(params, frequency=freq)
ssoundfile.save(params, '/tmp/sine-40.wav', out)

In this case, we use the linseg function (from the envelopes module) to create a sound buffer freq whose value increases from 200 to 600 over the course of 1 second.

We feed that value in as the frequency parameter of the sine_wave function. This means that the frequency changes over time.

Many of the sound component inputs can accept either a number or a sound buffer. This allows you to easily create either a fixed value or a time varying value for that input.

The full code for this example is available on github.

Sound buffer compatibility

When you pass a sound buffer as a parameter, it should have the same sample rate as the component it is being passed into. So in the previous case, freq must have the same sample rate as out. If not, strange effect will occur (the imput sound will be treated as if it had the same sample rate, so it will be slowed down or speeded up).

The input buffer does not need to be the same length as the as the output signal:

  • If the input buffer is too long, it will be truncated to the correct length.
  • If it is too short, it will be padded with zeros.

In the previous example, freq is created using the same BufferParams object that we use to create the main sound out. This means that they will both have the same length and sample rate, so they are definitely compatible.

Performing maths operations on sound buffers

Sound buffers are just NumPy arrays, so you can use NumPy vectorised operators on them.

For example, if a is a sound buffer, you can do things like this:

p = a*2

This multiplies evey element in buffer a by 2, and stores the result in a new array. This means that p will be a copy of a, but every element will be multiplied by 2. Effectively, you can change the amplitude of a buffer like this. NumPy provides all the standard maths operators.

As well as being neat, this is also very efficient. The code that loops over the array performing the multiplications is part of the NumPy internals, written in optimised C code.

You can also combine buffers:

q = (a + b)/2

This code takes the average of each corresponding element in a and b, and stores the result in a new sound buffer in q. It effectively mixes signals a and b, in equal parts, to create a news signal q.

For this to work correctly, a and b must be the same length and sample rate.

NumPy has a wealth of other features that can be applied in this way. In fact, almost all of the features in Pysound are implemented using NumPy. It is worth learning a bit about NumPy. Looking at the Pysound source code should highlight a areas of interest.

Popular tags

ebooks fractal generative art generativepy generativepy tutorials github koch curve l systems mandelbrot open source productivity pysound python recursion scipy sine sound spriograph tinkerbell turtle writing