package game import ( "bytes" "fmt" "hash/fnv" "io" "math" "github.com/hajimehoshi/ebiten/v2/audio" "github.com/hajimehoshi/ebiten/v2/audio/wav" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res" ) // Audio spelar effekter och en bakgrundsslinga, med volymer i tre lager: // huvudvolym × effekter respektive musik/sorl. nil-säkert: utan ljud // (−mute eller fel) är alla anrop no-ops. type Audio struct { ctx *audio.Context clips map[string][]byte // avkodad PCM (16-bit stereo LE) per klipp loop *audio.Player // pågående bakgrundsslinga loopName string loopBase float64 // slingans egen volym innan inställningarna master, effects, music float64 // 0..1 från inställningarna } const sampleRate = 44100 // Ljudklipp (filnamn utan .wav i resources/sound). const ( SoundApproach = "melody-approach" // ankomstjingel: tåget är på väg in SoundAnnounce = "announce" // ding-dong när tåget rullar in SoundDoors = "door-chime" // dörrarna stängs SoundChatter = "chatter" // stationssorl (slinga) SoundMusicMap = "music-map" // bankartans musik (slinga) SoundGateBeep = "gate-beep" SoundGateDeny = "gate-deny" SoundKiosk = "kiosk" SoundToilet = "toilet" SoundLearn = "learn" SoundConfirm = "confirm" SoundCancel = "cancel" SoundTransfer = "transfer" SoundWin = "win" SoundLose = "lose" SoundStaff = "staff" SoundPlane = "plane" SoundBusHorn = "bus-horn" ) // StationMelodies är avgångsmelodierna; varje station har sin (level.Melody // eller en vald efter banans id). var StationMelodies = []string{"melody-a", "melody-b", "melody-c", "melody-d"} // AllSounds är alla klipp som måste finnas i resources/sound. func AllSounds() []string { return append([]string{SoundApproach, SoundAnnounce, SoundDoors, SoundChatter, SoundMusicMap, SoundGateBeep, SoundGateDeny, SoundKiosk, SoundToilet, SoundLearn, SoundConfirm, SoundCancel, SoundTransfer, SoundWin, SoundLose, SoundStaff, SoundPlane, SoundBusHorn}, StationMelodies...) } // MelodyFor är stationens avgångsmelodi: banans melody-fält, annars en som // väljs deterministiskt av banans id. func MelodyFor(l *level.Level) string { if l.Melody != "" { return "melody-" + l.Melody } h := fnv.New32a() h.Write([]byte(l.Station + l.ID)) return StationMelodies[int(h.Sum32()%uint32(len(StationMelodies)))] } // AudibleTiles är hur många rutor bort man hör perrongens ljud. const AudibleTiles = 12.0 // newAudio laddar klippen. Returnerar nil om ljudet är avstängt. func newAudio(mute bool) (*Audio, error) { if mute { return nil, nil } a := &Audio{ctx: audio.NewContext(sampleRate), clips: map[string][]byte{}, master: 1, effects: 1, music: 1} for _, name := range AllSounds() { pcm, err := decodeSound(name) if err != nil { return nil, err } a.clips[name] = pcm } return a, nil } // decodeSound läser och avkodar ett klipp till PCM. func decodeSound(name string) ([]byte, error) { b, err := res.ReadFile("sound", name+".wav") if err != nil { return nil, err } stream, err := wav.DecodeWithoutResampling(bytes.NewReader(b)) if err != nil { return nil, fmt.Errorf("ljud %s: %w", name, err) } return io.ReadAll(stream) } // SetVolumes sätter lagren (0..1) och justerar en pågående slinga. func (a *Audio) SetVolumes(master, effects, music float64) { if a == nil { return } a.master, a.effects, a.music = clamp01(master), clamp01(effects), clamp01(music) if a.loop != nil { a.loop.SetVolume(clamp01(a.loopBase * a.music * a.master)) } } func clamp01(v float64) float64 { return math.Max(0, math.Min(1, v)) } // Play spelar ett klipp med volym 0..1 (gånger effekt- och huvudvolym). func (a *Audio) Play(name string, volume float64) { if a == nil || volume <= 0 { return } pcm, ok := a.clips[name] if !ok { return } v := clamp01(volume * a.effects * a.master) if v <= 0 { return } p := a.ctx.NewPlayerFromBytes(pcm) p.SetVolume(v) p.Play() } // Loop startar (eller byter) bakgrundsslingan med given volym; 0 stoppar. // Samma klipp som redan spelas fortsätter bara med ny volym. func (a *Audio) Loop(name string, volume float64) { if a == nil { return } if volume <= 0 { a.StopLoop() return } if a.loop != nil && a.loopName == name { a.loopBase = volume a.loop.SetVolume(clamp01(volume * a.music * a.master)) return } a.StopLoop() pcm, ok := a.clips[name] if !ok { return } stream := audio.NewInfiniteLoop(bytes.NewReader(pcm), int64(len(pcm))) p, err := a.ctx.NewPlayer(stream) if err != nil { return } p.SetVolume(clamp01(volume * a.music * a.master)) p.Play() a.loop, a.loopName, a.loopBase = p, name, volume } // StopLoop stoppar bakgrundsslingan. func (a *Audio) StopLoop() { if a == nil || a.loop == nil { return } a.loop.Close() a.loop, a.loopName = nil, "" } // volumeAt ger volym efter avstånd i rutor. func volumeAt(dist float64) float64 { return math.Max(0, 1-dist/AudibleTiles) }