numpy

Messing with sounds

I want to create a simple tool for generating sound, altered on user input. I figured out that I can generate wave with numpy, I can get user input with pygame and use pygame.sndarray and pygame.mixer to make the noise. Lets see part one, that

We are the noise generation!

I found a nice tutorial about generating sine wave with numpy, and I used that as a starting point. I defined a frequency and a sample rate, but instead of sine, I tried to make a simple saw, like this:
[0, 0.25, 0.5, 0.75, 1, 0, 0.25, 0.5, 0.75, 1, ...]

I used the linspace function from numpy to create one edge of the saw, which is as long as the frequencieth part of the sample:
fs = 44100
freq=440.
np.linspace(-1,1,fs/freq)

After reading the pygame documetation, I realized I should initialize the mixer module first, and check its properties:
import pygame
import pygame.mixer as mixer
import pygame.snarray as snd

mixer.init()
mixer.get_init()
# (22050, -16, 2)
So I needed to create an array with two coloumns. How?

First, I had to find a way to create two parallel saw edge. I achieved this with the tile and reshape funcion: a made two edges in line, thed cut it half.
import numpy as np
fs = mixer.get_init()[0]
chs = mixer.get_init()[2]
tst_sound = np.tile(np.linspace(-1,1,fs/freq), chs).reshape(fs/freq, chs)

I had already one short instance of sound, I only needed to repeat it freq times to make it last 1 sec. The problem is that the tile function repeats the number of coloumns, which is understandable, and made me found the transpose function. I swapped the rows and coloumns, repeated the new columns, and swapped them back.
tst_second = np.transpose(np.tile(np.transpose(tst_sound), freq))
tst_snd = snd.make_sound(tst_second)

If the shape of he sound is OK, the make_sound function should not return any problem. After I sorted out coloumn issue, it worked for me. Then I only needed to play it with the mixer module:
tst_snd.play()

Hell no! It's not the sound I want. It turns out, the mixer.get_init() returns three values with a purpose: it expects integers with the given numer of bits. So the type in which we want the array is:
new_type = "int"+str(abs(mixer.get_init()[1]))

To convert between types, there is the astype method:
tst_snd = tst_snd.astype(new_type)

Improvement

Getting more into documenttion, I found out that last argument of tile() accepted more than one dimension, so the code looks like this:
tst_sound = np.tile(np.linspace(-1.,1.,fs/frequency), (chs, 1))

Also, instead of transposing, I can use the axis argument of repeat(), to make it much more simple:
tst_second = np.repeat(tst_sound, frequency, 1)

We can pack all this into a function which takes the sample frequency and the number of channels directly from the mixer module:
def create_snd(frequency=440., duration=.3):
    # mixer init props:
    fs = mixer.get_init()[0]
    new_type = "int"+str(abs(mixer.get_init()[1]))
    chs = mixer.get_init()[2]
    # one wave
    tst_sound = np.tile(np.linspace(-1.,1.,fs/frequency), (chs, 1))
    # make in one second
    tst_second = np.repeat(tst_sound, frequency*duration, 1)
    # convert it to a sound and return it
    return(snd.make_sound(tst_second.astype(new_type)))

It cannot be played without a proper initializaton os pygame (I don't know why, probably reading the docs would help :D), so here are two short functions for playing a sine wave and manipulate its frequency:

def create_sin(frequency=440., duration=.3, amplitude=100):
    # mixer init props
    fs = mixer.get_init()[0]
    chs = mixer.get_init()[2]
    snd_type = "int"+str(abs(mixer.get_init()[1]))
    # time
    t = np.arange(0, duration, 1./fs)
    freq = np.array((frequency, frequency))
    sig = np.sin(2 * np.pi * freq * t.reshape(-1, 1))*amplitude
    return(snd.make_sound(sig.astype(snd_type)))
 
# program for changing notes
def int_snd_test():
    # program for changing notes
    #
    pg.init()
    pg.display.set_mode([50,50])
    mixer.pre_init(frequency=44100)
    mixer.init(frequency=44100, channels=1)
    #
    freq=440.
    done = False
    print("press w to increase, s to decrease frequency")
    #
    while not done:
        e = pg.event.wait()
        print("Waiting for user event")
        # create sound and play it:
        create_sin(frequency=freq, duration=1, amplitude=250).play()
        print("sound began to play...")
        # check user's events:
        if e.type == pg.KEYDOWN:
            if e.key == pg.K_ESCAPE:
                done = True
                pg.display.quit()
                break
            elif e.key == pg.K_w:
                freq += 100
                print("frequency up; ", freq)
            elif e.key == pg.K_s:
                freq -= 100
                print("frequency down;", freq)

However, some of the waves consist of two different sounds. Anyone has any idea why? I did not found in ot stack overflow.

Note that I added a duration argument to the function to be able make it shorter than one sec and amplitude variable becaouse the original was too silent.

Comments