From 8984aede0162f6bdcfc2dc0a54f563a3b1ff5684 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Tue, 31 Dec 2024 17:02:31 -0800 Subject: Add enough audio for background music. --- Demos/Pong/Main.hs | 31 +++++++++++----- README.md | 4 +- Spear.cabal | 4 ++ Spear/App.hs | 31 +++++++++++----- Spear/Sound/Sound.hs | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++ Spear/Sound/State.hs | 54 +++++++++++++++++++++++++++ 6 files changed, 203 insertions(+), 22 deletions(-) create mode 100644 Spear/Sound/Sound.hs create mode 100644 Spear/Sound/State.hs diff --git a/Demos/Pong/Main.hs b/Demos/Pong/Main.hs index b93325d..f77136f 100644 --- a/Demos/Pong/Main.hs +++ b/Demos/Pong/Main.hs @@ -13,6 +13,8 @@ import Spear.Physics.Collision import Spear.Render.Core.Pipeline import Spear.Render.Core.State import Spear.Render.Immediate +import Spear.Sound.Sound +import Spear.Sound.State import Spear.Window import Control.Monad (when) @@ -20,10 +22,11 @@ import Data.Maybe (mapMaybe) data GameState = GameState - { window :: Window + { context :: AppContext , renderCoreState :: RenderCoreState , immRenderState :: ImmRenderState , viewProjection :: Matrix4 + , backgroundMusic :: SoundSource , world :: [GameObject] } @@ -35,10 +38,19 @@ app = App options initGame endGame step render resize main :: IO () main = runApp app -initGame :: Window -> Game () GameState -initGame window = do +initGame :: AppContext -> Game () GameState +initGame context = do (immRenderState, renderCoreState) <- runSiblingGame newImmRenderer newRenderCoreState - return $ GameState window renderCoreState immRenderState Matrix4.id newWorld + (music, soundState') <- flip runSiblingGame (appSoundState context) $ do + musicBuffer <- loadAudioFile "/home/jeanne/Casual Tiki Party Main.wav" + music <- makeSoundSource + liftIO $ do + setSoundSourceBuffer music musicBuffer + setSoundLoopMode music Loop + playSounds [music] + return music + let context' = context { appSoundState = soundState' } + return $ GameState context' renderCoreState immRenderState Matrix4.id music newWorld endGame :: Game GameState () endGame = do @@ -48,13 +60,12 @@ endGame = do step :: Elapsed -> Dt -> [InputEvent] -> Game GameState Bool step elapsed dt inputEvents = do - gs <- get - events <- processInput (window gs) + gameState <- get + events <- processInput (appWindow . context $ gameState) --when (events /= []) $ liftIO . putStrLn $ show events - modify $ \gs -> - gs - { world = stepWorld (realToFrac elapsed) (realToFrac dt) events (world gs) - } + modify $ \gameState -> gameState + { world = stepWorld (realToFrac elapsed) (realToFrac dt) events (world gameState) + } return (not $ exitRequested inputEvents) processInput :: Window -> Game GameState [GameEvent] diff --git a/README.md b/README.md index 386250d..3296c35 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,7 @@ Installation (Ubuntu) Install dependencies, then build with cabal: ``` -$ sudo apt install libxxf86vm-dev libglfw3-dev -$ git clone https://github.com/jeannekamikaze/Spear.git -$ cd Spear +$ sudo apt install libxxf86vm-dev libglfw3-dev libopenal-dev libopenalut-dev $ cabal build ``` diff --git a/Spear.cabal b/Spear.cabal index 306ef6a..ed37d66 100644 --- a/Spear.cabal +++ b/Spear.cabal @@ -14,6 +14,8 @@ data-dir: "" library build-depends: GLFW-b -any, + OpenAL -any, + ALUT -any, OpenGL >= 3, OpenGLRaw -any, StateVar -any, @@ -81,6 +83,8 @@ library Spear.Scene.Graph Spear.Scene.Loader Spear.Scene.SceneResources + Spear.Sound.Sound + Spear.Sound.State Spear.Step Spear.Sys.Store Spear.Sys.Store.ID diff --git a/Spear/App.hs b/Spear/App.hs index 1520eee..8c0371e 100644 --- a/Spear/App.hs +++ b/Spear/App.hs @@ -1,6 +1,7 @@ module Spear.App ( App(..) , AppOptions(..) +, AppContext(..) , Elapsed , Dt , Step @@ -10,13 +11,16 @@ module Spear.App ) where -import Control.Monad -import Data.Fixed (mod') -import GHC.Float import Spear.Game -import Spear.Sys.Timer as Timer +import Spear.Sound.Sound +import Spear.Sound.State +import Spear.Sys.Timer as Timer import Spear.Window +import Control.Monad +import Data.Fixed (mod') +import GHC.Float + -- | Time elapsed. type Elapsed = Double @@ -48,13 +52,18 @@ defaultAppOptions = AppOptions -- | Application state. data App s = App { appOptions :: AppOptions - , initApp :: Window -> Game () s + , initApp :: AppContext -> Game () s , endApp :: Game s () , stepApp :: Step s , renderApp :: Game s () , resizeApp :: WindowEvent -> Game s () } +-- | Application context. +data AppContext = AppContext + { appWindow :: Window + , appSoundState :: SoundState + } -- | Run the application. runApp :: App s -> IO () @@ -62,10 +71,14 @@ runApp app = let ops = appOptions app w = windowWidth ops h = windowHeight ops - in withWindow (w, h) (title ops) $ \window -> flip evalGame () $ do - gameState <- initApp app window - (result, endGameState) <- runSubGame (loop app window) gameState - runSubGame' (endApp app) endGameState + in withWindow (w, h) (title ops) $ \window -> + withSoundContext $ flip evalGame () $ do + soundState <- evalSiblingGame initSoundSystem () + let appContext = AppContext window soundState + gameState <- initApp app appContext + (result, endGameState) <- runSubGame (loop app window) gameState + runSubGame' (endApp app) endGameState + runSiblingGame' destroySoundSystem soundState -- | Convert FPS to desired delta time. fpsToDdt :: Int -> TimeDelta diff --git a/Spear/Sound/Sound.hs b/Spear/Sound/Sound.hs new file mode 100644 index 0000000..53a1a46 --- /dev/null +++ b/Spear/Sound/Sound.hs @@ -0,0 +1,101 @@ +module Spear.Sound.Sound +( LoopMode(..) +, withSoundContext +, initSoundSystem +, destroySoundSystem +, loadAudioFile +, deleteSoundBuffer +, makeSoundSource +, deleteSoundSource +, setSoundSourceBuffer +, setSoundLoopMode +, playSounds +) +where + +import Spear.Game +import Spear.Sound.State + +import Data.Set as Set +import Data.StateVar (($=)) +import qualified Sound.ALUT as AL + + +data LoopMode + = SingleShot + | Loop + deriving (Show) + + +-- | Create the sound context and run an IO action within the context. +withSoundContext :: IO a -> IO a +withSoundContext action = AL.withProgNameAndArgs AL.runALUT $ + \name args -> action + +-- | Initialize the sound system. +initSoundSystem :: Game () SoundState +initSoundSystem = return newSoundState + +-- | Destroy the sound system. +destroySoundSystem :: Game SoundState () +destroySoundSystem = do + state <- get + mapM_ release' (toList $ buffers state) + mapM_ release' (toList $ sources state) + put newSoundState + +-- | Load an audio file. +loadAudioFile :: FilePath -> Game SoundState SoundBuffer +loadAudioFile path = do + alBuffer <- liftIO $ AL.createBuffer (AL.File path) + resourceKey <- register $ AL.deleteObjectName alBuffer + let buffer = SoundBuffer alBuffer resourceKey + modify (\state -> state { + buffers = Set.insert buffer (buffers state) + }) + return buffer + +-- | Delete the sound buffer. +deleteSoundBuffer :: SoundBuffer -> Game SoundState () +deleteSoundBuffer buffer = do + modify (\state -> state { + buffers = Set.delete buffer (buffers state) + }) + release' buffer + +-- | Create a sound source. +-- +-- The new source sounds flat, like background music or sound effects in a 2D +-- game. Change the source's (and listener's) properties to simulate 3D sound. +makeSoundSource :: Game SoundState SoundSource +makeSoundSource = do + alSource <- AL.genObjectName + resourceKey <- register $ AL.deleteObjectName alSource + let source = SoundSource alSource resourceKey + modify (\state -> state { + sources = Set.insert source (sources state) + }) + return source + +-- | Delete the sound source. +deleteSoundSource :: SoundSource -> Game SoundState () +deleteSoundSource source = do + modify (\state -> state { + sources = Set.delete source (sources state) + }) + release' source + +-- | Set the sound that the sound source emits. +setSoundSourceBuffer :: SoundSource -> SoundBuffer -> IO () +setSoundSourceBuffer source buffer = + AL.buffer (alSource source) $= Just (alBuffer buffer) + +-- | Set the sound's loop mode. +setSoundLoopMode :: SoundSource -> LoopMode -> IO () +setSoundLoopMode source mode = AL.loopingMode (alSource source) $= alMode mode + where alMode SingleShot = AL.OneShot + alMode Loop = AL.Looping + +-- | Play the sound sources. +playSounds :: [SoundSource] -> IO () +playSounds = AL.play . (alSource <$>) diff --git a/Spear/Sound/State.hs b/Spear/Sound/State.hs new file mode 100644 index 0000000..d843de5 --- /dev/null +++ b/Spear/Sound/State.hs @@ -0,0 +1,54 @@ +module Spear.Sound.State where + +import Spear.Game + +import Data.Hashable +import Data.Set as Set +import qualified Sound.ALUT as AL + + +-- | A sound buffer. +data SoundBuffer = SoundBuffer + { alBuffer :: AL.Buffer + , bufferResource :: ReleaseKey + } + +-- | A sound source. +data SoundSource = SoundSource + { alSource :: AL.Source + , sourceResource :: ReleaseKey + } + +-- | Sound state. +data SoundState = SoundState + { buffers :: Set SoundBuffer + , sources :: Set SoundSource + } + + +instance ResourceClass SoundBuffer where + getResource = bufferResource + +instance ResourceClass SoundSource where + getResource = sourceResource + +instance Eq SoundBuffer where + a == b = alBuffer a == alBuffer b + +instance Eq SoundSource where + a == b = alSource a == alSource b + +instance Ord SoundBuffer where + a < b = alBuffer a < alBuffer b + a <= b = alBuffer a <= alBuffer b + +instance Ord SoundSource where + a < b = alSource a < alSource b + a <= b = alSource a <= alSource b + + +newSoundState :: SoundState +newSoundState = SoundState + { buffers = Set.empty + , sources = Set.empty + } -- cgit v1.2.3