-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsfx_gen.go
More file actions
177 lines (160 loc) · 4.13 KB
/
sfx_gen.go
File metadata and controls
177 lines (160 loc) · 4.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package main
import (
"bytes"
"math"
"math/rand"
"sync"
)
// renderSamples pre-renders stereo int16LE samples into a bytes.Reader.
func renderSamples(samples []int16) *bytes.Reader {
buf := make([]byte, len(samples)*2*channels)
for i, s := range samples {
off := i * channels * bitDepth
// Left
buf[off] = byte(s)
buf[off+1] = byte(s >> 8)
// Right
buf[off+2] = byte(s)
buf[off+3] = byte(s >> 8)
}
return bytes.NewReader(buf)
}
// newCardFlipSound generates a soft ~180ms thud: gentle filtered noise with a muted tone.
func newCardFlipSound() *bytes.Reader {
n := int(0.18 * sampleRate)
samples := make([]int16, n)
var prevOut float64
for i := range samples {
t := float64(i) / float64(n)
// Smooth fade-in then long decay — avoids the harsh snap
var env float64
if t < 0.05 {
env = t / 0.05
} else {
env = 1.0 - (t-0.05)/0.95
}
env *= env // gentle curve
noise := rand.Float64()*2 - 1
// Low-pass filter the noise for a softer texture
alpha := 0.08
prevOut = prevOut*(1-alpha) + noise*alpha
freq := 400.0 - 200.0*t
tSec := float64(i) / float64(sampleRate)
tone := math.Sin(2 * math.Pi * freq * tSec)
val := (prevOut*0.5 + tone*0.5) * env * 0.35
samples[i] = int16(val * 32767)
}
return renderSamples(samples)
}
// newPaperSlideSound generates a ~300ms gentle filtered noise swoosh.
func newPaperSlideSound() *bytes.Reader {
n := int(0.30 * sampleRate)
samples := make([]int16, n)
var prevOut float64
for i := range samples {
t := float64(i) / float64(n)
// Softer envelope with gradual ramps
var env float64
switch {
case t < 0.3:
env = t / 0.3
case t < 0.5:
env = 1.0
default:
env = (1.0 - t) / 0.5
}
noise := rand.Float64()*2 - 1
alpha := 0.08 // heavier filtering for softer sound
prevOut = prevOut*(1-alpha) + noise*alpha
val := prevOut * env * 0.3
samples[i] = int16(val * 32767)
}
return renderSamples(samples)
}
// newWordChimeSound generates a ~35ms soft tonal tick tuned to a harmonic of the given root.
func newWordChimeSound(root float64) *bytes.Reader {
n := int(0.035 * sampleRate)
samples := make([]int16, n)
// Target ~500-900 Hz for a soft, high tick.
harm := math.Ceil(500.0 / root)
if rand.Intn(4) == 0 {
harm += 1.0 // occasional higher ping for variety
}
freq := root * harm * (1.0 + (rand.Float64()-0.5)*0.01) // +/- 0.5% detune
for i := range samples {
t := float64(i) / float64(n)
// Fast attack (3ms), steep exponential decay
var env float64
attackT := 0.08
if t < attackT {
env = t / attackT
} else {
env = math.Exp(-10.0 * (t - attackT))
}
tSec := float64(i) / float64(sampleRate)
val := math.Sin(2*math.Pi*freq*tSec) * env * 0.04
samples[i] = int16(val * 32767)
}
return renderSamples(samples)
}
// sfxBytes pre-renders an SFX into a byte slice for repeated playback.
func sfxBytes(name string) []byte {
var r *bytes.Reader
switch name {
case "card-flip":
r = newCardFlipSound()
case "paper-slide":
r = newPaperSlideSound()
default:
return nil
}
buf := make([]byte, r.Len())
_, _ = r.Read(buf)
return buf
}
// sfxCache holds pre-rendered SFX byte slices so we only generate once.
var sfxCache struct {
once [2]bool
buffers [2][]byte
}
func cachedSFX(name string) []byte {
idx := -1
switch name {
case "card-flip":
idx = 0
case "paper-slide":
idx = 1
}
if idx < 0 {
return nil
}
if !sfxCache.once[idx] {
sfxCache.buffers[idx] = sfxBytes(name)
sfxCache.once[idx] = true
}
return sfxCache.buffers[idx]
}
// chimeCache holds pre-rendered word chime bytes keyed by deck name.
var chimeCache struct {
mu sync.Mutex
data map[string][]byte
}
// cachedWordChime returns a pre-rendered chime for the given deck.
// A new random chime is generated on first call per deck.
func cachedWordChime(deck string) []byte {
chimeCache.mu.Lock()
defer chimeCache.mu.Unlock()
if chimeCache.data == nil {
chimeCache.data = make(map[string][]byte)
}
if buf, ok := chimeCache.data[deck]; ok {
return buf
}
voice := voiceForDeck(deck)
root := voice.roots[rand.Intn(len(voice.roots))]
r := newWordChimeSound(root)
buf := make([]byte, r.Len())
_, _ = r.Read(buf)
chimeCache.data[deck] = buf
return buf
}