Open Zireael opened 4 years ago
Hello hello,
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)
}
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:
Math.Sin
function makes a full revolution (goes up, down and returns back to zero again) every 2π
(=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.* freq
: we don't want 1 Hz
, we want freq Hz
.* 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.
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.
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.
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. :)
Wow, man @MarkKremer thanks for responding this thoughroughly, hope @Zireael finds it helpful.
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.
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.
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!
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.
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
andDb
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!
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 likebuffer := 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?