Files
HanjaKanaMun/src/audio/audio.odin
T
SansanandClaude Opus 5.5 5806f0498c Restructure into themed screens with flag card backs, real font, flip animation and sound
- Replace the bogus DroidSansJapanese.ttf (an HTML page) with M PLUS Rounded 1c,
  loaded with LoadFontEx and explicit kana codepoints
- Theme struct per language with flag-based card backs (Hinomaru, Taegukgi, Trairong)
- Card art rendered to textures so the flip squashes the whole card
- Synthesized flip/match/mismatch/win/click sounds (new audio package)
- Shared ui package for text and buttons; Screen enum replaces i32 ids
- Kana <-> romaji pairs drawn from all 46 basic hiragana

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:47:04 +02:00

136 lines
3.8 KiB
Odin

package audio
import "core:math"
import "core:math/rand"
import raylib "vendor:raylib"
// All sounds are synthesized at startup, so no audio files are needed
SAMPLE_RATE :: 44100
Sound_Effect :: enum {
FLIP,
MATCH,
MISMATCH,
WIN,
CLICK,
}
sounds: [Sound_Effect]raylib.Sound
ready: bool
@(private)
Tone :: struct {
start: f32, // seconds
freq: f32, // Hz
duration: f32, // seconds
amp: f32,
}
// Sine with a little 2nd/3rd harmonic, soft attack and exponential decay: a small bell
@(private)
add_tone :: proc(samples: []f32, tone: Tone) {
first := int(tone.start * SAMPLE_RATE)
count := int(tone.duration * SAMPLE_RATE)
attack: f32 = 0.006
for i in 0 ..< count {
idx := first + i
if idx >= len(samples) { break }
t := f32(i) / SAMPLE_RATE
env := min(t / attack, 1) * math.exp(-5 * t / tone.duration)
phase := 2 * math.PI * tone.freq * t
s := math.sin(phase) + 0.25 * math.sin(2 * phase) + 0.08 * math.sin(3 * phase)
samples[idx] += tone.amp * env * s
}
}
// Card "snap": low-passed noise with a fast decay over a short low thump
@(private)
add_flip :: proc(samples: []f32) {
lp: f32 = 0
for i in 0 ..< len(samples) {
t := f32(i) / SAMPLE_RATE
noise := rand.float32_range(-1, 1)
lp += 0.35 * (noise - lp)
swish := lp * math.exp(-t * 55) * min(t / 0.004, 1)
thump := math.sin(2 * math.PI * 150 * t) * math.exp(-t * 70)
samples[i] += 0.55 * swish + 0.35 * thump
}
}
@(private)
make_sound :: proc(samples: []f32) -> raylib.Sound {
pcm := make([]i16, len(samples))
defer delete(pcm)
for s, i in samples {
pcm[i] = i16(math.clamp(s, -1, 1) * 32000)
}
wave := raylib.Wave{
frameCount = u32(len(pcm)),
sampleRate = SAMPLE_RATE,
sampleSize = 16,
channels = 1,
data = raw_data(pcm),
}
// LoadSoundFromWave copies the samples into its own buffer
return raylib.LoadSoundFromWave(wave)
}
@(private)
build :: proc(duration: f32, tones: []Tone, flip := false) -> raylib.Sound {
samples := make([]f32, int(duration * SAMPLE_RATE))
defer delete(samples)
if flip { add_flip(samples) }
for tone in tones { add_tone(samples, tone) }
return make_sound(samples)
}
// Call after raylib.InitAudioDevice
load :: proc() {
if !raylib.IsAudioDeviceReady() { return }
E5 :: 659.25
A5 :: 880.00
B5 :: 987.77
C5 :: 523.25
G5 :: 783.99
C6 :: 1046.50
E6 :: 1318.51
sounds[.FLIP] = build(0.12, {}, flip = true)
sounds[.MATCH] = build(0.6, {
{start = 0.00, freq = E5, duration = 0.35, amp = 0.30},
{start = 0.09, freq = B5, duration = 0.50, amp = 0.28},
})
sounds[.MISMATCH] = build(0.45, {
{start = 0.00, freq = 330, duration = 0.20, amp = 0.20},
{start = 0.11, freq = 262, duration = 0.30, amp = 0.20},
})
sounds[.WIN] = build(1.3, {
{start = 0.00, freq = C5, duration = 0.40, amp = 0.18},
{start = 0.11, freq = E5, duration = 0.40, amp = 0.18},
{start = 0.22, freq = G5, duration = 0.40, amp = 0.18},
{start = 0.33, freq = C6, duration = 0.90, amp = 0.2},
{start = 0.33, freq = E6, duration = 0.90, amp = 0.07},
})
sounds[.CLICK] = build(0.08, {{start = 0, freq = A5, duration = 0.06, amp = 0.15}})
raylib.SetSoundVolume(sounds[.FLIP], 0.7)
ready = true
}
unload :: proc() {
if !ready { return }
for s in sounds { raylib.UnloadSound(s) }
ready = false
}
play :: proc(effect: Sound_Effect) {
if !ready { return }
if effect == .FLIP {
// Slight pitch variation so repeated flips don't sound identical
raylib.SetSoundPitch(sounds[.FLIP], rand.float32_range(0.9, 1.12))
}
raylib.PlaySound(sounds[effect])
}