Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change osc based on notes #108

Merged
merged 3 commits into from
Sep 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions audio/synth.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type Synthesizer struct {
// which is 23ms of audio.
//
// Values written to the buffer are usually in range between -1.0 and 1.0, but sometimes they can exceed the range
// (for example due to audio channels summing).
// (for example due to audio channels summing). Min is -4.0, max is 4.0 inclusive.
func (s *Synthesizer) ReadSamples(buffer []float64) {
if len(buffer) == 0 {
return
Expand All @@ -41,7 +41,7 @@ func (s *Synthesizer) ReadSamples(buffer []float64) {
for channelIdx, ch := range s.channels {
if ch.playing {
sfx := s.GetSfx(ch.sfxNo)
volume := float64(sfx.Notes[ch.noteNo].Volume)
volume := float64(sfx.Notes[ch.noteNo].Volume) / 7
samples[channelIdx] = ch.oscillator.NextSample() * volume
ch.sampleNo += 1
noteHasEnded := ch.sampleNo == ch.noteEndSample
Expand All @@ -50,6 +50,9 @@ func (s *Synthesizer) ReadSamples(buffer []float64) {
ch.noteNo++
if ch.noteNo == len(sfx.Notes) {
ch.playing = false
} else {
ch.oscillator.Func = oscillatorFunc(sfx.Notes[ch.noteNo].Instrument)
ch.oscillator.FreqHz = pitchToFreq(sfx.Notes[ch.noteNo].Pitch)
}
}
} else {
Expand Down
127 changes: 126 additions & 1 deletion audio/synth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package audio_test
import (
_ "embed"
"fmt"
"math"
"math/cmplx"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -304,6 +306,7 @@ func TestSynthesizer_Sfx(t *testing.T) {
s.Sfx(0, audio.Channel(channelNo), 0, 1)
// then
s.ReadSamples(buffer)
assertAllValuesBetween(t, -1.0, 1.0, buffer)
assertAllValuesDifferent(t, buffer)
})
}
Expand All @@ -324,6 +327,7 @@ func TestSynthesizer_Sfx(t *testing.T) {

expectedSample := singleChannelBuffer[0] * maxChannels
assert.InDelta(t, expectedSample, allChannelBuffer[0], 0.0000001)
assertAllValuesBetween(t, -4.0, 4.0, allChannelBuffer)
})

t.Run("should stop playing on a given channel", func(t *testing.T) {
Expand Down Expand Up @@ -453,7 +457,47 @@ func TestSynthesizer_Sfx(t *testing.T) {
// then
buffer2 := make([]float64, len(e.Notes)*durationOfNoteWhenSpeedIsOne)
synth.ReadSamples(buffer2)
// no assertion because generated samples are different because of phase shift
assert.True(t, dominantFrequency(buffer1) == dominantFrequency(buffer2), "frequency should be the same")
})

t.Run("should change oscillator frequency when second note has different pitch", func(t *testing.T) {
e := audio.SoundEffect{
Notes: [32]audio.Note{
{Pitch: audio.PitchC0, Volume: audio.VolumeLoudest},
{Pitch: audio.PitchDs5, Volume: audio.VolumeLoudest},
},
Speed: 255,
}
synth := audio.Synthesizer{}
synth.SetSfx(0, e)
synth.Sfx(0, 0, 0, 31)
buffer1 := make([]float64, 255*durationOfNoteWhenSpeedIsOne)
synth.ReadSamples(buffer1)
buffer2 := make([]float64, 255*durationOfNoteWhenSpeedIsOne)
// when
synth.ReadSamples(buffer2)
// then
assert.True(t, dominantFrequency(buffer1) < dominantFrequency(buffer2), "frequency of pitch C1 must be smaller than D#5")
})

t.Run("should generate different wave when second note has a different instrument", func(t *testing.T) {
e := audio.SoundEffect{
Notes: [32]audio.Note{
{Instrument: audio.InstrumentTriangle, Volume: audio.VolumeLoudest},
{Instrument: audio.InstrumentSaw, Volume: audio.VolumeLoudest},
},
Speed: 32,
}
synth := audio.Synthesizer{}
synth.SetSfx(0, e)
synth.Sfx(0, 0, 0, 31)
buffer1 := make([]float64, 32*durationOfNoteWhenSpeedIsOne)
synth.ReadSamples(buffer1)
buffer2 := make([]float64, 32*durationOfNoteWhenSpeedIsOne)
// when
synth.ReadSamples(buffer2)
// then
assertDifferentShape(t, buffer1, buffer2)
})
}

Expand Down Expand Up @@ -489,3 +533,84 @@ func generateSamples(e audio.SoundEffect, bufferSize int) []float64 {
synth.ReadSamples(buffer)
return buffer
}

func assertAllValuesBetween(t *testing.T, minInclusive, maxInclusive float64, buffer []float64) {
for i, b := range buffer {
require.Truef(t, b >= minInclusive && b <= maxInclusive, "buffer[%d] is not between [%f,%f]", i, minInclusive, maxInclusive)
}
}

func dominantFrequency(input []float64) int {
maxAmplitude := 0.0
dominantFrequencyIndex := 0

for i, value := range fft(input) {
amplitude := cmplx.Abs(value)
if amplitude > maxAmplitude {
maxAmplitude = amplitude
dominantFrequencyIndex = i
}
}

return int(
float64(dominantFrequencyIndex) * audio.SampleRate / float64(len(input)),
)
}

// fft runs Fast Fourier Transform on given input.
func fft(input []float64) []complex128 {
freqs := make([]complex128, len(input))
hfft(input, freqs, len(input), 1)
return freqs
}

// code by Dylan Meeus, from GoAudio library: https://github.com/DylanMeeus/GoAudio
func hfft(input []float64, freqs []complex128, n, step int) {
if n == 1 {
freqs[0] = complex(input[0], 0)
return
}

h := n / 2

hfft(input, freqs, h, 2*step)
hfft(input[step:], freqs[h:], h, 2*step)

for k := 0; k < h; k++ {
a := -2 * math.Pi * float64(k) * float64(n)
e := cmplx.Rect(1, a) * freqs[k+h]
freqs[k], freqs[k+h] = freqs[k]+e, freqs[k]-e
}
}

func assertDifferentShape(t *testing.T, buffer1, buffer2 []float64) {
dtw := dtwDistance(buffer1, buffer2)
assert.Truef(t, dtw >= 30.00, "Waves should have different shape, but dtw distance = %f. Must be >= 30.00", dtw)
}

// dtwDistance calculates dynamic time warping distance between two signals
func dtwDistance(signal1, signal2 []float64) float64 {
len1, len2 := len(signal1), len(signal2)
dtw := make([][]float64, len1+1)
for i := range dtw {
dtw[i] = make([]float64, len2+1)
}

for i := 1; i <= len1; i++ {
for j := 1; j <= len2; j++ {
cost := math.Abs(signal1[i-1] - signal2[j-1])
dtw[i][j] = cost + min3(dtw[i-1][j], dtw[i][j-1], dtw[i-1][j-1])
}
}

return dtw[len1][len2]
}

func min3(a, b, c float64) float64 {
if a <= b && a <= c {
return a
} else if b <= a && b <= c {
return b
}
return c
}
Loading