123 lines
2.8 KiB
Go
123 lines
2.8 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)
|
|
}
|
|
|
|
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
|
|
}
|