Files
muse/lib/audio/generator.go

130 lines
2.8 KiB
Go

package audio
import (
"log"
"math"
"sync"
"git.atri.dad/atridad/muse/schema"
)
const (
SampleRate = 44100
Bits = 16
Channels = 2
)
// Handles audio synthesis
type Generator struct {
sampleRate float64
bufferPool sync.Pool
}
// Creates a new audio generator
func NewGenerator() *Generator {
return &Generator{
sampleRate: float64(SampleRate),
bufferPool: sync.Pool{
New: func() interface{} {
return make([]float64, 0, 4096)
},
},
}
}
// Renders the complete song to audio samples
func (g *Generator) Generate(song schema.Song) ([]float64, error) {
samplesPerBeat := (60.0 / float64(song.BPM)) * g.sampleRate
log.Printf("Generating song at %.1f BPM, sample rate: %.0f, samples per beat: %.1f\n",
float64(song.BPM), g.sampleRate, samplesPerBeat)
totalBeats := song.CalculateTotalLength()
log.Printf("Total song length: %.1f beats", totalBeats)
totalSamples := int(float64(totalBeats) * samplesPerBeat)
buffer := make([]float64, totalSamples*Channels)
if totalSamples == 0 {
log.Printf("Warning: Zero samples calculated for song")
}
var wg sync.WaitGroup
var mu sync.Mutex
for _, track := range song.Tracks {
wg.Add(1)
go func(track schema.Track) {
defer wg.Done()
patternLength := len(track.Pattern)
if patternLength == 0 {
return
}
for beat := 0; beat < int(totalBeats); beat++ {
var note string
if track.Loop {
patternIndex := beat % patternLength
note = track.Pattern[patternIndex]
} else {
if beat < patternLength {
note = track.Pattern[beat]
} else {
note = "."
}
}
if note == "." || note == "" {
continue
}
startSample := int(float64(beat) * samplesPerBeat)
endSample := startSample + int(samplesPerBeat)
if startSample >= len(buffer)/Channels {
continue
}
if endSample > len(buffer)/Channels {
endSample = len(buffer) / Channels
}
freq := noteToFreq(note)
amplitude := 0.5
duration := float64(endSample-startSample) / g.sampleRate
audioData := g.generateOscillator(track.Instrument, freq, duration, amplitude*track.Volume, 0)
if len(audioData) == 0 {
continue
}
if len(track.Effects) > 0 {
effectChain := CreateEffectChain(track.Effects, g.sampleRate)
audioData = effectChain.Process(audioData)
}
pan := (track.Pan + 1) / 2
leftGain := math.Sqrt(1.0 - pan)
rightGain := math.Sqrt(pan)
mu.Lock()
for i, sample := range audioData {
if startSample+i >= len(buffer)/Channels {
break
}
leftSample := sample * leftGain
rightSample := sample * rightGain
buffer[(startSample+i)*Channels] += leftSample
buffer[(startSample+i)*Channels+1] += rightSample
}
mu.Unlock()
}
}(track)
}
wg.Wait()
return buffer, nil
}