126 lines
3.0 KiB
Go
126 lines
3.0 KiB
Go
package audio
|
||
|
||
import (
|
||
"encoding/binary"
|
||
"fmt"
|
||
"math"
|
||
)
|
||
|
||
type Generator interface {
|
||
// Generate fills consecutive float32 sample fragments for one channel.
|
||
// firstSample is the absolute MXL sample index, so phase does not depend on
|
||
// batch or ring-buffer boundaries.
|
||
Generate(channel uint, firstSample uint64, fragments ...[]byte) error
|
||
}
|
||
|
||
type SineGenerator struct {
|
||
channels uint
|
||
sampleRate float64
|
||
baseFrequency float64
|
||
amplitude float32
|
||
}
|
||
|
||
func NewSineGenerator(
|
||
cfg Config,
|
||
baseFrequency float64,
|
||
) (*SineGenerator, error) {
|
||
if cfg.Channels() == 0 {
|
||
return nil, fmt.Errorf("channel count must be greater than zero")
|
||
}
|
||
rate := cfg.Rate()
|
||
if rate.Num <= 0 || rate.Den <= 0 {
|
||
return nil, fmt.Errorf(
|
||
"sample rate numerator and denominator must be greater than zero, got %d/%d",
|
||
rate.Num,
|
||
rate.Den,
|
||
)
|
||
}
|
||
if math.IsNaN(baseFrequency) || math.IsInf(baseFrequency, 0) || baseFrequency <= 0 {
|
||
return nil, fmt.Errorf("base frequency must be finite and greater than zero, got %g", baseFrequency)
|
||
}
|
||
|
||
// Keep every generated tone below the Nyquist frequency (the
|
||
// Kotelnikov–Nyquist–Shannon sampling limit), sampleRate/2
|
||
// Nyquist leaved as compromise for common English terminology
|
||
sampleRate := float64(rate.Num) / float64(rate.Den)
|
||
highestFrequency := float64(cfg.Channels()) * baseFrequency
|
||
nyquist := sampleRate / 2
|
||
if highestFrequency >= nyquist {
|
||
return nil, fmt.Errorf(
|
||
"highest tone frequency %.0f Hz must be below Nyquist frequency %.0f Hz",
|
||
highestFrequency,
|
||
nyquist,
|
||
)
|
||
}
|
||
|
||
if math.IsNaN(cfg.LevelDBFS) || math.IsInf(cfg.LevelDBFS, 0) {
|
||
return nil, fmt.Errorf("audio level must be finite")
|
||
}
|
||
if cfg.LevelDBFS > 0 {
|
||
return nil, fmt.Errorf(
|
||
"audio level must not exceed 0 dBFS, got %.2f",
|
||
cfg.LevelDBFS,
|
||
)
|
||
}
|
||
if cfg.LevelDBFS < -100 {
|
||
return nil, fmt.Errorf(
|
||
"audio level must be at least -100 dBFS, got %.2f dBFS",
|
||
cfg.LevelDBFS,
|
||
)
|
||
}
|
||
|
||
amplitude := math.Pow(10, cfg.LevelDBFS/20)
|
||
return &SineGenerator{
|
||
channels: cfg.Channels(),
|
||
sampleRate: sampleRate,
|
||
baseFrequency: baseFrequency,
|
||
amplitude: float32(amplitude),
|
||
}, nil
|
||
}
|
||
|
||
func (g *SineGenerator) Generate(
|
||
channel uint,
|
||
firstSample uint64,
|
||
fragments ...[]byte,
|
||
) error {
|
||
if channel >= g.channels {
|
||
return fmt.Errorf(
|
||
"audio channel %d is out of range [0, %d)",
|
||
channel,
|
||
g.channels,
|
||
)
|
||
}
|
||
|
||
frequency := float64(channel+1) * g.baseFrequency
|
||
sampleIndex := firstSample
|
||
|
||
for fragmentIndex, fragment := range fragments {
|
||
if len(fragment)%4 != 0 {
|
||
return fmt.Errorf(
|
||
"audio fragment %d has %d bytes; float32 data requires a multiple of 4",
|
||
fragmentIndex,
|
||
len(fragment),
|
||
)
|
||
}
|
||
}
|
||
|
||
for _, fragment := range fragments {
|
||
for offset := 0; offset < len(fragment); offset += 4 {
|
||
phase := 2 * math.Pi *
|
||
frequency *
|
||
float64(sampleIndex) /
|
||
g.sampleRate
|
||
|
||
sample := g.amplitude * float32(math.Sin(phase))
|
||
|
||
binary.LittleEndian.PutUint32(
|
||
fragment[offset:offset+4],
|
||
math.Float32bits(sample),
|
||
)
|
||
sampleIndex++
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|