Add sound effects and Void Hunt minigame
- Add Web Audio API sound engine with procedural sounds - Add sounds for: pet, feed, spawn, achievement, purchase, release - Add mute toggle button - Add Void Hunt minigame (30s click target game) - Award 5 Void Essence per target caught - Persist mute state to localStorage
This commit is contained in:
@ -48,10 +48,14 @@ A whimsical pocket dimension where digital companions called "Voidlings" float a
|
|||||||
- Added 2 new voidling types: Glitch (rare green) and Crystal (rare blue)
|
- Added 2 new voidling types: Glitch (rare green) and Crystal (rare blue)
|
||||||
- Added "Void Tycoon" achievement for accumulating 500 Void Essence
|
- Added "Void Tycoon" achievement for accumulating 500 Void Essence
|
||||||
- Updated biodiversity achievement to require all 6 voidling types
|
- Updated biodiversity achievement to require all 6 voidling types
|
||||||
|
- Added sound effects system using Web Audio API (pet, feed, spawn, achievement, purchase, release)
|
||||||
|
- Added mute toggle button to control sounds
|
||||||
|
- Added Void Hunt minigame - click targets for 30 seconds to earn Void Essence
|
||||||
|
|
||||||
### Next Steps
|
### Next Steps
|
||||||
1. Add sound effects
|
1. Add more minigames
|
||||||
2. Add voidling minigames
|
2. Add voidling evolution/combat system
|
||||||
|
3. Add decorations for the chamber
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
import { soundEngine } from '../utils/sound'
|
||||||
|
|
||||||
interface Voidling {
|
interface Voidling {
|
||||||
id: string
|
id: string
|
||||||
@ -215,6 +216,54 @@ export default function Chamber() {
|
|||||||
const [isAutoCycle, setIsAutoCycle] = useState(true)
|
const [isAutoCycle, setIsAutoCycle] = useState(true)
|
||||||
const [cycleProgress, setCycleProgress] = useState(0)
|
const [cycleProgress, setCycleProgress] = useState(0)
|
||||||
const [showShop, setShowShop] = useState(false)
|
const [showShop, setShowShop] = useState(false)
|
||||||
|
const [isMuted, setIsMuted] = useState(() => {
|
||||||
|
const saved = localStorage.getItem('voidling_muted')
|
||||||
|
return saved === 'true'
|
||||||
|
})
|
||||||
|
const [showGames, setShowGames] = useState(false)
|
||||||
|
const [gameActive, setGameActive] = useState(false)
|
||||||
|
const [gameScore, setGameScore] = useState(0)
|
||||||
|
const [gameTimeLeft, setGameTimeLeft] = useState(30)
|
||||||
|
const [gameTargets, setGameTargets] = useState<{id: number, x: number, y: number}[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
soundEngine.setMuted(isMuted)
|
||||||
|
localStorage.setItem('voidling_muted', String(isMuted))
|
||||||
|
}, [isMuted])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!gameActive) return
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setGameTimeLeft(prev => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
endGame()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return prev - 1
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [gameActive])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!gameActive) return
|
||||||
|
|
||||||
|
const spawnTarget = () => {
|
||||||
|
const newTarget = {
|
||||||
|
id: Date.now(),
|
||||||
|
x: Math.random() * 80 + 10,
|
||||||
|
y: Math.random() * 60 + 20,
|
||||||
|
}
|
||||||
|
setGameTargets(prev => [...prev, newTarget])
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnTarget()
|
||||||
|
const spawnInterval = setInterval(spawnTarget, 1500)
|
||||||
|
|
||||||
|
return () => clearInterval(spawnInterval)
|
||||||
|
}, [gameActive])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedVoidlings = localStorage.getItem('voidlings')
|
const savedVoidlings = localStorage.getItem('voidlings')
|
||||||
@ -264,6 +313,7 @@ export default function Chamber() {
|
|||||||
if (achievement.condition(voidlings, userStats)) {
|
if (achievement.condition(voidlings, userStats)) {
|
||||||
setUnlockedAchievements(prev => [...prev, achievement.id])
|
setUnlockedAchievements(prev => [...prev, achievement.id])
|
||||||
setNewAchievement(achievement)
|
setNewAchievement(achievement)
|
||||||
|
soundEngine.playAchievement()
|
||||||
setTimeout(() => setNewAchievement(null), 3000)
|
setTimeout(() => setNewAchievement(null), 3000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -398,6 +448,7 @@ export default function Chamber() {
|
|||||||
vy: pos.vy,
|
vy: pos.vy,
|
||||||
pets: 0,
|
pets: 0,
|
||||||
}
|
}
|
||||||
|
soundEngine.playSpawn()
|
||||||
setVoidlings([...voidlings, newVoidling])
|
setVoidlings([...voidlings, newVoidling])
|
||||||
setNamingVoidling(newVoidling.id)
|
setNamingVoidling(newVoidling.id)
|
||||||
setNewName('')
|
setNewName('')
|
||||||
@ -433,6 +484,7 @@ export default function Chamber() {
|
|||||||
|
|
||||||
function feedVoidling(id: string) {
|
function feedVoidling(id: string) {
|
||||||
setFeedingId(id)
|
setFeedingId(id)
|
||||||
|
soundEngine.playFeed()
|
||||||
setVoidlings(voidlings.map(v => {
|
setVoidlings(voidlings.map(v => {
|
||||||
if (v.id === id) {
|
if (v.id === id) {
|
||||||
return { ...v, mood: 'happy', vy: v.vy - 0.5 }
|
return { ...v, mood: 'happy', vy: v.vy - 0.5 }
|
||||||
@ -449,6 +501,7 @@ export default function Chamber() {
|
|||||||
|
|
||||||
function petVoidling(id: string) {
|
function petVoidling(id: string) {
|
||||||
setPettingId(id)
|
setPettingId(id)
|
||||||
|
soundEngine.playPet()
|
||||||
setVoidlings(voidlings.map(v => {
|
setVoidlings(voidlings.map(v => {
|
||||||
if (v.id === id) {
|
if (v.id === id) {
|
||||||
return { ...v, mood: 'happy', pets: (v.pets || 0) + 1 }
|
return { ...v, mood: 'happy', pets: (v.pets || 0) + 1 }
|
||||||
@ -464,12 +517,41 @@ export default function Chamber() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function releaseVoidling(id: string) {
|
function releaseVoidling(id: string) {
|
||||||
|
soundEngine.playRelease()
|
||||||
setVoidlings(voidlings.filter(v => v.id !== id))
|
setVoidlings(voidlings.filter(v => v.id !== id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
setGameActive(true)
|
||||||
|
setGameScore(0)
|
||||||
|
setGameTimeLeft(30)
|
||||||
|
setGameTargets([])
|
||||||
|
}
|
||||||
|
|
||||||
|
function endGame() {
|
||||||
|
setGameActive(false)
|
||||||
|
const reward = gameScore * 5
|
||||||
|
if (reward > 0) {
|
||||||
|
setUserStats(prev => {
|
||||||
|
const newStats = { ...prev, voidEssence: prev.voidEssence + reward }
|
||||||
|
localStorage.setItem('voidling_stats', JSON.stringify(newStats))
|
||||||
|
return newStats
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setGameTargets([])
|
||||||
|
}
|
||||||
|
|
||||||
|
function hitTarget(id: number) {
|
||||||
|
soundEngine.playPet()
|
||||||
|
setGameScore(prev => prev + 1)
|
||||||
|
setGameTargets(prev => prev.filter(t => t.id !== id))
|
||||||
|
}
|
||||||
|
|
||||||
function purchaseItem(item: ShopItem) {
|
function purchaseItem(item: ShopItem) {
|
||||||
if (userStats.voidEssence < item.cost) return
|
if (userStats.voidEssence < item.cost) return
|
||||||
|
|
||||||
|
soundEngine.playPurchase()
|
||||||
|
|
||||||
setUserStats(prev => {
|
setUserStats(prev => {
|
||||||
const newStats = { ...prev, voidEssence: prev.voidEssence - item.cost }
|
const newStats = { ...prev, voidEssence: prev.voidEssence - item.cost }
|
||||||
localStorage.setItem('voidling_stats', JSON.stringify(newStats))
|
localStorage.setItem('voidling_stats', JSON.stringify(newStats))
|
||||||
@ -603,6 +685,19 @@ export default function Chamber() {
|
|||||||
>
|
>
|
||||||
+ Summon Voidling
|
+ Summon Voidling
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsMuted(!isMuted)}
|
||||||
|
className="px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-sm transition-all duration-200 hover:scale-105 border border-zinc-700"
|
||||||
|
title={isMuted ? 'Unmute' : 'Mute'}
|
||||||
|
>
|
||||||
|
{isMuted ? '🔇' : '🔊'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowGames(true)}
|
||||||
|
className="px-4 py-2 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 rounded-xl text-sm transition-all duration-200 hover:scale-105 border border-zinc-700 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
🎮 <span className="hidden sm:inline">Games</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -800,6 +895,85 @@ export default function Chamber() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showGames && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4" onClick={() => setShowGames(false)}>
|
||||||
|
<div className="bg-zinc-900 rounded-2xl p-6 max-w-sm w-full border border-zinc-700" onClick={e => e.stopPropagation()}>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-blue-400 to-purple-400">
|
||||||
|
Void Games
|
||||||
|
</h2>
|
||||||
|
<button onClick={() => setShowGames(false)} className="text-zinc-500 hover:text-zinc-300">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-4 bg-zinc-800/50 rounded-xl border border-zinc-700">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<span className="text-3xl">🎯</span>
|
||||||
|
<div>
|
||||||
|
<div className="font-bold">Void Hunt</div>
|
||||||
|
<div className="text-xs text-zinc-500">Click the targets!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-zinc-400 mb-3">Catch as many targets as you can in 30 seconds. Each catch awards 5 Void Essence!</p>
|
||||||
|
<button
|
||||||
|
onClick={() => { setShowGames(false); startGame(); }}
|
||||||
|
className="w-full py-2 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 rounded-lg text-sm font-medium transition-all"
|
||||||
|
>
|
||||||
|
Start Game
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{gameActive && (
|
||||||
|
<div className="fixed inset-0 pointer-events-none z-40">
|
||||||
|
<div className="absolute top-4 left-1/2 -translate-x-1/2 flex gap-8 text-2xl font-bold pointer-events-auto">
|
||||||
|
<div className="bg-zinc-900/80 px-6 py-2 rounded-xl border border-zinc-700">
|
||||||
|
⏱️ {gameTimeLeft}s
|
||||||
|
</div>
|
||||||
|
<div className="bg-zinc-900/80 px-6 py-2 rounded-xl border border-zinc-700">
|
||||||
|
🎯 {gameScore}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{gameTargets.map(target => (
|
||||||
|
<button
|
||||||
|
key={target.id}
|
||||||
|
onClick={() => hitTarget(target.id)}
|
||||||
|
className="absolute w-16 h-16 bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full animate-pulse hover:scale-110 transition-transform pointer-events-auto flex items-center justify-center text-2xl shadow-lg shadow-orange-500/50"
|
||||||
|
style={{ left: `${target.x}%`, top: `${target.y}%`, transform: 'translate(-50%, -50%)' }}
|
||||||
|
>
|
||||||
|
✨
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!gameActive && gameScore > 0 && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-zinc-900 rounded-2xl p-6 max-w-sm w-full border border-zinc-700 text-center">
|
||||||
|
<div className="text-5xl mb-4">🎉</div>
|
||||||
|
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 to-orange-400 mb-2">
|
||||||
|
Game Over!
|
||||||
|
</h2>
|
||||||
|
<p className="text-zinc-400 mb-4">
|
||||||
|
You caught <span className="text-yellow-400 font-bold">{gameScore}</span> targets!
|
||||||
|
</p>
|
||||||
|
<p className="text-lg text-yellow-400 font-bold mb-4">
|
||||||
|
+{gameScore * 5} Void Essence
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => { setGameScore(0); startGame(); }}
|
||||||
|
className="w-full py-3 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 rounded-xl font-medium transition-all"
|
||||||
|
>
|
||||||
|
Play Again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{newAchievement && (
|
{newAchievement && (
|
||||||
<div className="fixed top-8 right-8 bg-gradient-to-r from-purple-600 to-pink-600 p-4 rounded-xl shadow-lg animate-bounce z-50">
|
<div className="fixed top-8 right-8 bg-gradient-to-r from-purple-600 to-pink-600 p-4 rounded-xl shadow-lg animate-bounce z-50">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
188
src/utils/sound.ts
Normal file
188
src/utils/sound.ts
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
class SoundEngine {
|
||||||
|
private audioContext: AudioContext | null = null
|
||||||
|
private masterGain: GainNode | null = null
|
||||||
|
private isMuted: boolean = false
|
||||||
|
|
||||||
|
private getContext(): AudioContext {
|
||||||
|
if (!this.audioContext) {
|
||||||
|
this.audioContext = new AudioContext()
|
||||||
|
this.masterGain = this.audioContext.createGain()
|
||||||
|
this.masterGain.connect(this.audioContext.destination)
|
||||||
|
this.masterGain.gain.value = 0.3
|
||||||
|
}
|
||||||
|
return this.audioContext
|
||||||
|
}
|
||||||
|
|
||||||
|
setMuted(muted: boolean) {
|
||||||
|
this.isMuted = muted
|
||||||
|
if (this.masterGain) {
|
||||||
|
this.masterGain.gain.value = muted ? 0 : 0.3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getMuted(): boolean {
|
||||||
|
return this.isMuted
|
||||||
|
}
|
||||||
|
|
||||||
|
playPet() {
|
||||||
|
if (this.isMuted) return
|
||||||
|
const ctx = this.getContext()
|
||||||
|
const osc = ctx.createOscillator()
|
||||||
|
const gain = ctx.createGain()
|
||||||
|
|
||||||
|
osc.connect(gain)
|
||||||
|
gain.connect(this.masterGain!)
|
||||||
|
|
||||||
|
osc.frequency.setValueAtTime(880, ctx.currentTime)
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(1320, ctx.currentTime + 0.1)
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(0.3, ctx.currentTime)
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.15)
|
||||||
|
|
||||||
|
osc.start(ctx.currentTime)
|
||||||
|
osc.stop(ctx.currentTime + 0.15)
|
||||||
|
}
|
||||||
|
|
||||||
|
playFeed() {
|
||||||
|
if (this.isMuted) return
|
||||||
|
const ctx = this.getContext()
|
||||||
|
const osc = ctx.createOscillator()
|
||||||
|
const gain = ctx.createGain()
|
||||||
|
|
||||||
|
osc.connect(gain)
|
||||||
|
gain.connect(this.masterGain!)
|
||||||
|
|
||||||
|
osc.type = 'sine'
|
||||||
|
osc.frequency.setValueAtTime(440, ctx.currentTime)
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(660, ctx.currentTime + 0.1)
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(880, ctx.currentTime + 0.2)
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(0.25, ctx.currentTime)
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3)
|
||||||
|
|
||||||
|
osc.start(ctx.currentTime)
|
||||||
|
osc.stop(ctx.currentTime + 0.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
playSpawn() {
|
||||||
|
if (this.isMuted) return
|
||||||
|
const ctx = this.getContext()
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const osc = ctx.createOscillator()
|
||||||
|
const gain = ctx.createGain()
|
||||||
|
|
||||||
|
osc.connect(gain)
|
||||||
|
gain.connect(this.masterGain!)
|
||||||
|
|
||||||
|
osc.type = 'sine'
|
||||||
|
const baseFreq = 220 * (i + 1)
|
||||||
|
osc.frequency.setValueAtTime(baseFreq, ctx.currentTime + i * 0.1)
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(baseFreq * 1.5, ctx.currentTime + i * 0.1 + 0.2)
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(0.2, ctx.currentTime + i * 0.1)
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + i * 0.1 + 0.25)
|
||||||
|
|
||||||
|
osc.start(ctx.currentTime + i * 0.1)
|
||||||
|
osc.stop(ctx.currentTime + i * 0.1 + 0.25)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playAchievement() {
|
||||||
|
if (this.isMuted) return
|
||||||
|
const ctx = this.getContext()
|
||||||
|
|
||||||
|
const notes = [523.25, 659.25, 783.99, 1046.50]
|
||||||
|
|
||||||
|
notes.forEach((freq, i) => {
|
||||||
|
const osc = ctx.createOscillator()
|
||||||
|
const gain = ctx.createGain()
|
||||||
|
|
||||||
|
osc.connect(gain)
|
||||||
|
gain.connect(this.masterGain!)
|
||||||
|
|
||||||
|
osc.type = 'sine'
|
||||||
|
osc.frequency.setValueAtTime(freq, ctx.currentTime + i * 0.12)
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(0.25, ctx.currentTime + i * 0.12)
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + i * 0.12 + 0.3)
|
||||||
|
|
||||||
|
osc.start(ctx.currentTime + i * 0.12)
|
||||||
|
osc.stop(ctx.currentTime + i * 0.12 + 0.3)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
playPurchase() {
|
||||||
|
if (this.isMuted) return
|
||||||
|
const ctx = this.getContext()
|
||||||
|
|
||||||
|
const notes = [392, 523.25, 659.25]
|
||||||
|
|
||||||
|
notes.forEach((freq, i) => {
|
||||||
|
const osc = ctx.createOscillator()
|
||||||
|
const gain = ctx.createGain()
|
||||||
|
|
||||||
|
osc.connect(gain)
|
||||||
|
gain.connect(this.masterGain!)
|
||||||
|
|
||||||
|
osc.type = 'triangle'
|
||||||
|
osc.frequency.setValueAtTime(freq, ctx.currentTime + i * 0.08)
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(0.2, ctx.currentTime + i * 0.08)
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + i * 0.08 + 0.15)
|
||||||
|
|
||||||
|
osc.start(ctx.currentTime + i * 0.08)
|
||||||
|
osc.stop(ctx.currentTime + i * 0.08 + 0.15)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
playRelease() {
|
||||||
|
if (this.isMuted) return
|
||||||
|
const ctx = this.getContext()
|
||||||
|
const osc = ctx.createOscillator()
|
||||||
|
const gain = ctx.createGain()
|
||||||
|
|
||||||
|
osc.connect(gain)
|
||||||
|
gain.connect(this.masterGain!)
|
||||||
|
|
||||||
|
osc.type = 'sine'
|
||||||
|
osc.frequency.setValueAtTime(440, ctx.currentTime)
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(110, ctx.currentTime + 0.4)
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(0.2, ctx.currentTime)
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.4)
|
||||||
|
|
||||||
|
osc.start(ctx.currentTime)
|
||||||
|
osc.stop(ctx.currentTime + 0.4)
|
||||||
|
}
|
||||||
|
|
||||||
|
playTimeChange() {
|
||||||
|
if (this.isMuted) return
|
||||||
|
const ctx = this.getContext()
|
||||||
|
|
||||||
|
const baseFreq = timeOfDayToFreq(ctx.currentTime % 4)
|
||||||
|
|
||||||
|
const osc = ctx.createOscillator()
|
||||||
|
const gain = ctx.createGain()
|
||||||
|
|
||||||
|
osc.connect(gain)
|
||||||
|
gain.connect(this.masterGain!)
|
||||||
|
|
||||||
|
osc.type = 'sine'
|
||||||
|
osc.frequency.setValueAtTime(baseFreq * 0.5, ctx.currentTime)
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(baseFreq, ctx.currentTime + 0.5)
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(0.15, ctx.currentTime)
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.6)
|
||||||
|
|
||||||
|
osc.start(ctx.currentTime)
|
||||||
|
osc.stop(ctx.currentTime + 0.6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeOfDayToFreq(phase: number): number {
|
||||||
|
const freqs = [261.63, 329.63, 392, 196]
|
||||||
|
return freqs[Math.floor(phase) % freqs.length]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const soundEngine = new SoundEngine()
|
||||||
Reference in New Issue
Block a user