Files
go-mxl-pattern-generator/internal/audio/generator.go
T
2026-09-16 22:44:27 +03:00

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
}