Files
loop/src/Sound.hs
T
2026-01-23 09:59:18 +00:00

166 lines
6.0 KiB
Haskell

{-|
Module : Sound
Description : Channel selection and storage
This module checks for empty channels and plays sounds on them.
It uses data chunks loaded into an IntMap to determine the actual sounds played.
Sound specifications of type 'Sound' loaded into a Map can be used to
keep track of which sounds are playing.
Uses SDL.Mixer.
-}
module Sound (
-- * Simple (One-Shot) Playback
playSoundQueue
, playPositionalSoundQueue
, stopAllSounds
-- * Complex Playback
, playSoundAndUpdate
-- * Volume Control
, setSoundVolume
, setMusicVolume
, module Sound.Data
) where
import Sound.Data
import qualified SDL.Mixer as Mix
import Data.Maybe
import qualified IntMapHelp as IM
import qualified Data.Map as M
import Control.Monad
import Control.Monad.IO.Class
--import Control.Monad.Trans
import Control.Monad.Trans.Maybe
import Control.Lens
import Data.Int (Int16)
{- | Start playing new sounds and update sound specifications.
A Map of new sound specifications is merged with a Map of already playing sounds,
then sounds in the merged Map are updated.
The Map of updated sound specifications is returned.
New sounds with the same keys as
already playing sounds are merged as follows:
* '_soundChannel' is set to the old value
* if the old sound is not fading and the chunk is the same, '_soundStatus' is set to the old value
* all other values are set to the new values
In the update:
1. commence playing sounds with 'ToStart' status
2. decrement timers, set any fading status
3. apply 'Just' sound position effects, set value to 'Nothing'
4. remove sounds that have stopped playing from the map.
-}
playSoundAndUpdate
:: Ord a => IM.IntMap Mix.Chunk -> M.Map a Sound -> M.Map a Sound -> IO (M.Map a Sound)
playSoundAndUpdate sData oldSounds newSounds
= updateSounds sData (M.unionWith mergeSound oldSounds newSounds)
mergeSound :: Sound -> Sound -> Sound
mergeSound oldS newS
| _soundChunkID newS == _soundChunkID oldS && _playStatus (_soundStatus newS) == ToContinueStart
= newOldChannel & soundStatus . playStatus .~ _playStatus (_soundStatus oldS)
| otherwise = newOldChannel
where
newOldChannel = newS & soundChannel .~ _soundChannel oldS
updateSounds :: IM.IntMap Mix.Chunk -> M.Map a Sound -> IO (M.Map a Sound)
updateSounds sd ss = do
may <- mapM (runMaybeT . updateSound sd) ss
return $ M.mapMaybe id may
updateSound :: IM.IntMap Mix.Chunk -> Sound -> MaybeT IO Sound
updateSound sd s = initialisePlaying sd s
>>= liftIO . (decrementTimer >=> applyPosition)
>>= cleanupHalted
initialisePlaying :: IM.IntMap Mix.Chunk -> Sound -> MaybeT IO Sound
initialisePlaying sd s = case _playStatus $ _soundStatus s of
ToStart -> tryPlay sd s
ToContinueStart -> tryPlay sd s
JustStartedPlaying -> return $ s & soundStatus . playStatus .~ Playing
_ -> return s
tryPlay :: IM.IntMap Mix.Chunk -> Sound -> MaybeT IO Sound
tryPlay sd s = do
i <- tryGetChannel s
_ <- liftIO $ do
Mix.halt i
Mix.playOn i timesToPlay (sd IM.! _getSoundID (_soundChunkID s))
return $ s
& soundChannel ?~ i
& soundStatus . playStatus .~ JustStartedPlaying
where
timesToPlay
| _soundIsLooping (_soundStatus s) = Mix.Forever
| otherwise = Mix.Once
tryGetChannel :: Sound -> MaybeT IO Mix.Channel
tryGetChannel s = case _soundChannel s of
Just i -> return i
Nothing -> MaybeT $ Mix.getAvailable 0 -- 0 refers to the group for game sounds
decrementTimer :: Sound -> IO Sound
decrementTimer s = case _soundTime s of
Just t
| t > 0 -> return $ s & soundTime . _Just -~ 1
| otherwise -> do
forM_ (_soundChannel s) Mix.halt
return $ s & soundTime .~ Nothing
Nothing -> return s
applyPosition :: Sound -> IO Sound
applyPosition s = case _soundAngDist s of
Nothing -> return s
Just (a,d) -> Mix.effectPosition (fromJust $ _soundChannel s) a d
>> return (s & soundAngDist .~ Nothing)
cleanupHalted :: Sound -> MaybeT IO Sound
cleanupHalted s = do
i <- MaybeT $ return $ _soundChannel s
isPlaying <- liftIO $ Mix.playing i
if isPlaying
then return s
else mzero
stopAllSounds :: IO ()
stopAllSounds = Mix.halt Mix.AllChannels
-----------------------------------------------------------------
{- | Play sounds from a list of indices.
For each index, the corresponding sound starts playing if there is a free channel.
Use this if you don't care about timing, overlapping, fading, or sound positions. -}
playSoundQueue :: IM.IntMap Mix.Chunk -> [Int] -> IO ()
playSoundQueue chunkMap = mapM_ $ \n -> runMaybeT $ playIfFree (chunkMap IM.! n) Mix.Once
{- | Given a chunk, attempt to play this on a free channel a given number of
times. Returns 'Just' the channel if succeeds. -}
playIfFree :: Mix.Chunk -> Mix.Times -> MaybeT IO Mix.Channel
playIfFree c times = do
i <- MaybeT $ Mix.getAvailable Mix.DefaultGroup -- try all channels
liftIO $ Mix.playOn i times c
-----------------------------------------------------------------
{- | Play sounds from a list of index/position pairs.
As for 'playSoundQueue', but with positional information in the form of an Int16. -}
playPositionalSoundQueue :: IM.IntMap Mix.Chunk -> [(Int,Int16)] -> IO ()
playPositionalSoundQueue chunkMap
= mapM_ ( \(n,a) ->
runMaybeT $ playIfFree (chunkMap IM.! n) Mix.Once >>= setChannelPos a )
setChannelPos :: Int16 -> Mix.Channel -> MaybeT IO Mix.Channel
setChannelPos a i = do
_ <- liftIO $ Mix.effectPosition i a 0
return i
-----------------------------------------------------------------
{- | Set the volume for all sound channels.
Behind the scenes, scales a float [0,1] to an Int [0..128]. -}
setSoundVolume :: Float -> IO ()
setSoundVolume x = Mix.setVolume (round (x * 128)) Mix.AllChannels
{- | Set the music volume.
Behind the scenes, scales a float [0,1] to an Int [0..128]. -}
setMusicVolume :: Float -> IO ()
setMusicVolume x = Mix.setMusicVolume (round (x * 128))