gpetrowitsch
Posts: 4
Joined: Mon Apr 01, 2019 6:54 pm

Play tone without generating wave data beforehand

Mon Apr 01, 2019 7:07 pm

Hi all,

I kept searching the web and this forum for hours now, but found no satisfactory answer to this "problem".

In Python 3 I would like to call a function like

Code: Select all

someObject.playSine( frequency, duration)
or

Code: Select all

someObject.playRect(frequency,duration)
to play a tone with sine or rectangle waveform of the given frequency and period.

The reason why I want to have this, is that frequency and duration are calculated during run-time of my program and if I need to prepare an array first with all the waveform data, it often takes too long. There should be very little latency from calling the command until the tone is played.

Maybe I could use a GPIO and a timer and connect a piezo speaker, but I also want to play other audio via the audio jack. The I would have two speakers which seems a bit of an overkill to me.

On Windows there's the winsound module, which provides just that functionality, but I found nothing similar that would work on my Pi running Raspbian.

Thanks a lot for any help!
Regards, Gerhard

Andyroo

Re: Play tone without generating wave data beforehand

Mon Apr 01, 2019 9:53 pm

Depending on the number of sounds, could you make files of the correct frequency and then only play the specified duration?

The only other thing I could find (and I have no idea if it works on the Pi) is https://askubuntu.com/questions/202355/ ... hon#455825

User avatar
paddyg
Posts: 2555
Joined: Sat Jan 28, 2012 11:57 am
Location: UK

Re: Play tone without generating wave data beforehand

Mon Apr 01, 2019 9:57 pm

numpy could generate the array quickly, and output the result as raw bytes with very little overhead. I'm not sure of the internals of pygame mixer but I think it can create a Sound object from a bytes buffer. I will have a quick look at the docs for you.

this is some modified code I found on the pygame website which I have fixed for python3 and numpy I'm not really sure about the buffer format but I've assumed it uses uint32 and that seems to work!! I don't think I would have split the sample into onecycle then resized it - that looks pretty inefficient. Just do it all in one step. You could also do left and right tracks at the same time rather than the messy resize -> broadcast_to

Code: Select all

import pygame, time, pygame.sndarray
import numpy as np

SAMPLE_RATE = 44100
INT32_FACTOR = 1 << 31 - 1 # turn range to int32

def play_for(sample_array, ms, volLeft, volRight):
    sound = pygame.sndarray.make_sound(sample_array)
    beg = time.time()
    channel = sound.play(-1)
    channel.set_volume(volLeft,volRight)
    pygame.time.delay(ms)
    sound.stop()
    end = time.time()
    return beg, end
    
def sine_array_onecycle(hz, peak):
    length = SAMPLE_RATE / float(hz)
    omega = np.pi * 2 / length
    length = int(length)
    xvalues = np.arange(length) * omega
    mono = (peak * INT32_FACTOR * (1.0 + np.sin(xvalues))).astype(np.uint32)
    stereo =  np.broadcast_to(mono.reshape(length, 1), (length, 2))
    return stereo
    
def sine_array(hz, peak, n_samples = SAMPLE_RATE):
    return np.resize(sine_array_onecycle(hz, peak), (n_samples, 2))
    
pygame.mixer.pre_init(SAMPLE_RATE, -16, 2) # 44.1kHz, 16-bit signed, stereo
pygame.init()

f = sine_array(440.0, 1.0) # HZ, amplitude

play_for(f , 5000, 0.5, 0.5)
also https://groups.google.com/forum/?hl=en-GB&fromgroups=#!forum/pi3d

User avatar
paddyg
Posts: 2555
Joined: Sat Jan 28, 2012 11:57 am
Location: UK

Re: Play tone without generating wave data beforehand

Tue Apr 02, 2019 7:55 am

Duh - I've just noticed that there's a comment in the original code explaining that the mixer is initiated to use sixteen bit signed values :oops: !! So that uint32 should be int16 and the shuffling to keep positive isn't needed. I will repost a revised version here in a bit...

here

Code: Select all

import pygame, time, pygame.sndarray
import numpy as np

SAMPLE_RATE = 44100
INT16_FACTOR = 1 << 15 # turn -1 to +1 to full range int16

def play_for(sample_array, ms, volLeft, volRight):
    sound = pygame.sndarray.make_sound(sample_array)
    beg = time.time()
    channel = sound.play(-1)
    channel.set_volume(volLeft,volRight)
    pygame.time.delay(ms)
    sound.stop()
    end = time.time()
    return beg, end
    
def sine_array(hz, peak, n_samples=SAMPLE_RATE):
    cycle_length = SAMPLE_RATE / float(hz) # number (float) of samples in one cycle
    omega = np.pi * 2 / cycle_length # diff in radians between samples
    # n_samples needs to be as close as possible to an integer multiple of cycle_length to avoid 'jumps'
    n_samples = int(int(n_samples / cycle_length) * cycle_length)
    if n_samples == 0:
        n_samples = int(cycle_length)
    xvalues = np.arange(n_samples) * omega
    mono = (peak * INT16_FACTOR * np.sin(xvalues)).astype(np.int16)
    stereo =  np.broadcast_to(mono.reshape(n_samples, 1), (n_samples, 2))
    return stereo.copy() # broadcast_to produces a view but we need a proper array
    
pygame.mixer.pre_init(SAMPLE_RATE, -16, 2) # 44.1kHz, 16-bit signed, stereo
pygame.init()

for tone in (440.0, 523.3, 392.0, 415.3, 554.4, 493.9, 440.0):
    f = sine_array(tone, 1.0) # HZ, amplitude, integer number of samples
    play_for(f , 500, 0.5, 0.5)
also https://groups.google.com/forum/?hl=en-GB&fromgroups=#!forum/pi3d

PiGraham
Posts: 3971
Joined: Fri Jun 07, 2013 12:37 pm
Location: Waterlooville

Re: Play tone without generating wave data beforehand

Tue Apr 02, 2019 8:29 am

You could use pygame.midi and a soft synth like timidity (included in Rasbian)

gpetrowitsch
Posts: 4
Joined: Mon Apr 01, 2019 6:54 pm

Re: Play tone without generating wave data beforehand

Tue Apr 02, 2019 10:19 am

Thanks Andyroo, but I have no pre-defined sounds - could be anything.

gpetrowitsch
Posts: 4
Joined: Mon Apr 01, 2019 6:54 pm

Re: Play tone without generating wave data beforehand

Tue Apr 02, 2019 10:21 am

Thanks a lot, PiGraham! This could be the solution to my problem. I'll give it a try.
Do you know, if there's any problem in mixing tkinter and pygame (because I'm using tkinter for my appllication).

gpetrowitsch
Posts: 4
Joined: Mon Apr 01, 2019 6:54 pm

Re: Play tone without generating wave data beforehand

Tue Apr 02, 2019 10:25 am

Thanks paddyg for the huge effort. As PiGrahams solution looks more promising to me, I'll try that first. Maybe it even allows for switching on the tone without having to specify a duration in advance, but just switch it off any time later, which would be the ideal solution for me.

Return to “Python”