User avatar
RogerW
Posts: 307
Joined: Sat Dec 20, 2014 12:15 pm
Location: London UK

How to generate an audio tone from a program

Thu Jun 24, 2021 10:40 am

I want to generate an audio tone through the standard audio output. I need to dynamically control the frequency and volume. I am happy to use C/C+ or python.
I have tried using the pygame mixer. This works and I can generate a sine wave tone and control frequency and volume. Unfortunately to change the frequency I have to quit the mixer and then call its init. This takes too long and gives a gap in the sound.
I am using the current Raspberry Pi OS. Development is on a Pi4b-8 but I hope to use a pi Zero with command line only eventually. Can anyone recommend a suitable api?

klricks
Posts: 7858
Joined: Sat Jan 12, 2013 3:01 am
Location: Grants Pass, OR, USA
Contact: Website

Re: How to generate an audio tone from a program

Thu Jun 24, 2021 12:19 pm

RogerW wrote:
Thu Jun 24, 2021 10:40 am
I want to generate an audio tone through the standard audio output..........
......but I hope to use a pi Zero with command line only eventually......
Note that a RPi 0 does not have analog sound output..... Only sound via HDMI..
Unless specified otherwise my response is based on the latest and fully updated RPiOS Buster w/ Desktop OS.

User avatar
RogerW
Posts: 307
Joined: Sat Dec 20, 2014 12:15 pm
Location: London UK

Re: How to generate an audio tone from a program

Thu Jun 24, 2021 12:33 pm

My plan is to use Bluetooth

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

Re: How to generate an audio tone from a program

Thu Jun 24, 2021 12:40 pm


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

Re: How to generate an audio tone from a program

Thu Jun 24, 2021 12:43 pm

klricks wrote:
Thu Jun 24, 2021 12:19 pm
RogerW wrote:
Thu Jun 24, 2021 10:40 am
I want to generate an audio tone through the standard audio output..........
......but I hope to use a pi Zero with command line only eventually......
Note that a RPi 0 does not have analog sound output..... Only sound via HDMI..
True, but pwm audio or i2s can be configured on the gpio.
You can buy audio filter add-ons to turn the pwm into filtered line level audio

User avatar
RogerW
Posts: 307
Joined: Sat Dec 20, 2014 12:15 pm
Location: London UK

Re: How to generate an audio tone from a program

Thu Jun 24, 2021 12:55 pm

PiGraham wrote:
Thu Jun 24, 2021 12:40 pm
See this post
viewtopic.php?f=32&t=313592#p1876682
Thanks for the link. Unfortunately I want to use the standard audio output via bluetooth. I know I could output to a PIO but I do not want to use external hardware and I would prefer not to use a square wave.

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

Re: How to generate an audio tone from a program

Thu Jun 24, 2021 4:13 pm

RogerW wrote:
Thu Jun 24, 2021 12:55 pm
PiGraham wrote:
Thu Jun 24, 2021 12:40 pm
See this post
viewtopic.php?f=32&t=313592#p1876682
Thanks for the link. Unfortunately I want to use the standard audio output via bluetooth. I know I could output to a PIO but I do not want to use external hardware and I would prefer not to use a square wave.
I may be wrong but As I read that post it is using pygame to output audio on standard audio output.
You could fill the array with any waveform you like, or play an audio file. Also mentioned is pygame.midi which can play whatever notes you like on any voice supported by a software midi synth.

User avatar
RogerW
Posts: 307
Joined: Sat Dec 20, 2014 12:15 pm
Location: London UK

Re: How to generate an audio tone from a program

Thu Jun 24, 2021 4:23 pm

Yes I have tried pygame and it will generate a tone. However I need to seemlesly change the frequency and I cannot do this with pygame. There is a time delay when I change frequency that I cannot find a way round.

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

Re: How to generate an audio tone from a program

Thu Jun 24, 2021 4:38 pm

RogerW wrote:
Thu Jun 24, 2021 4:23 pm
Yes I have tried pygame and it will generate a tone. However I need to seemlesly change the frequency and I cannot do this with pygame. There is a time delay when I change frequency that I cannot find a way round.
Do you mean you want to switch from one tone to the next without a gap or do you want to slide from one pitch to another?

I think you do either with midi.

See https://studiocode.dev/resources/midi-pitch-bend/

I'd be surprised if you cant do it with a wavetable as awell, but you would have to generate the cycles of changing pitch. in the buffer.

knute
Posts: 703
Joined: Thu Oct 23, 2014 12:14 am
Location: Texas
Contact: Website

Re: How to generate an audio tone from a program

Thu Jun 24, 2021 4:48 pm

Here is some Java code to play tones.

Code: Select all

import java.util.stream.*;
import javax.sound.sampled.*;

/*
 * Tone is a class to sound a tone using the JavaSound API.  The sample rate
 * defaults to 16000f and the level defaults to 1.0f.  Level is on a log scale
 * from 0.0f to 1.0f.  If a level control is not available from the
 * SourceDataLine then the leve is set to the default.
 *
 * @author  Knute Johnson
 * @version 29 January 2020
 */
public class Tone {
    /** Default sample rate */
    private static final float SAMPLE_RATE = 16000f;

    /** Default level */
    private static final float LEVEL = 1.0f;

    /** Acutal sample rate */
    private final float sampleRate;

    /** SourceDataLine used to play sound */
    private final SourceDataLine sdl;

    /** Master volume control */
    private final FloatControl control;

    /**
     * Creates a new Tone object with the specified sample rate
     * @param   sampleRate  audio format sample rate
     *
     * @throws LineUnavailableException if a SourceDataLine with the specified
     *          sample rate is not available
     * @throws IllegalArgumentException if sample rate is negative
     */
    public Tone(float sampleRate) throws LineUnavailableException {
        if (sampleRate < 0.0f)
            throw new IllegalArgumentException("bad sampleRate");

        this.sampleRate = sampleRate;
        AudioFormat af = new AudioFormat(sampleRate,8,1,true,false);
        //System.out.println(af);
        sdl = AudioSystem.getSourceDataLine(af);
        sdl.open();
        if (sdl.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
            control =
             (FloatControl)sdl.getControl(FloatControl.Type.MASTER_GAIN);
        } else {
            control = null;
        }
        sdl.start();
    }

    /**
     * Create a Tone object with the default sample rate
     *
     * @throws LineUnavailableException if a SourceDataLine with the default 
     *          sample rate is not available
     */
    public Tone() throws LineUnavailableException {
        this(SAMPLE_RATE);
    }

    /**
     * Sound a tone of the specified duration at the specified frequency and
     * level.
     *
     * @param   duration    length of tone in seconds
     * @param   frequency   frequency of tone in hertz
     * @param   level       level of tone in percent
     *
     * @throws  IllegalArgumentException if any parameter is negative or level
     *          is greater than 1.0f.
     */
    public void tone(float duration, int frequency, float level) {
        if (duration < 0.0f || frequency < 0 || level < 0.0f || level > 1.0f)
            throw new IllegalArgumentException("bad parameter(s)");

        if (control != null)
            control.setValue((float)(Math.log10(level) * 20.0));
        byte[] buf = new byte[(int)(sampleRate * duration)];
        double radiansPerSample = 2.0 * Math.PI * frequency / sampleRate;
        for (int i=0; i<buf.length; i++) {
            buf[i] = (byte)(Math.sin(radiansPerSample * i) * 127.0);
        }
        sdl.write(buf,0,buf.length);
        sdl.drain();
    }

    /**
     * Sound a tone of the specified duration at the specified frequency and
     * default level.
     *
     * @param   duration    length of tone in seconds
     * @param   frequency   frequency of tone in hertz
     *
     * @throws  IllegalArgumentException if duration or frequency is negative
     */
    public void tone(float duration, int frequency) {
        if (duration < 0.0f || frequency < 0)
            throw new IllegalArgumentException("bad parameter(s)");

        tone(duration,frequency,LEVEL);
    }

    /**
     * Check for level control.
     *
     * @return  true if level control is available
     */
    public boolean isLevelControlAvailable() {
        return control != null;
    }

    /**
     * Closes the tone object.  A closed Tone object will no longer create a
     * sound.
     */
    public void close() {
        sdl.stop();
        sdl.close();
    }

    /**
     * Convenience method to sleep the thread.
     *
     * @param   msec    milliseconds to sleep
     */
    public void pause(int msec) {
        try {
            Thread.sleep(msec);
        } catch (InterruptedException ie) { }
    }

    /**
     * Plays a tone for the specified duration with the specified sampleRate
     * at the specified frequency and level.
     *
     * @param   duration    length of tone in seconds
     * @param   sampleRate  sample rate in frames per second
     * @param   frequency   frequency of tone in hertz
     * @param   level       level of tone in percent
     *
     * @throws  IllegalArgumentException if any parameter is negative, level
     *          is greater than 1.0f or a SourceDataLine cannot be created
     */
    public static void play(float duration, float sampleRate, int frequency,
     float level) throws LineUnavailableException {
        if (duration < 0.0f || sampleRate < 0.0f || frequency < 0 ||
         level < 0.0f || level > 1.0f)
            throw new IllegalArgumentException("bad parameter(s)");

        byte[] buf = new byte[(int)(sampleRate * duration)];
        double radiansPerSample = 2.0 * Math.PI * frequency / sampleRate;
        for (int i=0; i<buf.length; i++) {
            buf[i] = (byte)(Math.sin(radiansPerSample * i) * 127.0);
        }

        AudioFormat af = new AudioFormat(sampleRate,8,1,true,false);
        SourceDataLine sdl = AudioSystem.getSourceDataLine(af);
        sdl.open(af);
        if (sdl.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
            FloatControl control =
             (FloatControl)sdl.getControl(FloatControl.Type.MASTER_GAIN);
            control.setValue((float)(Math.log10(level) * 20.0));
        }
        sdl.start();
        sdl.write(buf,0,buf.length);
        sdl.drain();
        sdl.stop();
        sdl.close();
    }

    /**
     * Plays a tone for the specified duration with the default sampleRate
     * at the specified frequency and level.
     *
     * @param   duration    length of tone in seconds
     * @param   frequency   frequency of tone in hertz
     * @param   level       level of tone in percent
     *
     * @throws  IllegalArgumentException if any parameter is negative, level
     *          is greater than 1.0f or a SourceDataLine cannot be created
     */
    public static void play(float duration, int frequency, float level)
     throws LineUnavailableException {
        if (duration < 0.0f || frequency < 0 || level < 0.0f || level > 1.0f)
            throw new IllegalArgumentException("bad parameter(s)");

        play(duration,SAMPLE_RATE,frequency,level);
    }
        
    /**
     * Plays a tone for the specified duration with the default sampleRate
     * at the specified frequency and default level.
     *
     * @param   duration    length of tone in seconds
     * @param   frequency   frequency of tone in hertz
     *
     * @throws  IllegalArgumentException if any duration is negative or a
     *           SourceDataLine cannot be created
     */
    public static void play(float duration, int frequency) throws
     LineUnavailableException {
         if (duration < 0.0f || frequency < 0)
             throw new IllegalArgumentException("bad parameter(s)");

        play(duration,SAMPLE_RATE,frequency,LEVEL);
    }

    public static void main(String... args) throws Exception {
        Tone tone = new Tone(44100);
        tone.pause(1000);
        tone.tone(0.1f,392,0.4f);
        tone.pause(20);
        tone.tone(0.1f,392,0.4f);
        tone.pause(20);
        tone.tone(0.1f,392,0.4f);
        tone.pause(20);
        tone.tone(1.0f,311,0.5f);
        tone.close();
    }
}


User avatar
RogerW
Posts: 307
Joined: Sat Dec 20, 2014 12:15 pm
Location: London UK

Re: How to generate an audio tone from a program

Thu Jun 24, 2021 5:07 pm

PiGraham
Thanks for the rply. I am trying to develop a theramin so I need to slide frequency and volume depending on the input fro two sensors. With pygame I cannot change the frequency fast enough. MIDI may be useful but I am afraid I don't see how.

knute
Thanks for the code. I am not familiar with Java but I suspect I would have the same problem. I need to slide from one frequency to the next without a gap.

Heater
Posts: 18369
Joined: Tue Jul 17, 2012 3:02 pm

Re: How to generate an audio tone from a program

Thu Jun 24, 2021 10:31 pm

It's probably easier to build a theramin with good old analogue electronics hardware than to ever get a Linux sound system to work properly.
Memory in C++ is a leaky abstraction .

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

Re: How to generate an audio tone from a program

Fri Jun 25, 2021 7:36 am

RogerW wrote:
Thu Jun 24, 2021 5:07 pm
PiGraham
Thanks for the rply. I am trying to develop a theramin so I need to slide frequency and volume depending on the input fro two sensors. With pygame I cannot change the frequency fast enough. MIDI may be useful but I am afraid I don't see how.
This video might give you an idea of what can be done with midi.
pitch bend slides frequency. Wha effects or envelopes can slide amplitude.
I'm no expert on midi but I think it is a good direction to try. I doubt that pymidi isn't fast enough. The CPU load on the midi controller part should be very light.
Here is an example of what a soft synth on a Pi can do, with pitch bends etc.

Install the a soft midi synth.
Create a midi controller with pymidi ithat maps your sensors to midi messages sent to the soft synth.

Control music with your sensors.

If you want a really authentic Theremin sound you probably need to go analogue and make an actual theramin.
Last edited by PiGraham on Tue Jun 29, 2021 9:48 am, edited 1 time in total.

User avatar
RogerW
Posts: 307
Joined: Sat Dec 20, 2014 12:15 pm
Location: London UK

Re: How to generate an audio tone from a program

Fri Jun 25, 2021 1:44 pm

Thanks for the replies and links.
I am not after authenticity - just a project to use some pi related bits and pieces. I have Sharp distance sensors, a pi zero w and mcp3008 AtoD. chips. I have already written software to read the mcp3008 in python and C++.
Looks like MIDI might be the way to go but I want to do everything in the pi even if that makes the device primitive. Using discrete notes rather than continuous tone is something I will have to think about.
In case it might be useful to someone else here is my python tone generator software.
This is just a testbed to play with

Code: Select all

# Test synth

from signal import signal, SIGINT
from time import sleep

from synth import Synth

# respond to Ctrl+C to terminate
run = True
def on_exit(a,b):
    global run
    run = False
    
signal(SIGINT,on_exit)	
	
synth = Synth(100)
freq = 500
vol = 1.0
duration = -1

#synth.play(700,0.9,2.5)
#sleep(4)

while run:
	#print(freq)
	synth.play(freq,vol,duration)
	sleep(0.5)
	freq += 5

synth.close()
This class does the work. put synth.py in the same directory as the testbed

Code: Select all

# synth.py
# written by Roger Woollett

# generate a sine wave audio sound of set frequency and volume
# uses pygame

import pygame
import math
import numpy

class Synth():
	def __init__(self,samples = 200):
		
		# create a buffer to contain one complete sine wave
		self.num_samples = samples
		bits = 16
		buff = numpy.zeros(self.num_samples, dtype = numpy.int16)
		
		# max_value is (nearly) largest signed 16 bit integer
		max_value = 2**(bits - 1) - 20 # do not use absolute max size - causes problems
		angle = 0
		inc = 2*math.pi/self.num_samples

		# fill buffer with one complete cycle
		for i in range(self.num_samples):
			buff[i] = max_value*math.sin(angle)
			angle += inc
	 
		# setup pygame
		pygame.mixer.pre_init(22050, -bits, 1)
		pygame.init()
		self.sound = pygame.sndarray.make_sound(buff)

	def play(self,frequency,volume = 1.0,duration = -1):
		# frequency in Hz
		# volume 0.0 to 1.0
		# duration in seconds or -1 for continuous
		
		# This because otherwise 0 would be continuous
		if duration == 0:
			return
		 
		# quit the mixer so we can set new sample rate
		pygame.mixer.quit()
		pygame.mixer.init(frequency*self.num_samples)
		self.sound.set_volume(volume)
		if duration == -1:
			self.sound.play(loops = -1)
		else:
			self.sound.play(loops = int(duration*frequency) - 1)
		
	def stop(self):
		self.sound.stop()
		
	def close(self):
		# call before program exit
		pygame.quit()

Return to “General programming discussion”