beep icon indicating copy to clipboard operation
beep copied to clipboard

Code advice: play a sound for n seconds every n seconds

Open Zireael opened this issue 4 years ago • 7 comments

Hi, I'm new to go, but saw beep package and implementing it would be the best/simplest solution to my requirements. I want to make a simple program that plays a bit of sound for n seconds every n seconds. Requirements:

  1. I want to decide what tone gets played (something not annoying). For this I repurposed Noise generator code and it works good enough
  2. I want to play the tone for n seconds. I'm still trying to figure this one out. For now I can get the tone to play as a series of clicks by playing with the numbers
  3. Repeat playing the same tone every n seconds. Here I was trying to put the generated Noise() into a buffer (something like buffer := beep.NewBuffer(format) buffer.Append(streamer)) and play that buffer, but no luck. Types mismatch or so.

The main issue I have at the moment is that the sound doesn't get played after the first for{} loop. Would you be able to give me some pointers on how to make a constant tune play for n seconds, and then play that tune again every n seconds?

package main

import (
	"flag"
	"fmt"
	"math/rand"
	"time"

	"github.com/faiface/beep"
	"github.com/faiface/beep/effects"
	"github.com/faiface/beep/speaker"
)

// Noise
func Noise() beep.Streamer {
	return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) {
		for i := range samples {
			fmt.Println(i)
			samples[i][0] = 0.25
			samples[i][1] = 0.25
			// samples[i][0] = rand.Float64()*2 - 1
			// samples[i][1] = rand.Float64()*2 - 1
		}
		return len(samples), true
	})
}

func main() {

	volumePtr := flag.Float64("volume", 5, "Audio volume (0 is normal volume, <0 quieter, >0 louder)")
	delayPtr := flag.Float64("delay", 3, "Time between audio repeats (seconds)")
	repeatPtr := flag.Int("repeats", 3, "How many times the audio will play. 0 = infinity")
	flag.Parse()

	rand.Seed(time.Now().UnixNano())

	sr := beep.SampleRate(200)
	speaker.Init(sr, int(sr))
	audiobuffer := Noise()
	volume := &effects.Volume{
		Streamer: audiobuffer,
		Base:     2,
		Volume:   *volumePtr,
		Silent:   false,
	}


	for k := 1; k <= *repeatPtr || *repeatPtr == 0; k++ {
		speaker.Play(beep.Seq(beep.Take(sr.N(200*time.Millisecond), volume), beep.Callback(func() {
		})))

		// speaker.Play(beep.Take(5, volume))
		// speaker.Play(volume)

		time.Sleep(time.Millisecond * 1) // line required for the sound to get played
		speaker.Clear()
		fmt.Printf("Waiting for %v seconds\n", *delayPtr)
		time.Sleep(time.Second * time.Duration(*delayPtr))
	}
}

Zireael avatar Apr 13 '20 14:04 Zireael

Hello hello,

Setting up the speaker correctly

First we need a speaker that does what we want. This is the function signature for the Init function:

func Init(sampleRate beep.SampleRate, bufferSize int) error

The samplerate is usually something like 44100 Hz (playback rate of CD's). It can be lower or higher. 200 Hz is too low for usual purposes.

The docs have a good comment on choosing a bufferSize:

The bufferSize argument specifies the number of samples of the speaker's buffer. Bigger bufferSize means lower CPU usage and more reliable playback. Lower bufferSize means better responsiveness and less delay.

Something around sampleRate.N(time.Millisecond*200) is probably ok. So we get:

const sampleRate = beep.SampleRate(44100)
if err := speaker.Init(sampleRate, sampleRate.N(time.Millisecond*200)); err != nil {
	panic(err)
}

Tone generation

The easiest way to generate a tone is using a sine wave. These tones can be extremely annoying, but choosing a good frequency helps a lot. I'm choosing 261.6 Hz, this is the frequency the middle C key on a keyboard plays at. You can also find other frequencies this way.

To create a frequency of freq Hz:

amplitude := math.Sin(2.0 * math.Pi * freq / float64(sampleRate.N(time.Second)) * float64(i))

This contains a couple of parts:

  1. The Math.Sin function makes a full revolution (goes up, down and returns back to zero again) every (=1/2π Hz). To make this a nice 1 Hz we multiply by .
  2. / sampleRate.N(time.Second) to scale it so that 1 revolution takes 1 second at our desired sample rate instead of once every sample.
  3. * freq: we don't want 1 Hz, we want freq Hz.
  4. * float64(i) to get the sample at position i of the playback.

Putting it in your tone generator:

func Tone(sampleRate beep.SampleRate, freq float64) beep.Streamer {
	var playbackPos int
	return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) {
		for i := range samples {
			amp := math.Sin(2.0 * math.Pi * freq / float64(sampleRate.N(time.Second)) * float64(playbackPos))
			samples[i][0] = amp
			samples[i][1] = amp
			playbackPos++
		}
		return len(samples), true
	})
}

I'm using playbackPos so that when the function gets called the second, third, fourth, ... time, it will start of at the position it ended the previous call. This is because the function will only be called to fill a relatively small buffer at once, so to generate a longer sound it must be called multiple times. If you don't do this you'll get weird artifacts in the sound because it suddenly jumps to another amplitude when going to the next buffer.

Play the tone for n seconds

I think you got this figured out already:

tone := Tone(sampleRate, 261.6) // Play middle C
playDuration := 200 * time.Millisecond
speaker.Play(beep.Take(sampleRate.N(playDuration), tone))
time.Sleep(playDuration)

I'm not using the Seq with callback here because it will notify you when the buffer is exhausted, not when the audio has been played completely. So it will notify us too soon.

I think there is also a delay between speaker.Play() and the speaker actually playing. So the time.Sleep method isn't perfect either. I think the Beep library is a bit lacking here.

Playing tones in a loop

Using a buffer doesn't really work for this. This is because when you convert a buffer to a stream using Buffer.Streamer(from, to int) StreamSeeker you have to give a start and end sample number and this can't be changed later. Buffer is for storing samples in a compact way in memory, not manipulating streams.

The quick and dirty way:

tone := Tone(sampleRate, 261.6) // Middle C
playDuration := 200 * time.Millisecond
sleepDuration := 800 * time.Millisecond
for i := 0; i < 3; i++ {
	speaker.Play(beep.Take(sampleRate.N(playDuration), tone))
	time.Sleep(playDuration + sleepDuration)
}

Using beep.Seq:

var seq []beep.Streamer
done := make(chan struct{})
for i := 0; i < 3; i++ {
	seq = append(seq, beep.Take(sampleRate.N(playDuration), tone))
	seq = append(seq, beep.Silence(sampleRate.N(sleepDuration)))
}
seq = append(seq, beep.Callback(func() {
	done<- struct{}{}
}))
speaker.Play(beep.Seq(seq...))
<-done

Using beep.Iterate:

i := 0
done := make(chan struct{})
speaker.Play(beep.Iterate(func() beep.Streamer {
	if i >= 3 {
		done <- struct{}{}
		return nil
	}
	i++

	return beep.Seq(
		beep.Take(sampleRate.N(playDuration), tone),
		beep.Silence(sampleRate.N(sleepDuration)),
	)
}))
<-done

The last two suffer from the same flaw as the Seq+Callback method to detect the end. The timing between tones is better though. You don't notice it because there is a silence between them but the samples are placed nicely after each other.

Using beep.Ctrl: You can figure this out yourself.

Other stuff

  • A sine wave generates samples between -1 and 1 amplitude. If you change the volume to be higher than 1 you'll get samples that are out of range and they may or may not be clipped depending on the OS/speakers drivers etc..
  • If the sine wave tone isn't good enough you could just play a wav/mp3 instead with whatever sound you like. You could also try playing multiple frequencies at once to create chords:
    playDuration := 200 * time.Millisecond
    sleepDuration := 800 * time.Millisecond
    i := 0
    done := make(chan struct{})
    speaker.Play(beep.Iterate(func() beep.Streamer {
    	if i >= 3 {
       	done <- struct{}{}
       	return nil
       }
       i++
    
       return beep.Seq(
       	beep.Mix(
       		beep.Take(sampleRate.N(playDuration), Tone(sampleRate, 220.0)), // A
       		beep.Take(sampleRate.N(playDuration), Tone(sampleRate, 277.1)), // C# (I think?)
       		beep.Take(sampleRate.N(playDuration), Tone(sampleRate, 329.6)), // E
       	),
    	beep.Silence(sampleRate.N(sleepDuration)),
       )
    }))
    <-done
    
    Note: you'll have to decrease the volume a bit (or make the amplitude smaller in the Tone function, which does the same), because mixing tones will just add them up and they will be clipped. If you get a horrible sound something is probably wrong.

Hope this helps. :)

MarkKremer avatar Apr 14 '20 11:04 MarkKremer

Wow, man @MarkKremer thanks for responding this thoughroughly, hope @Zireael finds it helpful.

faiface avatar Apr 14 '20 23:04 faiface

Thanks @MarkKremer, especially for the example on how to create tones and combine them together. ^^ Looks like I'll be able to build what I was after based on your snippets. I didn't see a tone generating code in examples wiki, and I'd say it would be helpful to be put in the wiki for others as an example for any music box or beep-boop projects.

Zireael avatar Apr 16 '20 23:04 Zireael

I think it would be nice to have an oscillator in the library.

Just thinking out loud:

freq := oscillator.NoteFrequency("C#") // What about different octaves???
stream, err := oscillator.Sine(freq)
freq := oscillator.Midi(48)

I don't have time to build this this very second but maybe we could discuss the interface and me or someone else can build it later.

MarkKremer avatar Apr 17 '20 15:04 MarkKremer

I think it would be nice to have an oscillator in the library.

Just thinking out loud:

freq := oscillator.NoteFrequency("C#") // What about different octaves???
stream, err := oscillator.Sine(freq)
freq := oscillator.Midi(48)

I don't have time to build this this very second but maybe we could discuss the interface and me or someone else can build it later.

What if we'd do something like

freq := oscillator.NoteFrequency("C#", n)

Where n is the octave num from -5 to 5 (same as in MIDI)? Would be happy to help with the design and implementation!

youshy avatar Jun 05 '20 22:06 youshy

That's a great suggestion. Would you like to take the lead working on this and making a first draft for this? I'm still happy to help where needed.

@faiface Can we assume you're happy to accept a PR for this eventually?


I tried to figure some stuff out. This site says that the octave numbers can be different across MIDI programs/devices. So what would be middle C? I think C4 is the most common. But that looks different from your -5 to 5. Could you explain it a bit more? I don't know much about how this works.

I forgot that C♭ and other -flat notes existed. This can be a problem if we use the note name as string input. Sonic Pi uses Cs4 and Db4 as names for constants. That could be a solution for us as well. We could also use constants instead of a function:

type Frequency float32

const (
	// ...
	B3 Frequency = 246.9417
	C4 = 261.6256
	Cs4 = 277.1826
	Db4 = 277.1826
	D4 = 293.6648
	Ds4 = 311.1270
	Eb4 = 311.1270
	// ...
)

Or something like that. I couldn't find if and how they deal with octave numbers smaller than zero. I think constants are a bit cleaner to use in most cases because they take up less space, but a bit harder to use if you want to loop over them or calculater something.

MarkKremer avatar Jun 06 '20 12:06 MarkKremer

Ok, I actually messed up with -5 to 5. We should go with Scientific pitch notation so then the lowest note is C0 which is 16,352 Hz.

As for flats and sharps, I like the Cs and Db idea. Also, I'd go with equal tempered tuning so, basically, our lib would be tuned as a full concert piano.

As a point there - for the equal tempered tuning, Cs and Db will be the same, but if we'd start branching out to another tuning systems, all based on western music (12 tone systems), then those two will be of different values.

I'm happy to make the first draft in a few weeks!

youshy avatar Jun 14 '20 08:06 youshy