Merge pull request #124 from cswank/equalizer

Parametric equalizer
This commit is contained in:
Allen Ray
2021-08-17 00:20:54 -04:00
committed by GitHub
3 changed files with 269 additions and 0 deletions

194
effects/equalizer.go Normal file
View File

@@ -0,0 +1,194 @@
package effects
import (
"math"
"github.com/faiface/beep"
)
type (
// This parametric equalizer is based on the GK Nilsen's post at:
// https://octovoid.com/2017/11/04/coding-a-parametric-equalizer-for-audio-applications/
equalizer struct {
streamer beep.Streamer
sections []section
}
section struct {
a, b [2][]float64
xPast, yPast [][2]float64
}
// EqualizerSections is the interfacd that is passed into NewEqualizer
EqualizerSections interface {
sections(fs float64) []section
}
StereoEqualizerSection struct {
Left MonoEqualizerSection
Right MonoEqualizerSection
}
MonoEqualizerSection struct {
// F0 (center frequency) sets the mid-point of the sections
// frequency range and is given in Hertz [Hz].
F0 float64
// Bf (bandwidth) represents the width of the section across
// frequency and is measured in Hertz [Hz]. A low bandwidth
// corresponds to a narrow frequency range meaning that the
// section will concentrate its operation to only the
// frequencies close to the center frequency. On the other hand,
// a high bandwidth yields a section of wide frequency range —
// affecting a broader range of frequencies surrounding the
// center frequency.
Bf float64
// GB (bandwidth gain) is given in decibels [dB] and represents
// the level at which the bandwidth is measured. That is, to
// have a meaningful measure of bandwidth, we must define the
// level at which it is measured.
GB float64
// G0 (reference gain) is given in decibels [dB] and simply
// represents the level of the sections offset.
G0 float64
// G (boost/cut gain) is given in decibels [dB] and prescribes
// the effect imposed on the audio loudness for the sections
// frequency range. A boost/cut level of 0 dB corresponds to
// unity (no operation), whereas negative numbers corresponds to
// cut (volume down) and positive numbers to boost (volume up).
G float64
}
// StereoEqualizerSections implements EqualizerSections and can be passed into NewEqualizer
StereoEqualizerSections []StereoEqualizerSection
// MonoEqualizerSections implements EqualizerSections and can be passed into NewEqualizer
MonoEqualizerSections []MonoEqualizerSection
)
// NewEqualizer returns a beep.Streamer that modifies the stream based on the EqualizerSection slice that is passed in.
// The SampleRate (sr) must match that of the Streamer.
func NewEqualizer(st beep.Streamer, sr beep.SampleRate, s EqualizerSections) beep.Streamer {
return &equalizer{
streamer: st,
sections: s.sections(float64(sr)),
}
}
func (m MonoEqualizerSections) sections(fs float64) []section {
out := make([]section, len(m))
for i, s := range m {
out[i] = s.section(fs)
}
return out
}
func (m StereoEqualizerSections) sections(fs float64) []section {
out := make([]section, len(m))
for i, s := range m {
out[i] = s.section(fs)
}
return out
}
// Stream streams the wrapped Streamer modified by Equalizer.
func (e *equalizer) Stream(samples [][2]float64) (n int, ok bool) {
n, ok = e.streamer.Stream(samples)
for _, s := range e.sections {
s.apply(samples)
}
return n, ok
}
// Err propagates the wrapped Streamer's errors.
func (e *equalizer) Err() error {
return e.streamer.Err()
}
func (m MonoEqualizerSection) section(fs float64) section {
beta := math.Tan(m.Bf/2.0*math.Pi/(fs/2.0)) *
math.Sqrt(math.Abs(math.Pow(math.Pow(10, m.GB/20.0), 2.0)-
math.Pow(math.Pow(10.0, m.G0/20.0), 2.0))) /
math.Sqrt(math.Abs(math.Pow(math.Pow(10.0, m.G/20.0), 2.0)-
math.Pow(math.Pow(10.0, m.GB/20.0), 2.0)))
b := []float64{
(math.Pow(10.0, m.G0/20.0) + math.Pow(10.0, m.G/20.0)*beta) / (1 + beta),
(-2 * math.Pow(10.0, m.G0/20.0) * math.Cos(m.F0*math.Pi/(fs/2.0))) / (1 + beta),
(math.Pow(10.0, m.G0/20) - math.Pow(10.0, m.G/20.0)*beta) / (1 + beta),
}
a := []float64{
1.0,
-2 * math.Cos(m.F0*math.Pi/(fs/2.0)) / (1 + beta),
(1 - beta) / (1 + beta),
}
return section{
a: [2][]float64{a, a},
b: [2][]float64{b, b},
}
}
func (s StereoEqualizerSection) section(fs float64) section {
l := s.Left.section(fs)
r := s.Right.section(fs)
return section{
a: [2][]float64{l.a[0], r.a[0]},
b: [2][]float64{l.b[0], r.b[0]},
}
}
func (s *section) apply(x [][2]float64) {
ord := len(s.a[0]) - 1
np := len(x) - 1
if np < ord {
x = append(x, make([][2]float64, ord-np)...)
np = ord
}
y := make([][2]float64, len(x))
if len(s.xPast) < len(x) {
s.xPast = append(s.xPast, make([][2]float64, len(x)-len(s.xPast))...)
}
if len(s.yPast) < len(x) {
s.yPast = append(s.yPast, make([][2]float64, len(x)-len(s.yPast))...)
}
for i := 0; i < len(x); i++ {
for j := 0; j < ord+1; j++ {
if i-j < 0 {
y[i][0] = y[i][0] + s.b[0][j]*s.xPast[len(s.xPast)-j][0]
y[i][1] = y[i][1] + s.b[1][j]*s.xPast[len(s.xPast)-j][1]
} else {
y[i][0] = y[i][0] + s.b[0][j]*x[i-j][0]
y[i][1] = y[i][1] + s.b[1][j]*x[i-j][1]
}
}
for j := 0; j < ord; j++ {
if i-j-1 < 0 {
y[i][0] = y[i][0] - s.a[0][j+1]*s.yPast[len(s.yPast)-j-1][0]
y[i][1] = y[i][1] - s.a[1][j+1]*s.yPast[len(s.yPast)-j-1][1]
} else {
y[i][0] = y[i][0] - s.a[0][j+1]*y[i-j-1][0]
y[i][1] = y[i][1] - s.a[1][j+1]*y[i-j-1][1]
}
}
y[i][0] = y[i][0] / s.a[0][0]
y[i][1] = y[i][1] / s.a[1][0]
}
s.xPast = x[:]
s.yPast = y[:]
copy(x, y)
}

View File

@@ -0,0 +1,36 @@
package main
import (
"math/rand"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/effects"
"github.com/faiface/beep/speaker"
)
func noise() beep.Streamer {
return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) {
for i := range samples {
samples[i][0] = rand.Float64()*2 - 1
samples[i][1] = rand.Float64()*2 - 1
}
return len(samples), true
})
}
func main() {
sr := beep.SampleRate(44100)
speaker.Init(sr, sr.N(time.Second/10))
eq := effects.NewEqualizer(noise(), sr, effects.MonoEqualizerSections{
{F0: 200, Bf: 5, GB: 3, G0: 0, G: 8},
{F0: 250, Bf: 5, GB: 3, G0: 0, G: 10},
{F0: 300, Bf: 5, GB: 3, G0: 0, G: 12},
{F0: 350, Bf: 5, GB: 3, G0: 0, G: 14},
{F0: 10000, Bf: 8000, GB: 3, G0: 0, G: -100},
})
speaker.Play(eq)
select {}
}

View File

@@ -0,0 +1,39 @@
package main
import (
"math/rand"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/effects"
"github.com/faiface/beep/speaker"
)
func noise() beep.Streamer {
return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) {
for i := range samples {
samples[i][0] = rand.Float64()*2 - 1
samples[i][1] = rand.Float64()*2 - 1
}
return len(samples), true
})
}
func main() {
sr := beep.SampleRate(44100)
speaker.Init(sr, sr.N(time.Second/10))
eq := effects.NewEqualizer(noise(), sr, effects.StereoEqualizerSections{
{
Left: effects.MonoEqualizerSection{F0: 200, Bf: 5, GB: 3, G0: 0, G: 8},
Right: effects.MonoEqualizerSection{F0: 200, Bf: 5, GB: 3, G0: 0, G: -8},
},
{
Left: effects.MonoEqualizerSection{F0: 10000, Bf: 1000, GB: 3, G0: 0, G: 90},
Right: effects.MonoEqualizerSection{F0: 10000, Bf: 1000, GB: 3, G0: 0, G: -90},
},
})
speaker.Play(eq)
select {}
}