From e9adb6d1172e13748f326da0cc627ca4f40d6dd2 Mon Sep 17 00:00:00 2001 From: Jacek Olszak Date: Sat, 2 Sep 2023 17:13:32 +0200 Subject: [PATCH 1/3] Amp of a single channel must be [-1,1] Currently, it is [-7,7] --- audio/synth.go | 4 ++-- audio/synth_test.go | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/audio/synth.go b/audio/synth.go index 1479a88..3dbd7c4 100644 --- a/audio/synth.go +++ b/audio/synth.go @@ -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 @@ -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 diff --git a/audio/synth_test.go b/audio/synth_test.go index ef5ed79..17a4bdc 100644 --- a/audio/synth_test.go +++ b/audio/synth_test.go @@ -304,6 +304,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) }) } @@ -324,6 +325,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) { @@ -489,3 +491,9 @@ 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) + } +} From d0dcf63bcebc7e609906b43e6cea76c105e73e7e Mon Sep 17 00:00:00 2001 From: Jacek Olszak Date: Sat, 2 Sep 2023 19:21:39 +0200 Subject: [PATCH 2/3] Change oscillator frequency based on sfx note pitch --- audio/synth.go | 3 ++ audio/synth_test.go | 67 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/audio/synth.go b/audio/synth.go index 3dbd7c4..360f180 100644 --- a/audio/synth.go +++ b/audio/synth.go @@ -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 { diff --git a/audio/synth_test.go b/audio/synth_test.go index 17a4bdc..bba97de 100644 --- a/audio/synth_test.go +++ b/audio/synth_test.go @@ -6,6 +6,8 @@ package audio_test import ( _ "embed" "fmt" + "math" + "math/cmplx" "testing" "github.com/stretchr/testify/assert" @@ -455,7 +457,27 @@ 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") }) } @@ -497,3 +519,46 @@ func assertAllValuesBetween(t *testing.T, minInclusive, maxInclusive float64, bu 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 + } +} From ea59173a036343f92d48229501b04ee454a17fb1 Mon Sep 17 00:00:00 2001 From: Jacek Olszak Date: Sat, 2 Sep 2023 21:01:24 +0200 Subject: [PATCH 3/3] Change oscillator waveform func based on sfx note instrument --- audio/synth.go | 2 +- audio/synth_test.go | 52 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/audio/synth.go b/audio/synth.go index 360f180..796fb1b 100644 --- a/audio/synth.go +++ b/audio/synth.go @@ -51,7 +51,7 @@ func (s *Synthesizer) ReadSamples(buffer []float64) { if ch.noteNo == len(sfx.Notes) { ch.playing = false } else { - //ch.oscillator.Func = oscillatorFunc(sfx.Notes[ch.noteNo].Instrument) + ch.oscillator.Func = oscillatorFunc(sfx.Notes[ch.noteNo].Instrument) ch.oscillator.FreqHz = pitchToFreq(sfx.Notes[ch.noteNo].Pitch) } } diff --git a/audio/synth_test.go b/audio/synth_test.go index bba97de..1f8e5aa 100644 --- a/audio/synth_test.go +++ b/audio/synth_test.go @@ -479,6 +479,26 @@ func TestSynthesizer_Sfx(t *testing.T) { // 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) + }) } func clone(s []byte) []byte { @@ -562,3 +582,35 @@ func hfft(input []float64, freqs []complex128, n, step int) { 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 +}