1
votes

I asked a similar question earlier, but I made the question more complex than it had to be. I am generating a 100 hz sine wave, that I then playback using simpleaudio.
Note: I had this problem when I encoded the wave to a .wav file. Sounded exactly the same as with simple audio. Also changing channels from 2 to 1 changes the sound, but does not fix this problem.

To install simple audio:

sudo apt-get install -y python3-dev libasound2-dev
python -m pip install simpleaudio

Stand alone code:

import numpy as np
import simpleaudio as sa
import matplotlib.pyplot as plt

def generate_sine_tone(numsamples, sample_time, frequency):
    t = np.arange(numsamples) * sample_time # Time vector
    signal = 8388605*np.sin(2*np.pi * frequency*t)
    return signal

if __name__ == "__main__":
    duration = 1
    samprate = 44100 # Sampling rate
    numsamples = samprate*duration# Sample count
    st = 1.0 / samprate # Sample time
    t = np.arange(numsamples) * st # Time vecto

    nchannels = 2
    sampwidth = 3

    signal = generate_sine_tone(numsamples, st, 100)
    signal2 = np.asarray([ int(x) for x in signal ])

    play_obj = sa.play_buffer(signal2, nchannels, sampwidth, samprate)
    print(signal2)
    plt.figure(0)
    plt.plot(signal2)
    plt.show()

Running this in the command line will produce a graph of the sine wave for 1 second, or 44100, samples, which is 100 periods of the sine wave. It will also play the sound into your speakers, so turn your system sound down a good bit before running.

enter image description here

My other posts on this issue: Trying to generate a sine wave '.wav' file in Python. Comes out as a square wave
https://music.stackexchange.com/questions/110688/generated-sine-wave-in-python-comes-out-buzzy-or-square-ey

expected sound: https://www.youtube.com/watch?v=eDk1bOX-P3w&t=4s
received sound (approx): https://www.youtube.com/watch?v=F7DnVBJ9R34

This problem is annoying me sooo much, I would greatly appreciate any help that can be provided.

2

2 Answers

2
votes

There are two problems here.

The lesser one is that you are creating a single array and playing it back as if it were stereo. You need to set nchannels = 1 (or duplicate all the values by creating an array with two columns).

The other problem is trying to create 24-bit samples. Very few people have good enough equipment and good enough ears to tell the difference between 24-bit and 16-bit audio. Using a sample width of 2 makes things much easier. You can generate 24-bit samples if you wish and normalize them to 16-bit for playback: signal *= 32767 / np.max(np.abs(signal))

This code works

import numpy as np
import simpleaudio as sa

def generate_sine_tone(numsamples, sample_time, frequency):
    t = np.arange(numsamples) * sample_time # Time vector
    signal = 32767*np.sin(2*np.pi * frequency*t)
    return signal

duration = 1
samprate = 44100 # Sampling rate  
numsamples = samprate*duration# Sample count
st = 1.0 / samprate # Sample time

nchannels = 1
sampwidth = 2

signal = generate_sine_tone(numsamples, st, 100)
signal2 = signal.astype(np.int16)
#signal2 = np.asarray([ int(x) for x in signal ])

play_obj = sa.play_buffer(signal2, nchannels, sampwidth, samprate)
play_obj.wait_done()
2
votes

The simpleaudio.play_buffer() function does not convert your data. It only takes the exact memory buffer (i.e. the buffer it gets from the object you gave) and interprets it as what you claim it to contain. In your program your description of what the buffer contains (2 * 3 byte items) is not what it actually contains (1 * 8 byte items). Unfortunately in your example program this does not result in an error, because the size of the buffer you gave it coincidentally happens to be an exact multiple of 6, the size in bytes you claim your memory buffer's items to have. If you try it with one more sample, numsamples = 44101, you will get an error, because 44101 * 8 is not divisible by 6:

ValueError: Buffer size (in bytes) is not a multiple of bytes-per-sample and the number of channels.

Try what print(signal2.itemsize) shows. It's not the 3 * 2 that you claim it to be in your call to simpleaudio.play_buffer(). If the following is still correct, there's no way to get 24 bit buffers from Numpy even if you tried to: NumPy: 3-byte, 6-byte types (aka uint24, uint48)

And perhaps that's why the tutorial tells you to just use 16-bit data type for Numpy buffers, see https://github.com/hamiltron/py-simple-audio/blob/master/docs/tutorial.rst

Numpy arrays can be used to store audio but there are a few crucial requirements. If they are to store stereo audio, the array must have two columns since each column contains one channel of audio data. They must also have a signed 16-bit integer dtype and the sample amplitude values must consequently fall in the range of -32768 to 32767.

What are these "buffers"? They are a way for Python objects to pass low-level raw byte data between each other and libraries written in e.g. C. See this: https://docs.python.org/3/c-api/buffer.html or this: https://jakevdp.github.io/blog/2014/05/05/introduction-to-the-python-buffer-protocol/

If you want to create 24 bit buffers from your audio data, then you'll have to use some other library or low-level byte-by-byte hacking for creating the memory buffer, because Numpy won't do it for you. But you might be able to use dtype=numpy.float32 to get 32-bit floats that have 4-byte samples per channel. Simpleaudio detects this from the sample size, for example for Alsa:

https://github.com/hamiltron/py-simple-audio/blob/master/c_src/simpleaudio_alsa.c

    /* set that format appropriately */
    if (bytes_per_chan == 1) {
        sample_format = SND_PCM_FORMAT_U8;
    } else if (bytes_per_chan == 2) {
        sample_format = SND_PCM_FORMAT_S16_LE;
    } else if (bytes_per_chan == 3) {
        sample_format = SND_PCM_FORMAT_S24_3LE;
    } else if (bytes_per_chan == 4) {
        sample_format = SND_PCM_FORMAT_FLOAT_LE;
    } else {
        ALSA_EXCEPTION("Unsupported Sample Format.", 0, "", err_msg_buf);
        return NULL;
    }

That's a little bit like using the weight of a vehicle for determining if it's a car, a motorcycle or a bicycle. It works, but it might feel odd to only be asked about the weight of a vehicle and not at all about its type.

So. To fix your program, use the dtype parameter of asarray() to convert your data to the buffer format you want, and declare the correct format in play_buffer(). And perhaps remove the scaling factor 8388605 from the sine generation, replace it with whatever you actually want and place it somewhere near the format specification.