All checks were successful
release / build-release (push) Successful in 1m33s
Folk på stationen (NPC:er som vandrar, kliver av tåg och går mot utgången, blockerar spelaren men inte varandra), vänster-/högerfiler som bromsar på fel sida, bredare gångar, reklamaffischer (clutter), japanskt sorl i loop (noise), vagnval med kvinnovagn (stort avdrag med man i sällskapet, tidsstyrd), kömarkeringar på perrongen, trängsel per vagn (bonus för tom vagn, svettig-debuff och svårare resa i full vagn, släpp förbi passagerare) och att folk kliver av innan man får gå på. Ny bana 005 Ueno. Skärmdumpsverktyget kan filtrera på bana/skript. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
141 lines
3.8 KiB
Go
141 lines
3.8 KiB
Go
// Genererar placeholder-ljud tills riktiga inspelningar finns:
|
||
//
|
||
// go run ./tools/gensound
|
||
//
|
||
// Skriver resources/sound/melody-approach.wav (en kort pentatonisk
|
||
// avgångsmelodi i japansk stationsstil, spelas när tåget är på väg in) och
|
||
// resources/sound/door-chime.wav (dörrsignal strax före avgång). 16-bit
|
||
// mono 44,1 kHz, syntetiserade med enkla vågformer så att allt är
|
||
// deterministiskt.
|
||
package main
|
||
|
||
import (
|
||
"encoding/binary"
|
||
"fmt"
|
||
"math"
|
||
"os"
|
||
"path/filepath"
|
||
)
|
||
|
||
const rate = 44100
|
||
|
||
type note struct {
|
||
freq float64 // Hz, 0 = paus
|
||
dur float64 // sekunder
|
||
}
|
||
|
||
// Pentatonisk melodi (C-dur-pentatonik), två takter.
|
||
var approachMelody = []note{
|
||
{523.25, 0.18}, {587.33, 0.18}, {659.25, 0.18}, {783.99, 0.36},
|
||
{880.00, 0.18}, {783.99, 0.18}, {659.25, 0.18}, {523.25, 0.36},
|
||
{0, 0.12},
|
||
{659.25, 0.18}, {783.99, 0.18}, {880.00, 0.18}, {1046.50, 0.54},
|
||
}
|
||
|
||
// Dörrsignal: två toner växelvis.
|
||
var doorChime = []note{
|
||
{1318.51, 0.14}, {1046.50, 0.14}, {1318.51, 0.14}, {1046.50, 0.14},
|
||
{1318.51, 0.14}, {1046.50, 0.14}, {1318.51, 0.14}, {1046.50, 0.28},
|
||
}
|
||
|
||
func render(notes []note, attack, release float64, overtone float64) []int16 {
|
||
var out []int16
|
||
for _, n := range notes {
|
||
samples := int(n.dur * rate)
|
||
for i := 0; i < samples; i++ {
|
||
if n.freq == 0 {
|
||
out = append(out, 0)
|
||
continue
|
||
}
|
||
t := float64(i) / rate
|
||
env := 1.0
|
||
if t < attack {
|
||
env = t / attack
|
||
}
|
||
if rem := n.dur - t; rem < release {
|
||
env *= rem / release
|
||
}
|
||
v := math.Sin(2*math.Pi*n.freq*t) + overtone*math.Sin(2*math.Pi*2*n.freq*t)
|
||
v = v / (1 + overtone) * env * 0.5
|
||
out = append(out, int16(v*32767))
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func writeWAV(path string, pcm []int16) error {
|
||
f, err := os.Create(path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer f.Close()
|
||
dataLen := uint32(len(pcm) * 2)
|
||
hdr := []any{
|
||
[]byte("RIFF"), uint32(36 + dataLen), []byte("WAVE"),
|
||
[]byte("fmt "), uint32(16), uint16(1), uint16(1), uint32(rate), uint32(rate * 2), uint16(2), uint16(16),
|
||
[]byte("data"), dataLen,
|
||
}
|
||
for _, h := range hdr {
|
||
if err := binary.Write(f, binary.LittleEndian, h); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return binary.Write(f, binary.LittleEndian, pcm)
|
||
}
|
||
|
||
// chatter syntetiserar sex sekunder loopbart stationssorl: lågpassat brus
|
||
// med stavelseliknande amplitudmodulation från en deterministisk LCG.
|
||
func chatter(seconds float64) []int16 {
|
||
n := int(seconds * rate)
|
||
out := make([]int16, n)
|
||
var lcg uint32 = 12345
|
||
next := func() float64 { lcg = lcg*1664525 + 1013904223; return float64(lcg>>8)/float64(1<<24)*2 - 1 }
|
||
lp := 0.0
|
||
env, envTarget, envT := 0.3, 0.3, 0.0
|
||
for i := 0; i < n; i++ {
|
||
t := float64(i) / rate
|
||
// ny "stavelse" var 120–250 ms
|
||
if t >= envT {
|
||
envTarget = 0.1 + 0.6*math.Abs(next())
|
||
envT = t + 0.12 + 0.13*math.Abs(next())
|
||
}
|
||
env += (envTarget - env) * 0.002
|
||
lp += (next() - lp) * 0.08 // lågpass ≈ 600 Hz
|
||
v := lp * env * 0.6
|
||
// mjuk in-/utfasning för sömlös loop
|
||
fade := math.Min(1, math.Min(t, seconds-t)/0.2)
|
||
out[i] = int16(v * fade * 32767)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func main() {
|
||
out := filepath.Join(repoRoot(), "resources", "sound")
|
||
must(os.MkdirAll(out, 0o755))
|
||
must(writeWAV(filepath.Join(out, "melody-approach.wav"), render(approachMelody, 0.01, 0.06, 0.3)))
|
||
must(writeWAV(filepath.Join(out, "door-chime.wav"), render(doorChime, 0.005, 0.04, 0.1)))
|
||
must(writeWAV(filepath.Join(out, "chatter.wav"), chatter(6)))
|
||
fmt.Println("skrev", filepath.Join(out, "melody-approach.wav"), ", door-chime.wav och chatter.wav")
|
||
}
|
||
|
||
func repoRoot() string {
|
||
dir, _ := os.Getwd()
|
||
for {
|
||
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||
return dir
|
||
}
|
||
parent := filepath.Dir(dir)
|
||
if parent == dir {
|
||
must(fmt.Errorf("hittade ingen go.mod"))
|
||
}
|
||
dir = parent
|
||
}
|
||
}
|
||
|
||
func must(err error) {
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "gensound:", err)
|
||
os.Exit(1)
|
||
}
|
||
}
|