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 }