Files
KidsKeyboard/internal/audio/manager.go
2026-05-22 13:00:28 +02:00

131 lines
2.6 KiB
Go

package audio
import (
"bytes"
"encoding/binary"
"io"
"math"
"os"
"time"
ebiaudio "github.com/hajimehoshi/ebiten/v2/audio"
"github.com/hajimehoshi/ebiten/v2/audio/wav"
)
const sampleRate = 44100
type Manager struct {
context *ebiaudio.Context
players []*ebiaudio.Player
current *ebiaudio.Player
}
func NewManager() *Manager {
return &Manager{context: ebiaudio.NewContext(sampleRate)}
}
func (m *Manager) Update() {
alive := m.players[:0]
for _, p := range m.players {
if p.IsPlaying() {
alive = append(alive, p)
continue
}
_ = p.Close()
}
m.players = alive
if m.current != nil && !m.current.IsPlaying() {
_ = m.current.Close()
m.current = nil
}
}
func (m *Manager) PlayTone(freq float64, d time.Duration, volume float64) {
m.playPCM(generateTone(freq, d, volume), false)
}
func (m *Manager) PlayToneInterrupt(freq float64, d time.Duration, volume float64) {
m.playPCM(generateTone(freq, d, volume), true)
}
func (m *Manager) PlayBeep() {
m.PlayTone(880, 80*time.Millisecond, 0.22)
}
func (m *Manager) PlayError() {
data := append(generateTone(160, 110*time.Millisecond, 0.28), generateTone(110, 150*time.Millisecond, 0.28)...)
m.playPCM(data, false)
}
func (m *Manager) PlayWAV(path string, interrupt bool) bool {
f, err := os.Open(path)
if err != nil {
return false
}
defer f.Close()
stream, err := wav.DecodeWithSampleRate(sampleRate, f)
if err != nil {
return false
}
data, err := io.ReadAll(stream)
if err != nil {
return false
}
m.playPCM(data, interrupt)
return true
}
func (m *Manager) StopCurrent() {
if m.current == nil {
return
}
m.current.Pause()
_ = m.current.Close()
m.current = nil
}
func (m *Manager) playPCM(data []byte, interrupt bool) {
if len(data) == 0 {
return
}
if interrupt {
m.StopCurrent()
}
player := m.context.NewPlayerFromBytes(data)
player.Play()
if interrupt {
m.current = player
return
}
m.players = append(m.players, player)
}
func generateTone(freq float64, d time.Duration, volume float64) []byte {
samples := int(float64(sampleRate) * d.Seconds())
buf := bytes.NewBuffer(make([]byte, 0, samples*4))
for i := 0; i < samples; i++ {
t := float64(i) / sampleRate
env := envelope(i, samples)
v := int16(math.Sin(2*math.Pi*freq*t) * volume * env * math.MaxInt16)
_ = binary.Write(buf, binary.LittleEndian, v)
_ = binary.Write(buf, binary.LittleEndian, v)
}
return buf.Bytes()
}
func envelope(i, samples int) float64 {
if samples <= 0 {
return 0
}
attack := sampleRate / 200
release := sampleRate / 100
if i < attack {
return float64(i) / float64(attack)
}
if samples-i < release {
return float64(samples-i) / float64(release)
}
return 1
}