Ariver
2026-07-03 5fbe9789faf05ce99bbff6ec4cd0b905519acbd0
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
178
179
180
181
182
package sound
 
import (
    "encoding/binary"
    "math"
    "sync"
    "syscall"
    "unsafe"
)
 
var (
    winmm        = syscall.NewLazyDLL("winmm.dll")
    playSoundW   = winmm.NewProc("PlaySoundW")
    sndMemory    uintptr = 0x0004
    sndAsync     uintptr = 0x0001
    sndNoDefault uintptr = 0x0002
 
    startWAV  []byte
    doneWAV   []byte
    cancelWAV []byte
    once      sync.Once
)
 
const (
    sRate    = 48000
    bitDepth = 16
    nCh      = 1
)
 
func init() {
    once.Do(func() {
        // Ascending "deng-deng ↑"
        startWAV = genMalletPair(330.0, 440.0, 0.13, 0.28, 0.015)
        // Descending "deng-deng ↓"
        doneWAV = genMalletPair(330.0, 247.0, 0.13, 0.28, 0.015)
        // Cancel: single low soft tap
        cancelWAV = genMalletSingle(247.0, 0.20)
    })
}
 
func PlayStart()  { go playWAV(startWAV) }
func PlayDone()   { go playWAV(doneWAV) }
func PlayCancel() { go playWAV(cancelWAV) }
 
func playWAV(data []byte) {
    if len(data) == 0 {
        return
    }
    playSoundW.Call(
        uintptr(unsafe.Pointer(&data[0])),
        0,
        sndMemory|sndAsync|sndNoDefault,
    )
}
 
// genMalletPair generates two mallet-like tones (like a marimba) with a gap.
// Mimics the Typeless "deng-deng" pattern:
//   - 20ms leading silence
//   - Tone 1: fast attack (3ms) + exponential decay over dur1
//   - gap of silence
//   - Tone 2: fast attack (3ms) + exponential decay over dur2 (longer tail)
//   - 20ms trailing silence
func genMalletPair(freq1, freq2, dur1, dur2, gap float64) []byte {
    leadSilence := int(0.02 * sRate)
    n1 := int(dur1 * sRate)
    nGap := int(gap * sRate)
    n2 := int(dur2 * sRate)
    trailSilence := int(0.02 * sRate)
    total := leadSilence + n1 + nGap + n2 + trailSilence
 
    samples := make([]int16, total)
 
    // Tone 1
    off := leadSilence
    for i := 0; i < n1; i++ {
        t := float64(i) / sRate
        env := malletEnvelope(i, n1, 6.0) // faster decay
        val := malletOsc(freq1, t, env)
        samples[off+i] = int16(clamp(val*32767, -32767, 32767))
    }
 
    // Tone 2
    off = leadSilence + n1 + nGap
    for i := 0; i < n2; i++ {
        t := float64(i) / sRate
        env := malletEnvelope(i, n2, 4.5) // slower decay, longer tail
        val := malletOsc(freq2, t, env)
        samples[off+i] = int16(clamp(val*32767, -32767, 32767))
    }
 
    return buildWAV(samples)
}
 
// genMalletSingle generates a single mallet tap.
func genMalletSingle(freq, dur float64) []byte {
    lead := int(0.02 * sRate)
    n := int(dur * sRate)
    trail := int(0.02 * sRate)
    total := lead + n + trail
 
    samples := make([]int16, total)
    for i := 0; i < n; i++ {
        t := float64(i) / sRate
        env := malletEnvelope(i, n, 5.0)
        val := malletOsc(freq, t, env) * 0.8
        samples[lead+i] = int16(clamp(val*32767, -32767, 32767))
    }
    return buildWAV(samples)
}
 
// malletOsc produces a marimba-like oscillator: fundamental + 2nd harmonic + 3rd harmonic.
// Volume is low (~12% of full scale).
func malletOsc(freq, t, env float64) float64 {
    fundamental := math.Sin(2 * math.Pi * freq * t)
    harmonic2 := math.Sin(2*math.Pi*freq*2*t) * 0.15
    harmonic3 := math.Sin(2*math.Pi*freq*3*t) * 0.05
    return (fundamental + harmonic2 + harmonic3) * env * 0.10
}
 
// malletEnvelope: 3ms attack, then exponential decay.
// decayRate controls how fast it fades (higher = faster).
func malletEnvelope(i, total int, decayRate float64) float64 {
    attackSamples := sRate * 3 / 1000 // 3ms
    t := float64(i) / sRate
 
    var env float64
    if i < attackSamples {
        // Fast smooth attack (sine curve for no click)
        env = math.Sin(math.Pi / 2 * float64(i) / float64(attackSamples))
    } else {
        // Exponential decay from peak
        decayT := t - float64(attackSamples)/sRate
        env = math.Exp(-decayRate * decayT)
    }
 
    // Gentle fade-out at the very end to avoid any click
    fadeOut := sRate * 5 / 1000 // 5ms
    if i > total-fadeOut {
        remaining := float64(total - i)
        env *= remaining / float64(fadeOut)
    }
 
    return env
}
 
func clamp(v, lo, hi float64) float64 {
    if v < lo {
        return lo
    }
    if v > hi {
        return hi
    }
    return v
}
 
func buildWAV(samples []int16) []byte {
    dataSize := len(samples) * 2
    fileSize := 44 + dataSize
 
    buf := make([]byte, fileSize)
    copy(buf[0:4], "RIFF")
    binary.LittleEndian.PutUint32(buf[4:8], uint32(fileSize-8))
    copy(buf[8:12], "WAVE")
 
    copy(buf[12:16], "fmt ")
    binary.LittleEndian.PutUint32(buf[16:20], 16)
    binary.LittleEndian.PutUint16(buf[20:22], 1)
    binary.LittleEndian.PutUint16(buf[22:24], nCh)
    binary.LittleEndian.PutUint32(buf[24:28], sRate)
    binary.LittleEndian.PutUint32(buf[28:32], sRate*nCh*bitDepth/8)
    binary.LittleEndian.PutUint16(buf[32:34], nCh*bitDepth/8)
    binary.LittleEndian.PutUint16(buf[34:36], bitDepth)
 
    copy(buf[36:40], "data")
    binary.LittleEndian.PutUint32(buf[40:44], uint32(dataSize))
 
    for i, s := range samples {
        binary.LittleEndian.PutUint16(buf[44+i*2:44+i*2+2], uint16(s))
    }
 
    return buf
}