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 }