// 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) } }