0.1.0 - Initial commit
This commit is contained in:
129
lib/audio/generator.go
Normal file
129
lib/audio/generator.go
Normal file
@@ -0,0 +1,129 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user