- 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>
136 lines
3.8 KiB
Odin
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])
|
|
}
|