52

I need to generate a sine wave sound in Python, and I need to be able to control frequency, duration, and relative volume. By 'generate' I mean that I want it to play though the speakers immediately, not save to a file.

What is the easiest way to do this?

Wilduck
  • 13,822
  • 10
  • 58
  • 90
astrofrog
  • 32,883
  • 32
  • 90
  • 131

6 Answers6

72

Version with numpy:

import time

import numpy as np
import pyaudio

p = pyaudio.PyAudio()

volume = 0.5  # range [0.0, 1.0]
fs = 44100  # sampling rate, Hz, must be integer
duration = 5.0  # in seconds, may be float
f = 440.0  # sine frequency, Hz, may be float

# generate samples, note conversion to float32 array
samples = (np.sin(2 * np.pi * np.arange(fs * duration) * f / fs)).astype(np.float32)

# per @yahweh comment explicitly convert to bytes sequence
output_bytes = (volume * samples).tobytes()

# for paFloat32 sample values must be in range [-1.0, 1.0]
stream = p.open(format=pyaudio.paFloat32,
                channels=1,
                rate=fs,
                output=True)

# play. May repeat with different volume values (if done interactively)
start_time = time.time()
stream.write(output_bytes)
print("Played sound for {:.2f} seconds".format(time.time() - start_time))

stream.stop_stream()
stream.close()

p.terminate()

Version without numpy:

import array
import math
import time

import pyaudio

p = pyaudio.PyAudio()

volume = 0.5  # range [0.0, 1.0]
fs = 44100  # sampling rate, Hz, must be integer
duration = 5.0  # in seconds, may be float
f = 440.0  # sine frequency, Hz, may be float

# generate samples, note conversion to float32 array
num_samples = int(fs * duration)
samples = [volume * math.sin(2 * math.pi * k * f / fs) for k in range(0, num_samples)]

# per @yahweh comment explicitly convert to bytes sequence
output_bytes = array.array('f', samples).tobytes()

# for paFloat32 sample values must be in range [-1.0, 1.0]
stream = p.open(format=pyaudio.paFloat32,
                channels=1,
                rate=fs,
                output=True)

# play. May repeat with different volume values (if done interactively)
start_time = time.time()
stream.write(output_bytes)
print("Played sound for {:.2f} seconds".format(time.time() - start_time))

stream.stop_stream()
stream.close()

p.terminate()
ivan_onys
  • 2,282
  • 17
  • 21
17

ivan-onys gave an excellent answer, but there is a little addition to it: this script will produce 4 times shorter sound than expected because Pyaudio write method needs string data of float32, but when you pass numpy array to this method, it converts whole array as entity to a string, therefore you have to convert data in numpy array to the byte sequence yourself like this:

samples = (np.sin(2*np.pi*np.arange(fs*duration)*f/fs)).astype(np.float32).tobytes()

and you have to change this line as well:

stream.write(samples)
yahweh
  • 193
  • 2
  • 8
  • Very interesting @yahweh. This solves another problem I had. Can you tell me why .tobytes() solves the problem? – mm_ Jul 23 '18 at 17:37
4

Today for Python 3.5+ the best way is to install the packages recommended by the developer.

http://people.csail.mit.edu/hubert/pyaudio/

For Debian do

sudo apt-get install python3-all-dev portaudio19-dev

before trying to install pyaudio

Drill Bit
  • 57
  • 8
4

One of the more consistent andeasy to install ways to deal with sound in Python is the Pygame multimedia libraries.

I'd recomend using it - there is the pygame.sndarray submodule that allows you to manipulate numbers in a data vector that become a high-level sound object that can be playerd in the pygame.mixer module.

The documentation in the pygame.org site should be enough for using the sndarray module.

jsbueno
  • 99,910
  • 10
  • 151
  • 209
1

The script from ivan_onys produces a signal that is four times shorter than intended. If a TypeError is returned when volume is a float, try adding .tobytes() to the following line instead.

stream.write((volume*samples).tobytes())

@mm_ float32 = 32 bits, and 8 bits = 1 byte, so float32 = 4 bytes. When samples are passed to stream.write as float32, byte count (duration) is divided by 4. Writing samples back .tobytes() corrects for quartering the sample count when writing to float32.

Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
zhen_les
  • 11
  • 2
0

I the bregman lab toolbox you have a set of functions that does exactly what you want. This python module is a little bit buggy but you can adapt this code to get your own functions

lizzie
  • 1,506
  • 1
  • 18
  • 31