├── stack.yaml ├── Setup.hs ├── .gitignore ├── cbits └── helpers.c ├── examples ├── Music.hs ├── Basic.hs ├── Jumbled.hs ├── BasicRaw.hs └── Effect.hs ├── .travis.yml ├── README.md ├── LICENSE ├── src └── SDL │ ├── Raw │ ├── Helper.hs │ └── Mixer.hsc │ └── Mixer.hs └── sdl2-mixer.cabal /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-7.18 2 | system-ghc: true 3 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | *.mp3 3 | *.wav 4 | /.cabal-sandbox 5 | /cabal.sandbox.config 6 | .stack-work 7 | /dist-newstyle 8 | .ghc.environment.* 9 | -------------------------------------------------------------------------------- /cbits/helpers.c: -------------------------------------------------------------------------------- 1 | #include "SDL2/SDL.h" 2 | #include "SDL2/SDL_mixer.h" 3 | 4 | // These were all macros in SDL_mixer.h. 5 | 6 | extern DECLSPEC Mix_Chunk * SDLCALL 7 | Mix_LoadWAV_helper( 8 | char *file) { 9 | 10 | return Mix_LoadWAV_RW(SDL_RWFromFile(file, "rb"), 1); 11 | } 12 | 13 | extern DECLSPEC int SDLCALL 14 | Mix_PlayChannel_helper( 15 | int channel, 16 | Mix_Chunk *chunk, 17 | int loops) { 18 | 19 | return Mix_PlayChannelTimed(channel, chunk, loops, -1); 20 | } 21 | 22 | extern DECLSPEC int SDLCALL 23 | Mix_FadeInChannel_helper( 24 | int channel, 25 | Mix_Chunk *chunk, 26 | int loops, 27 | int ms) { 28 | 29 | return Mix_FadeInChannelTimed(channel, chunk, loops, ms, -1); 30 | } 31 | -------------------------------------------------------------------------------- /examples/Music.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Control.Monad (when) 4 | import System.Environment (getArgs) 5 | import System.Exit (exitFailure) 6 | 7 | import qualified SDL 8 | import qualified SDL.Mixer as Mix 9 | 10 | main :: IO () 11 | main = do 12 | SDL.initialize [SDL.InitAudio] 13 | 14 | Mix.withAudio Mix.defaultAudio 256 $ do 15 | putStr "Available music decoders: " 16 | print =<< Mix.musicDecoders 17 | 18 | args <- getArgs 19 | case args of 20 | [] -> putStrLn "Usage: cabal run sdl2-mixer-music FILE" >> exitFailure 21 | xs -> runExample $ head xs 22 | 23 | SDL.quit 24 | 25 | -- | Play the given file as a Music. 26 | runExample :: FilePath -> IO () 27 | runExample path = do 28 | music <- Mix.load path 29 | print $ Mix.musicType music 30 | Mix.whenMusicFinished $ putStrLn "Music finished playing!" 31 | Mix.playMusic Mix.Once music 32 | delayWhile Mix.playingMusic 33 | Mix.free music 34 | 35 | delayWhile :: IO Bool -> IO () 36 | delayWhile check = loop' 37 | where 38 | loop' = do 39 | still <- check 40 | when still $ SDL.delay 300 >> delayWhile check 41 | -------------------------------------------------------------------------------- /examples/Basic.hs: -------------------------------------------------------------------------------- 1 | import qualified SDL.Mixer as Mix 2 | import qualified SDL 3 | 4 | import Control.Monad (when) 5 | import Data.Default.Class (def) 6 | import System.Environment (getArgs) 7 | import System.Exit (exitFailure) 8 | 9 | main :: IO () 10 | main = do 11 | -- read arguments 12 | fileName <- do 13 | args <- getArgs 14 | case args of 15 | (arg:_) -> return arg 16 | _ -> do 17 | putStrLn "Usage: cabal run sdl2-mixer-basic " 18 | exitFailure 19 | 20 | -- initialize libraries 21 | SDL.initialize [SDL.InitAudio] 22 | Mix.initialize [Mix.InitMP3] 23 | 24 | -- open device 25 | Mix.openAudio def 256 26 | 27 | -- open file 28 | sound <- Mix.load fileName 29 | 30 | -- play file 31 | Mix.play sound 32 | 33 | -- wait until finished 34 | whileTrueM $ Mix.playing Mix.AllChannels 35 | 36 | -- free resources 37 | Mix.free sound 38 | 39 | -- close device 40 | Mix.closeAudio 41 | 42 | -- quit 43 | Mix.quit 44 | SDL.quit 45 | 46 | whileTrueM :: Monad m => m Bool -> m () 47 | whileTrueM cond = do 48 | loop <- cond 49 | when loop $ whileTrueM cond 50 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: haskell 2 | 3 | sudo: required 4 | 5 | env: 6 | - GHCVER=8.0.2 CABALVER=1.24 HAPPYVER=1.19.5 LD_LIBRARY_PATH=/usr/local/lib 7 | 8 | before_install: 9 | - cd .. 10 | 11 | # Install SDL. 12 | - wget -q http://www.libsdl.org/release/SDL2-2.0.3.tar.gz 13 | - tar xf SDL2-*.tar.gz 14 | - cd SDL2-* && ./configure && make && sudo make install && cd .. 15 | 16 | # Install SDL_mixer. 17 | - wget -q http://www.libsdl.org/projects/SDL_mixer/release/SDL2_mixer-2.0.0.tar.gz 18 | - tar xf SDL2_mixer-*.tar.gz 19 | - cd SDL2_mixer-* && ./configure && make && sudo make install && cd .. 20 | 21 | # Install GHC and cabal-install. 22 | - travis_retry sudo add-apt-repository -y ppa:hvr/ghc 23 | - travis_retry sudo apt-get update 24 | - travis_retry sudo apt-get install cabal-install-$CABALVER ghc-$GHCVER happy-$HAPPYVER 25 | - export CABAL=/opt/cabal/bin/cabal-$CABALVER 26 | - export PATH=/opt/ghc/$GHCVER/bin:$PATH 27 | - rm -rf ~/.cabal 28 | - $CABAL update 29 | 30 | install: 31 | # Install haskell-game/sdl2. 32 | - git clone https://github.com/haskell-game/sdl2.git 33 | - cd sdl2 && $CABAL install --flags="no-linear" && cd .. 34 | 35 | script: 36 | - cd sdl2-mixer 37 | - $CABAL install --flags="example" 38 | -------------------------------------------------------------------------------- /examples/Jumbled.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Control.Monad (when) 4 | import Data.Default.Class (def) 5 | import System.Environment (getArgs) 6 | import System.Exit (exitFailure) 7 | 8 | import qualified SDL 9 | import qualified SDL.Mixer as Mix 10 | 11 | main :: IO () 12 | main = do 13 | SDL.initialize [SDL.InitAudio] 14 | 15 | Mix.withAudio def 256 $ do 16 | putStr "Available chunk decoders: " 17 | print =<< Mix.chunkDecoders 18 | 19 | args <- getArgs 20 | case args of 21 | [] -> do 22 | putStrLn "Usage: cabal run sdl2-mixer-jumbled FILE..." 23 | exitFailure 24 | xs -> runExample xs 25 | 26 | SDL.quit 27 | 28 | -- | Play each of the sounds at the same time! 29 | runExample :: [FilePath] -> IO () 30 | runExample paths = do 31 | Mix.setChannels $ length paths 32 | Mix.whenChannelFinished $ \c -> putStrLn $ show c ++ " finished playing!" 33 | chunks <- mapM Mix.load paths 34 | mapM_ Mix.play chunks 35 | delayWhile $ Mix.playing Mix.AllChannels 36 | Mix.setChannels 0 37 | mapM_ Mix.free chunks 38 | 39 | delayWhile :: IO Bool -> IO () 40 | delayWhile check = loop' 41 | where 42 | loop' = do 43 | still <- check 44 | when still $ SDL.delay 300 >> delayWhile check 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sdl2-mixer 2 | 3 | [![Hackage](https://img.shields.io/hackage/v/sdl2-mixer.svg)](https://hackage.haskell.org/package/sdl2-mixer) 4 | [![Build Status](https://travis-ci.org/sbidin/sdl2-mixer.svg?branch=master)](https://travis-ci.org/sbidin/sdl2-mixer) 5 | 6 | #### Haskell bindings to SDL2_mixer 7 | 8 | Both the raw and the higher level bindings should allow you to use any aspect 9 | of the original SDL2_mixer library. Please report an issue if you encounter a 10 | bug or feel that something is missing. 11 | 12 | ##### Install 13 | 14 | ```bash 15 | cabal install sdl2-mixer 16 | ``` 17 | 18 | ##### Documentation 19 | 20 | For documentation, [visit Hackage](https://hackage.haskell.org/package/sdl2-mixer). 21 | 22 | The 23 | [original SDL2_mixer documentation](http://www.libsdl.org/projects/SDL_mixer/docs/SDL_mixer.html) 24 | can also help, as the bindings are close to a direct mapping. 25 | 26 | ##### Examples 27 | 28 | Several example executables are included with the library. You can find them in 29 | the `examples` directory. 30 | 31 | ```bash 32 | cd sdl2-mixer 33 | cabal run sdl2-mixer-basic 34 | cabal run sdl2-mixer-raw 35 | cabal run sdl2-mixer-music 36 | cabal run sdl2-mixer-jumbled ... 37 | cabal run sdl2-mixer-effect 38 | ``` 39 | -------------------------------------------------------------------------------- /examples/BasicRaw.hs: -------------------------------------------------------------------------------- 1 | import qualified SDL.Raw.Mixer as Mix 2 | import qualified SDL 3 | 4 | import Control.Monad (unless, when) 5 | import Foreign.C.String (withCString) 6 | import Foreign.Ptr (nullPtr) 7 | import System.Environment (getArgs) 8 | import System.Exit (exitFailure) 9 | 10 | main :: IO () 11 | main = do 12 | -- read arguments 13 | fileName <- do 14 | args <- getArgs 15 | case args of 16 | (arg:_) -> return arg 17 | _ -> do 18 | putStrLn "Usage: cabal run sdl2-mixer-raw " 19 | exitFailure 20 | 21 | -- initialize libraries 22 | SDL.initialize [SDL.InitAudio] 23 | _ <- Mix.init Mix.INIT_MP3 24 | 25 | let rate = 22050 26 | format = Mix.AUDIO_S16SYS 27 | channels = 2 28 | bufsize = 256 29 | 30 | -- open device 31 | result <- Mix.openAudio rate format channels bufsize 32 | assert $ result == 0 33 | 34 | -- open file 35 | sound <- withCString fileName $ \cstr -> Mix.loadWAV cstr 36 | assert $ sound /= nullPtr 37 | 38 | -- play file 39 | channel <- Mix.playChannel (-1) sound 0 40 | assert $ channel /= -1 41 | 42 | -- wait until finished 43 | whileTrueM $ (/= 0) <$> Mix.playing channel 44 | 45 | -- free resources 46 | Mix.freeChunk sound 47 | 48 | -- close device 49 | Mix.closeAudio 50 | 51 | -- quit 52 | Mix.quit 53 | SDL.quit 54 | 55 | assert :: Bool -> IO () 56 | assert = flip unless $ error "Assertion failed" 57 | 58 | whileTrueM :: Monad m => m Bool -> m () 59 | whileTrueM cond = do 60 | loop <- cond 61 | when loop $ whileTrueM cond 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Vladimir Semyonov 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Vladimir Semyonov nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /examples/Effect.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Data.Int (Int16) 4 | import Data.Word (Word8) 5 | import Control.Monad (when, forM_) 6 | import System.Environment (getArgs) 7 | import System.Exit (exitFailure) 8 | 9 | import qualified SDL 10 | import qualified SDL.Mixer as Mix 11 | import qualified Data.Vector.Storable.Mutable as MV 12 | 13 | main :: IO () 14 | main = do 15 | SDL.initialize [SDL.InitAudio] 16 | 17 | Mix.withAudio Mix.defaultAudio 256 $ do 18 | putStr "Available music decoders: " 19 | print =<< Mix.musicDecoders 20 | 21 | args <- getArgs 22 | case args of 23 | [] -> putStrLn "Usage: cabal run sdl2-mixer-effect FILE" >> exitFailure 24 | xs -> runExample $ head xs 25 | 26 | SDL.quit 27 | 28 | -- An effect that does something silly: lowers the volume 2 times. 29 | halveVolume :: MV.IOVector Word8 -> IO () 30 | halveVolume bytes = do 31 | let shorts = MV.unsafeCast bytes :: MV.IOVector Int16 32 | forM_ [0 .. MV.length shorts - 1] $ \i -> do 33 | s <- MV.read shorts i 34 | MV.write shorts i $ div s 2 35 | 36 | -- Apply an Effect on the Music being played. 37 | runExample :: FilePath -> IO () 38 | runExample path = do 39 | 40 | -- Add effects, get back effect removal actions. 41 | -- The volume should be FOUR times as low after this. 42 | popEffects <- 43 | mapM (Mix.effect Mix.PostProcessing (\_ -> return ()) . const) 44 | [halveVolume, halveVolume] 45 | 46 | -- Then give the left channel louder music than the right one. 47 | popPan <- Mix.effectPan Mix.PostProcessing 128 64 48 | 49 | music <- Mix.load path 50 | Mix.playMusic Mix.Once music 51 | 52 | delayWhile Mix.playingMusic 53 | 54 | -- The effects are no longer applied after this. 55 | sequence_ $ popPan : popEffects 56 | 57 | Mix.free music 58 | 59 | delayWhile :: IO Bool -> IO () 60 | delayWhile check = loop' 61 | where 62 | loop' = do 63 | still <- check 64 | when still $ SDL.delay 300 >> delayWhile check 65 | -------------------------------------------------------------------------------- /src/SDL/Raw/Helper.hs: -------------------------------------------------------------------------------- 1 | {-| 2 | 3 | Module : SDL.Raw.Helper 4 | License : BSD3 5 | 6 | Exposes a way to automatically generate a foreign import alongside its lifted, 7 | inlined MonadIO variant. Use this to simplify the package's SDL.Raw.* modules. 8 | 9 | -} 10 | 11 | {-# LANGUAGE BangPatterns #-} 12 | {-# LANGUAGE LambdaCase #-} 13 | {-# LANGUAGE TemplateHaskell #-} 14 | 15 | module SDL.Raw.Helper (liftF) where 16 | 17 | import Control.Monad (replicateM) 18 | import Control.Monad.IO.Class (MonadIO, liftIO) 19 | import Language.Haskell.TH 20 | 21 | -- | Given a name @fname@, a name of a C function @cname@ and the desired 22 | -- Haskell type @ftype@, this function generates: 23 | -- 24 | -- * A foreign import of @cname@, named as @fname'@. 25 | -- * An always-inline MonadIO version of @fname'@, named @fname@. 26 | liftF :: String -> String -> Q Type -> Q [Dec] 27 | liftF fname cname ftype = do 28 | let f' = mkName $ fname ++ "'" -- Direct binding. 29 | let f = mkName fname -- Lifted. 30 | t' <- ftype -- Type of direct binding. 31 | 32 | -- The generated function accepts n arguments. 33 | args <- replicateM (countArgs t') $ newName "x" 34 | 35 | -- If the function has no arguments, then we just liftIO it directly. 36 | -- However, this fails to typecheck without an explicit type signature. 37 | -- Therefore, we include one. TODO: Can we get rid of this? 38 | sigd <- case args of 39 | [] -> ((:[]) . SigD f) `fmap` liftType t' 40 | _ -> return [] 41 | 42 | return $ concat 43 | [ 44 | [ ForeignD $ ImportF CCall Safe cname f' t' 45 | , PragmaD $ InlineP f Inline FunLike AllPhases 46 | ] 47 | , sigd 48 | , [ FunD f 49 | [ Clause 50 | (map VarP args) 51 | (NormalB $ 'liftIO `applyTo` [f' `applyTo` map VarE args]) 52 | [] 53 | ] 54 | ] 55 | ] 56 | 57 | -- | How many arguments does a function of a given type take? 58 | countArgs :: Type -> Int 59 | countArgs = count 0 60 | where 61 | count !n = \case 62 | (AppT (AppT ArrowT _) t) -> count (n+1) t 63 | (ForallT _ _ t) -> count n t 64 | (SigT t _) -> count n t 65 | _ -> n 66 | 67 | -- | An expression where f is applied to n arguments. 68 | applyTo :: Name -> [Exp] -> Exp 69 | applyTo f [] = VarE f 70 | applyTo f es = loop (tail es) . AppE (VarE f) $ head es 71 | where 72 | loop as e = foldl AppE e as 73 | 74 | -- | Fuzzily speaking, converts a given IO type into a MonadIO m one. 75 | liftType :: Type -> Q Type 76 | liftType = \case 77 | AppT _ t -> do 78 | m <- newName "m" 79 | return $ 80 | ForallT 81 | [PlainTV m] 82 | [AppT (ConT ''MonadIO) $ VarT m] 83 | (AppT (VarT m) t) 84 | t -> return t 85 | -------------------------------------------------------------------------------- /sdl2-mixer.cabal: -------------------------------------------------------------------------------- 1 | name: sdl2-mixer 2 | version: 1.1.0 3 | synopsis: Bindings to SDL2_mixer. 4 | description: Haskell bindings to SDL2_mixer. 5 | license: BSD3 6 | license-file: LICENSE 7 | author: Vladimir Semyonov 8 | Siniša Biđin 9 | maintainer: Siniša Biđin 10 | copyright: Copyright © 2015 Vladimir Semyonov 11 | Copyright © 2015 Siniša Biđin 12 | category: Sound, Foreign 13 | build-type: Simple 14 | cabal-version: >=1.10 15 | 16 | source-repository head 17 | type: git 18 | location: https://github.com/sbidin/sdl2-mixer.git 19 | 20 | library 21 | ghc-options: -Wall 22 | 23 | exposed-modules: 24 | SDL.Mixer, 25 | SDL.Raw.Mixer 26 | 27 | other-modules: 28 | SDL.Raw.Helper 29 | 30 | hs-source-dirs: 31 | src 32 | 33 | includes: 34 | SDL_mixer.h 35 | 36 | extra-libraries: 37 | SDL2_mixer 38 | 39 | pkgconfig-depends: 40 | SDL2_mixer >= 2.0.0 41 | 42 | c-sources: 43 | cbits/helpers.c 44 | 45 | build-depends: 46 | base >= 4.7 && < 5, 47 | bytestring >= 0.10.4.0, 48 | data-default-class >= 0.0.1, 49 | lifted-base >= 0.2, 50 | monad-control >= 1.0, 51 | sdl2 >= 2.0.0, 52 | template-haskell, 53 | vector >= 0.10 54 | 55 | default-language: 56 | Haskell2010 57 | 58 | flag example 59 | description: Build the example executables 60 | default: False 61 | 62 | executable sdl2-mixer-basic 63 | ghc-options: -Wall 64 | hs-source-dirs: examples 65 | main-is: Basic.hs 66 | default-language: Haskell2010 67 | 68 | if flag(example) 69 | build-depends: 70 | base >= 4.7 && < 5, 71 | data-default-class >= 0.0.1, 72 | sdl2 >= 2.0.0, 73 | sdl2-mixer 74 | else 75 | buildable: False 76 | 77 | executable sdl2-mixer-raw 78 | ghc-options: -Wall 79 | hs-source-dirs: examples 80 | main-is: BasicRaw.hs 81 | default-language: Haskell2010 82 | 83 | if flag(example) 84 | build-depends: 85 | base >= 4.7 && < 5, 86 | sdl2 >= 2.0.0, 87 | sdl2-mixer 88 | else 89 | buildable: False 90 | 91 | executable sdl2-mixer-jumbled 92 | ghc-options: -Wall 93 | hs-source-dirs: examples 94 | main-is: Jumbled.hs 95 | default-language: Haskell2010 96 | 97 | if flag(example) 98 | build-depends: 99 | base >= 4.7 && < 5, 100 | data-default-class >= 0.0.1, 101 | sdl2 >= 2.0.0, 102 | sdl2-mixer 103 | else 104 | buildable: False 105 | 106 | executable sdl2-mixer-music 107 | ghc-options: -Wall 108 | hs-source-dirs: examples 109 | main-is: Music.hs 110 | default-language: Haskell2010 111 | 112 | if flag(example) 113 | build-depends: 114 | base >= 4.7 && < 5, 115 | data-default-class >= 0.0.1, 116 | sdl2 >= 2.0.0, 117 | sdl2-mixer 118 | else 119 | buildable: False 120 | 121 | executable sdl2-mixer-effect 122 | ghc-options: -Wall -threaded 123 | hs-source-dirs: examples 124 | main-is: Effect.hs 125 | default-language: Haskell2010 126 | 127 | if flag(example) 128 | build-depends: 129 | base >= 4.7 && < 5, 130 | data-default-class >= 0.0.1, 131 | sdl2 >= 2.0.0, 132 | sdl2-mixer, 133 | vector >= 0.10 134 | else 135 | buildable: False 136 | -------------------------------------------------------------------------------- /src/SDL/Raw/Mixer.hsc: -------------------------------------------------------------------------------- 1 | {-| 2 | 3 | Module : SDL.Raw.Mixer 4 | License : BSD3 5 | Stability : experimental 6 | 7 | Raw bindings to the @SDL2_mixer@ library. No error-handling is done here. For 8 | more information about specific function behaviour, see the @SDL2_mixer@ 9 | documentation. 10 | 11 | -} 12 | 13 | {-# OPTIONS_GHC -fno-warn-missing-signatures #-} 14 | 15 | {-# LANGUAGE PatternSynonyms #-} 16 | {-# LANGUAGE RecordWildCards #-} 17 | {-# LANGUAGE TemplateHaskell #-} 18 | 19 | module SDL.Raw.Mixer 20 | ( 21 | 22 | -- * General 23 | getVersion 24 | , pattern SDL_MIXER_MAJOR_VERSION 25 | , pattern SDL_MIXER_MINOR_VERSION 26 | , pattern SDL_MIXER_PATCHLEVEL 27 | , InitFlag 28 | , init 29 | , pattern INIT_FLAC 30 | , pattern INIT_MOD 31 | , pattern INIT_MP3 32 | , pattern INIT_OGG 33 | , quit 34 | , Format 35 | , pattern DEFAULT_FORMAT 36 | , pattern DEFAULT_FREQUENCY 37 | , pattern DEFAULT_CHANNELS 38 | , openAudio 39 | , pattern AUDIO_U8 40 | , pattern AUDIO_S8 41 | , pattern AUDIO_U16LSB 42 | , pattern AUDIO_S16LSB 43 | , pattern AUDIO_U16MSB 44 | , pattern AUDIO_S16MSB 45 | , pattern AUDIO_U16 46 | , pattern AUDIO_S16 47 | , pattern AUDIO_U16SYS 48 | , pattern AUDIO_S16SYS 49 | , closeAudio 50 | , querySpec 51 | 52 | -- * Samples 53 | , getNumChunkDecoders 54 | , getChunkDecoder 55 | , Chunk(..) 56 | , loadWAV 57 | , loadWAV_RW 58 | , quickLoadWAV 59 | , quickLoadRaw 60 | , pattern MAX_VOLUME 61 | , volumeChunk 62 | , freeChunk 63 | 64 | -- * Channels 65 | , allocateChannels 66 | , pattern CHANNELS 67 | , Channel 68 | , volume 69 | , playChannel 70 | , playChannelTimed 71 | , fadeInChannel 72 | , fadeInChannelTimed 73 | , pause 74 | , resume 75 | , haltChannel 76 | , expireChannel 77 | , fadeOutChannel 78 | , channelFinished 79 | , wrapChannelCallback 80 | , playing 81 | , paused 82 | , Fading 83 | , fadingChannel 84 | , pattern NO_FADING 85 | , pattern FADING_OUT 86 | , pattern FADING_IN 87 | , getChunk 88 | 89 | -- * Groups 90 | , reserveChannels 91 | , Tag 92 | , groupChannel 93 | , groupChannels 94 | , groupCount 95 | , groupAvailable 96 | , groupOldest 97 | , groupNewer 98 | , fadeOutGroup 99 | , haltGroup 100 | 101 | -- * Music 102 | , getNumMusicDecoders 103 | , getMusicDecoder 104 | , Music 105 | , loadMUS 106 | , loadMUS_RW 107 | , loadMUSType_RW 108 | , freeMusic 109 | , playMusic 110 | , fadeInMusic 111 | , fadeInMusicPos 112 | , hookMusic 113 | , volumeMusic 114 | , pauseMusic 115 | , resumeMusic 116 | , rewindMusic 117 | , setMusicPosition 118 | , setMusicCMD 119 | , haltMusic 120 | , fadeOutMusic 121 | , wrapMusicCallback 122 | , hookMusicFinished 123 | , MusicType 124 | , getMusicType 125 | , pattern MUS_NONE 126 | , pattern MUS_CMD 127 | , pattern MUS_WAV 128 | , pattern MUS_MOD 129 | , pattern MUS_MID 130 | , pattern MUS_OGG 131 | , pattern MUS_MP3 132 | , pattern MUS_FLAC 133 | , playingMusic 134 | , pausedMusic 135 | , fadingMusic 136 | , getMusicHookData 137 | 138 | -- * Effects 139 | , Effect 140 | , wrapEffect 141 | , EffectFinished 142 | , wrapEffectFinished 143 | , registerEffect 144 | , pattern CHANNEL_POST 145 | , unregisterEffect 146 | , unregisterAllEffects 147 | , setPostMix 148 | , setPanning 149 | , setDistance 150 | , setPosition 151 | , setReverseStereo 152 | 153 | -- * MikMod 154 | , setSynchroValue 155 | , getSynchroValue 156 | 157 | -- * MIDI backends 158 | , setSoundFonts 159 | , getSoundFonts 160 | , eachSoundFont 161 | 162 | ) where 163 | 164 | #include "SDL_mixer.h" 165 | 166 | import Data.Int (Int16) 167 | import Data.Word (Word8, Word16, Word32) 168 | import Foreign.C.String (CString) 169 | import Foreign.C.Types (CInt(..), CDouble(..)) 170 | import Foreign.Ptr (Ptr, FunPtr) 171 | import Foreign.Storable (Storable(..)) 172 | import Prelude hiding (init) 173 | import SDL.Raw.Helper (liftF) 174 | import SDL.Raw.Types (RWops(..), Version(..)) 175 | 176 | -- 4.1 General 177 | 178 | liftF "getVersion" "Mix_Linked_Version" 179 | [t|IO (Ptr Version)|] 180 | 181 | pattern SDL_MIXER_MAJOR_VERSION = (#const SDL_MIXER_MAJOR_VERSION) 182 | pattern SDL_MIXER_MINOR_VERSION = (#const SDL_MIXER_MINOR_VERSION) 183 | pattern SDL_MIXER_PATCHLEVEL = (#const SDL_MIXER_PATCHLEVEL) 184 | 185 | type InitFlag = CInt 186 | 187 | liftF "init" "Mix_Init" 188 | [t|InitFlag -> IO CInt|] 189 | 190 | pattern INIT_FLAC = (#const MIX_INIT_FLAC) 191 | pattern INIT_MOD = (#const MIX_INIT_MOD) 192 | pattern INIT_MP3 = (#const MIX_INIT_MP3) 193 | pattern INIT_OGG = (#const MIX_INIT_OGG) 194 | 195 | liftF "quit" "Mix_Quit" 196 | [t|IO ()|] 197 | 198 | type Format = Word16 199 | 200 | pattern DEFAULT_FREQUENCY = (#const MIX_DEFAULT_FREQUENCY) 201 | pattern DEFAULT_CHANNELS = (#const MIX_DEFAULT_CHANNELS) 202 | 203 | liftF "openAudio" "Mix_OpenAudio" 204 | [t|CInt -> Format -> CInt -> CInt -> IO CInt|] 205 | 206 | pattern AUDIO_U8 = (#const AUDIO_U8) 207 | pattern AUDIO_S8 = (#const AUDIO_S8) 208 | pattern AUDIO_U16LSB = (#const AUDIO_U16LSB) 209 | pattern AUDIO_S16LSB = (#const AUDIO_S16LSB) 210 | pattern AUDIO_U16MSB = (#const AUDIO_U16MSB) 211 | pattern AUDIO_S16MSB = (#const AUDIO_S16MSB) 212 | pattern AUDIO_U16 = (#const AUDIO_U16) 213 | pattern AUDIO_S16 = (#const AUDIO_S16) 214 | pattern AUDIO_U16SYS = (#const AUDIO_U16SYS) 215 | pattern AUDIO_S16SYS = (#const AUDIO_S16SYS) 216 | pattern DEFAULT_FORMAT = (#const MIX_DEFAULT_FORMAT) 217 | 218 | liftF "closeAudio" "Mix_CloseAudio" 219 | [t|IO ()|] 220 | 221 | liftF "querySpec" "Mix_QuerySpec" 222 | [t|Ptr CInt -> Ptr Format -> Ptr CInt -> IO CInt|] 223 | 224 | -- 4.2 Samples 225 | 226 | liftF "getNumChunkDecoders" "Mix_GetNumChunkDecoders" 227 | [t|IO CInt|] 228 | 229 | liftF "getChunkDecoder" "Mix_GetChunkDecoder" 230 | [t|CInt -> IO CString|] 231 | 232 | data Chunk = Chunk 233 | { chunkAllocated :: CInt 234 | , chunkAbuf :: Ptr Word8 235 | , chunkAlen :: Word32 236 | , chunkVolume :: Word8 237 | } deriving (Eq, Show) 238 | 239 | instance Storable Chunk where 240 | alignment = sizeOf 241 | sizeOf _ = (#size Mix_Chunk) 242 | 243 | peek ptr = 244 | Chunk 245 | <$> (#peek Mix_Chunk, allocated) ptr 246 | <*> (#peek Mix_Chunk, abuf) ptr 247 | <*> (#peek Mix_Chunk, alen) ptr 248 | <*> (#peek Mix_Chunk, volume) ptr 249 | 250 | poke ptr (Chunk {..}) = do 251 | (#poke Mix_Chunk, allocated) ptr chunkAllocated 252 | (#poke Mix_Chunk, abuf) ptr chunkAbuf 253 | (#poke Mix_Chunk, alen) ptr chunkAlen 254 | (#poke Mix_Chunk, volume) ptr chunkVolume 255 | 256 | liftF "loadWAV" "Mix_LoadWAV_helper" 257 | [t|CString -> IO (Ptr Chunk)|] 258 | 259 | liftF "loadWAV_RW" "Mix_LoadWAV_RW" 260 | [t|Ptr RWops -> CInt -> IO (Ptr Chunk)|] 261 | 262 | liftF "quickLoadWAV" "Mix_QuickLoad_WAV" 263 | [t|Ptr Word8 -> IO (Ptr Chunk)|] 264 | 265 | liftF "quickLoadRaw" "Mix_QuickLoad_RAW" 266 | [t|Ptr Word8 -> IO (Ptr Chunk)|] 267 | 268 | pattern MAX_VOLUME = (#const MIX_MAX_VOLUME) 269 | 270 | liftF "volumeChunk" "Mix_VolumeChunk" 271 | [t|Ptr Chunk -> CInt -> IO CInt|] 272 | 273 | liftF "freeChunk" "Mix_FreeChunk" 274 | [t|Ptr Chunk -> IO ()|] 275 | 276 | -- 4.3 Channels 277 | 278 | liftF "allocateChannels" "Mix_AllocateChannels" 279 | [t|CInt -> IO CInt|] 280 | 281 | pattern CHANNELS = (#const MIX_CHANNELS) 282 | 283 | type Channel = CInt 284 | 285 | liftF "volume" "Mix_Volume" 286 | [t|Channel -> CInt -> IO CInt|] 287 | 288 | liftF "playChannel" "Mix_PlayChannel_helper" 289 | [t|Channel -> Ptr Chunk -> CInt -> IO CInt|] 290 | 291 | liftF "playChannelTimed" "Mix_PlayChannelTimed" 292 | [t|Channel -> Ptr Chunk -> CInt -> CInt -> IO CInt|] 293 | 294 | liftF "fadeInChannel" "Mix_FadeInChannel_helper" 295 | [t|Channel -> Ptr Chunk -> CInt -> CInt -> IO CInt|] 296 | 297 | liftF "fadeInChannelTimed" "Mix_FadeInChannelTimed" 298 | [t|Channel -> Ptr Chunk -> CInt -> CInt -> CInt -> IO CInt|] 299 | 300 | liftF "pause" "Mix_Pause" 301 | [t|Channel -> IO ()|] 302 | 303 | liftF "resume" "Mix_Resume" 304 | [t|Channel -> IO ()|] 305 | 306 | liftF "haltChannel" "Mix_HaltChannel" 307 | [t|Channel -> IO CInt|] 308 | 309 | liftF "expireChannel" "Mix_ExpireChannel" 310 | [t|Channel -> CInt -> IO CInt|] 311 | 312 | liftF "fadeOutChannel" "Mix_FadeOutChannel" 313 | [t|Channel -> CInt -> IO CInt|] 314 | 315 | foreign import ccall "wrapper" 316 | wrapChannelCallback :: (Channel -> IO ()) -> IO (FunPtr (Channel -> IO ())) 317 | 318 | liftF "channelFinished" "Mix_ChannelFinished" 319 | [t|FunPtr (Channel -> IO ()) -> IO ()|] 320 | 321 | liftF "playing" "Mix_Playing" 322 | [t|Channel -> IO CInt|] 323 | 324 | liftF "paused" "Mix_Paused" 325 | [t|Channel -> IO CInt|] 326 | 327 | type Fading = (#type Mix_Fading) 328 | 329 | pattern NO_FADING = (#const MIX_NO_FADING) 330 | pattern FADING_IN = (#const MIX_FADING_IN) 331 | pattern FADING_OUT = (#const MIX_FADING_OUT) 332 | 333 | liftF "fadingChannel" "Mix_FadingChannel" 334 | [t|Channel -> IO Fading|] 335 | 336 | liftF "getChunk" "Mix_GetChunk" 337 | [t|Channel -> IO (Ptr Chunk)|] 338 | 339 | -- 4.4 Groups 340 | 341 | liftF "reserveChannels" "Mix_ReserveChannels" 342 | [t|CInt -> IO CInt|] 343 | 344 | type Tag = CInt 345 | 346 | liftF "groupChannel" "Mix_GroupChannel" 347 | [t|Channel -> Tag -> IO CInt|] 348 | 349 | liftF "groupChannels" "Mix_GroupChannels" 350 | [t|Channel -> Channel -> Tag -> IO CInt|] 351 | 352 | liftF "groupCount" "Mix_GroupCount" 353 | [t|Tag -> IO CInt|] 354 | 355 | liftF "groupAvailable" "Mix_GroupAvailable" 356 | [t|Tag -> IO CInt|] 357 | 358 | liftF "groupOldest" "Mix_GroupOldest" 359 | [t|Tag -> IO CInt|] 360 | 361 | liftF "groupNewer" "Mix_GroupNewer" 362 | [t|Tag -> IO CInt|] 363 | 364 | liftF "fadeOutGroup" "Mix_FadeOutGroup" 365 | [t|Tag -> CInt -> IO CInt|] 366 | 367 | liftF "haltGroup" "Mix_HaltGroup" 368 | [t|Tag -> IO CInt|] 369 | 370 | -- 4.5 Music 371 | 372 | liftF "getNumMusicDecoders" "Mix_GetNumMusicDecoders" 373 | [t|IO CInt|] 374 | 375 | liftF "getMusicDecoder" "Mix_GetMusicDecoder" 376 | [t|CInt -> IO CString|] 377 | 378 | data Music 379 | 380 | liftF "loadMUS" "Mix_LoadMUS" 381 | [t|CString -> IO (Ptr Music)|] 382 | 383 | liftF "loadMUS_RW" "Mix_LoadMUS_RW" 384 | [t|Ptr RWops -> CInt -> IO (Ptr Music)|] 385 | 386 | type MusicType = (#type Mix_MusicType) 387 | 388 | liftF "loadMUSType_RW" "Mix_LoadMUSType_RW" 389 | [t|Ptr RWops -> MusicType -> CInt -> IO (Ptr Music)|] 390 | 391 | pattern MUS_NONE = (#const MUS_NONE) 392 | pattern MUS_CMD = (#const MUS_CMD) 393 | pattern MUS_WAV = (#const MUS_WAV) 394 | pattern MUS_MOD = (#const MUS_MOD) 395 | pattern MUS_MID = (#const MUS_MID) 396 | pattern MUS_OGG = (#const MUS_OGG) 397 | pattern MUS_MP3 = (#const MUS_MP3) 398 | pattern MUS_FLAC = (#const MUS_FLAC) 399 | 400 | liftF "freeMusic" "Mix_FreeMusic" 401 | [t|Ptr Music -> IO ()|] 402 | 403 | liftF "playMusic" "Mix_PlayMusic" 404 | [t|Ptr Music -> CInt -> IO CInt|] 405 | 406 | liftF "fadeInMusic" "Mix_FadeInMusic" 407 | [t|Ptr Music -> CInt -> CInt -> IO CInt|] 408 | 409 | liftF "fadeInMusicPos" "Mix_FadeInMusicPos" 410 | [t|Ptr Music -> CInt -> CInt -> CDouble -> IO CInt|] 411 | 412 | liftF "hookMusic" "Mix_HookMusic" 413 | [t|FunPtr (Ptr () -> Ptr Word8 -> CInt -> IO ()) -> Ptr () -> IO ()|] 414 | 415 | liftF "volumeMusic" "Mix_VolumeMusic" 416 | [t|CInt -> IO CInt|] 417 | 418 | liftF "pauseMusic" "Mix_PauseMusic" 419 | [t|IO ()|] 420 | 421 | liftF "resumeMusic" "Mix_ResumeMusic" 422 | [t|IO ()|] 423 | 424 | liftF "rewindMusic" "Mix_RewindMusic" 425 | [t|IO ()|] 426 | 427 | liftF "setMusicPosition" "Mix_SetMusicPosition" 428 | [t|CDouble -> IO CInt|] 429 | 430 | liftF "setMusicCMD" "Mix_SetMusicCMD" 431 | [t|CString -> IO CInt|] 432 | 433 | liftF "haltMusic" "Mix_HaltMusic" 434 | [t|IO CInt|] 435 | 436 | liftF "fadeOutMusic" "Mix_FadeOutMusic" 437 | [t|CInt -> IO CInt|] 438 | 439 | foreign import ccall "wrapper" 440 | wrapMusicCallback :: IO () -> IO (FunPtr (IO ())) 441 | 442 | liftF "hookMusicFinished" "Mix_HookMusicFinished" 443 | [t|FunPtr (IO ()) -> IO ()|] 444 | 445 | liftF "getMusicType" "Mix_GetMusicType" 446 | [t|Ptr Music -> IO MusicType|] 447 | 448 | liftF "playingMusic" "Mix_PlayingMusic" 449 | [t|IO CInt|] 450 | 451 | liftF "pausedMusic" "Mix_PausedMusic" 452 | [t|IO CInt|] 453 | 454 | liftF "fadingMusic" "Mix_FadingChannel" 455 | [t|IO Fading|] 456 | 457 | liftF "getMusicHookData" "Mix_GetMusicHookData" 458 | [t|IO (Ptr ())|] 459 | 460 | -- 4.6 Effects 461 | 462 | pattern CHANNEL_POST = (#const MIX_CHANNEL_POST) 463 | 464 | type Effect = Channel -> Ptr () -> CInt -> Ptr() -> IO () 465 | 466 | foreign import ccall "wrapper" 467 | wrapEffect :: Effect -> IO (FunPtr Effect) 468 | 469 | type EffectFinished = Channel -> Ptr () -> IO () 470 | 471 | foreign import ccall "wrapper" 472 | wrapEffectFinished :: EffectFinished -> IO (FunPtr EffectFinished) 473 | 474 | liftF "registerEffect" "Mix_RegisterEffect" 475 | [t|Channel -> FunPtr Effect -> FunPtr EffectFinished -> Ptr () -> IO CInt|] 476 | 477 | liftF "unregisterEffect" "Mix_UnregisterEffect" 478 | [t|Channel -> FunPtr Effect -> IO CInt|] 479 | 480 | liftF "unregisterAllEffects" "Mix_UnregisterAllEffects" 481 | [t|Channel -> IO CInt|] 482 | 483 | liftF "setPostMix" "Mix_SetPostMix" 484 | [t|FunPtr (Ptr () -> Ptr Word8 -> CInt -> IO ()) -> Ptr () -> IO ()|] 485 | 486 | liftF "setPanning" "Mix_SetPanning" 487 | [t|Channel -> Word8 -> Word8 -> IO CInt|] 488 | 489 | liftF "setDistance" "Mix_SetDistance" 490 | [t|Channel -> Word8 -> IO CInt|] 491 | 492 | liftF "setPosition" "Mix_SetPosition" 493 | [t|Channel -> Int16 -> Word8 -> IO CInt|] 494 | 495 | liftF "setReverseStereo" "Mix_SetReverseStereo" 496 | [t|Channel -> CInt -> IO CInt|] 497 | 498 | -- ?.? Not documented 499 | 500 | liftF "setSynchroValue" "Mix_SetSynchroValue" 501 | [t|CInt -> IO CInt|] 502 | 503 | liftF "getSynchroValue" "Mix_GetSynchroValue" 504 | [t|IO CInt|] 505 | 506 | liftF "setSoundFonts" "Mix_SetSoundFonts" 507 | [t|Ptr CString -> IO CInt|] 508 | 509 | liftF "getSoundFonts" "Mix_GetSoundFonts" 510 | [t|IO (Ptr CString)|] 511 | 512 | liftF "eachSoundFont" "Mix_EachSoundFont" 513 | [t|FunPtr (CString -> Ptr () -> IO CInt) -> Ptr () -> IO CInt|] 514 | -------------------------------------------------------------------------------- /src/SDL/Mixer.hs: -------------------------------------------------------------------------------- 1 | {-| 2 | 3 | Module : SDL.Mixer 4 | License : BSD3 5 | Stability : experimental 6 | 7 | Bindings to the @SDL2_mixer@ library. 8 | 9 | -} 10 | 11 | {-# LANGUAGE FlexibleContexts #-} 12 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 13 | {-# LANGUAGE LambdaCase #-} 14 | {-# LANGUAGE OverloadedStrings #-} 15 | {-# LANGUAGE PatternSynonyms #-} 16 | {-# LANGUAGE RecordWildCards #-} 17 | {-# LANGUAGE ScopedTypeVariables #-} 18 | {-# LANGUAGE TypeFamilies #-} 19 | 20 | module SDL.Mixer 21 | ( 22 | -- * Audio setup 23 | -- 24 | -- | In order to use the rest of the library, you need to 25 | -- supply 'withAudio' or 'openAudio' with an 'Audio' configuration. 26 | withAudio 27 | , Audio(..) 28 | , Format(..) 29 | , Output(..) 30 | , defaultAudio 31 | , ChunkSize 32 | , queryAudio 33 | 34 | -- ** Alternative 35 | , openAudio 36 | , closeAudio 37 | 38 | -- * Loading audio data 39 | -- 40 | -- | Use 'load' or 'decode' to get both 'Chunk' and 'Music' values. 41 | , Loadable(..) 42 | , Chunk(..) 43 | , chunkDecoders 44 | , Music(..) 45 | , musicDecoders 46 | 47 | -- * Chunks 48 | -- 49 | -- | 'Chunk's are played on 'Channel's, which can be combined into 'Group's. 50 | 51 | -- ** Playing chunks 52 | , Channel 53 | , pattern AllChannels 54 | , setChannels 55 | , getChannels 56 | , play 57 | , playForever 58 | , Times 59 | , pattern Once 60 | , pattern Forever 61 | , playOn 62 | , Milliseconds 63 | , Limit 64 | , pattern NoLimit 65 | , playLimit 66 | , fadeIn 67 | , fadeInOn 68 | , fadeInLimit 69 | 70 | -- ** Grouping channels 71 | , reserveChannels 72 | , Group 73 | , pattern DefaultGroup 74 | , group 75 | , groupSpan 76 | , groupCount 77 | , getAvailable 78 | , getOldest 79 | , getNewest 80 | 81 | -- ** Controlling playback 82 | , pause 83 | , resume 84 | , halt 85 | , haltAfter 86 | , haltGroup 87 | 88 | -- ** Setting the volume 89 | , Volume 90 | , HasVolume(..) 91 | 92 | -- ** Querying for status 93 | , playing 94 | , playingCount 95 | , paused 96 | , pausedCount 97 | , playedLast 98 | , Fading 99 | , fading 100 | 101 | -- ** Fading out 102 | , fadeOut 103 | , fadeOutGroup 104 | 105 | -- ** Reacting to finish 106 | , whenChannelFinished 107 | 108 | -- * Music 109 | -- 110 | -- | 'Chunk's and 'Music' differ by the way they are played. While multiple 111 | -- 'Chunk's can be played on different desired 'Channel's at the same time, 112 | -- there can only be one 'Music' playing at the same time. 113 | -- 114 | -- Therefore, the functions used for 'Music' are separate. 115 | 116 | -- ** Playing music 117 | , playMusic 118 | , Position 119 | , fadeInMusic 120 | , fadeInMusicAt 121 | , fadeInMusicAtMOD 122 | 123 | -- ** Controlling playback 124 | , pauseMusic 125 | , haltMusic 126 | , resumeMusic 127 | , rewindMusic 128 | , setMusicPosition 129 | , setMusicPositionMOD 130 | 131 | -- ** Setting the volume 132 | , setMusicVolume 133 | , getMusicVolume 134 | 135 | -- ** Querying for status 136 | , playingMusic 137 | , pausedMusic 138 | , fadingMusic 139 | , MusicType(..) 140 | , musicType 141 | , playingMusicType 142 | 143 | -- ** Fading out 144 | , fadeOutMusic 145 | 146 | -- ** Reacting to finish 147 | , whenMusicFinished 148 | 149 | -- * Effects 150 | , Effect 151 | , EffectFinished 152 | , pattern PostProcessing 153 | , effect 154 | 155 | -- ** In-built effects 156 | , effectPan 157 | , effectDistance 158 | , effectPosition 159 | , effectReverseStereo 160 | 161 | -- * Other 162 | , initialize 163 | , InitFlag(..) 164 | , quit 165 | , version 166 | 167 | ) where 168 | 169 | import Control.Exception (throwIO) 170 | import Control.Exception.Lifted (finally) 171 | import Control.Monad (void, forM, when) 172 | import Control.Monad.IO.Class (MonadIO, liftIO) 173 | import Control.Monad.Trans.Control (MonadBaseControl) 174 | import Data.Bits ((.|.), (.&.)) 175 | import Data.ByteString (ByteString, readFile) 176 | import Data.ByteString.Unsafe (unsafeUseAsCStringLen) 177 | import Data.Default.Class (Default(def)) 178 | import Data.Foldable (foldl) 179 | import Data.IORef (IORef, newIORef, readIORef, writeIORef) 180 | import Data.Int (Int16) 181 | import Data.Vector.Storable.Mutable (IOVector, unsafeFromForeignPtr0) 182 | import Data.Word (Word8) 183 | import Foreign.C.String (peekCString) 184 | import Foreign.C.Types (CInt) 185 | import Foreign.ForeignPtr (newForeignPtr_, castForeignPtr) 186 | import Foreign.Marshal.Alloc (alloca) 187 | import Foreign.Ptr (FunPtr, nullFunPtr, freeHaskellFunPtr) 188 | import Foreign.Ptr (Ptr, castPtr, nullPtr) 189 | import Foreign.Storable (Storable(..)) 190 | import Prelude hiding (foldl, readFile) 191 | import SDL (SDLException(SDLCallFailed)) 192 | import SDL.Internal.Exception 193 | import SDL.Raw.Filesystem (rwFromConstMem) 194 | import System.IO.Unsafe (unsafePerformIO) 195 | 196 | import qualified SDL.Raw 197 | import qualified SDL.Raw.Mixer 198 | 199 | -- | Initialize the library by loading support for a certain set of 200 | -- sample/music formats. 201 | -- 202 | -- Note that calling this is not strictly necessary: support for a certain 203 | -- format will be loaded automatically when attempting to load data in that 204 | -- format. Using 'initialize' allows you to decide /when/ to load support. 205 | -- 206 | -- You may call this function multiple times. 207 | initialize :: (Foldable f, MonadIO m) => f InitFlag -> m () 208 | initialize flags = do 209 | let raw = foldl (\a b -> a .|. initToCInt b) 0 flags 210 | throwIf_ ((/= raw) . (.&. raw)) "SDL.Mixer.initialize" "Mix_Init" $ 211 | SDL.Raw.Mixer.init raw 212 | 213 | -- | Used with 'initialize' to designate loading support for a particular 214 | -- sample/music format. 215 | data InitFlag 216 | = InitFLAC 217 | | InitMOD 218 | | InitMP3 219 | | InitOGG 220 | deriving (Eq, Ord, Bounded, Read, Show) 221 | 222 | initToCInt :: InitFlag -> CInt 223 | initToCInt = \case 224 | InitFLAC -> SDL.Raw.Mixer.INIT_FLAC 225 | InitMOD -> SDL.Raw.Mixer.INIT_MOD 226 | InitMP3 -> SDL.Raw.Mixer.INIT_MP3 227 | InitOGG -> SDL.Raw.Mixer.INIT_OGG 228 | 229 | -- | Cleans up any loaded libraries, freeing memory. 230 | quit :: MonadIO m => m () 231 | quit = SDL.Raw.Mixer.quit -- FIXME: May not free all init'd libs! Check docs. 232 | 233 | -- | Gets the major, minor, patch versions of the linked @SDL2_mixer@ library. 234 | version :: (Integral a, MonadIO m) => m (a, a, a) 235 | version = liftIO $ do 236 | SDL.Raw.Version major minor patch <- peek =<< SDL.Raw.Mixer.getVersion 237 | return (fromIntegral major, fromIntegral minor, fromIntegral patch) 238 | 239 | -- | Initializes the @SDL2_mixer@ API. 240 | -- 241 | -- This should be the first function you call after initializing @SDL@ itself 242 | -- with 'SDL.Init.InitAudio'. 243 | -- 244 | -- Automatically cleans up the API when the inner computation finishes. 245 | withAudio 246 | :: (MonadBaseControl IO m, MonadIO m) => Audio -> ChunkSize -> m a -> m a 247 | withAudio conf csize act = do 248 | openAudio conf csize 249 | finally act closeAudio 250 | 251 | -- | An alternative to 'withAudio', also initializes the @SDL2_mixer@ API. 252 | -- 253 | -- However, 'openAudio' does not take care of automatically calling 254 | -- 'closeAudio' after a computation ends, so you have to take care to do so 255 | -- manually. 256 | openAudio :: MonadIO m => Audio -> ChunkSize -> m () 257 | openAudio (Audio {..}) chunkSize = 258 | throwIfNeg_ "SDL.Mixer.openAudio" "Mix_OpenAudio" $ 259 | SDL.Raw.Mixer.openAudio 260 | (fromIntegral audioFrequency) 261 | (formatToWord audioFormat) 262 | (outputToCInt audioOutput) 263 | (fromIntegral chunkSize) 264 | 265 | -- | An audio configuration. Use this with 'withAudio'. 266 | data Audio = Audio 267 | { audioFrequency :: Int -- ^ A sampling frequency. 268 | , audioFormat :: Format -- ^ An output sample format. 269 | , audioOutput :: Output -- ^ 'Mono' or 'Stereo' output. 270 | } deriving (Eq, Read, Show) 271 | 272 | instance Default Audio where 273 | def = Audio { audioFrequency = SDL.Raw.Mixer.DEFAULT_FREQUENCY 274 | , audioFormat = wordToFormat SDL.Raw.Mixer.DEFAULT_FORMAT 275 | , audioOutput = cIntToOutput SDL.Raw.Mixer.DEFAULT_CHANNELS 276 | } 277 | 278 | -- | A default 'Audio' configuration. 279 | -- 280 | -- Same as 'Data.Default.Class.def'. 281 | -- 282 | -- Uses 22050 as the 'audioFrequency', 'FormatS16_Sys' as the 'audioFormat' and 283 | -- 'Stereo' as the 'audioOutput'. 284 | defaultAudio :: Audio 285 | defaultAudio = def 286 | 287 | -- | The size of each mixed sample. 288 | -- 289 | -- The smaller this is, the more often callbacks will be invoked. If this is 290 | -- made too small on a slow system, the sounds may skip. If made too large, 291 | -- sound effects could lag. 292 | type ChunkSize = Int 293 | 294 | -- | A sample format. 295 | data Format 296 | = FormatU8 -- ^ Unsigned 8-bit samples. 297 | | FormatS8 -- ^ Signed 8-bit samples. 298 | | FormatU16_LSB -- ^ Unsigned 16-bit samples, in little-endian byte order. 299 | | FormatS16_LSB -- ^ Signed 16-bit samples, in little-endian byte order. 300 | | FormatU16_MSB -- ^ Unsigned 16-bit samples, in big-endian byte order. 301 | | FormatS16_MSB -- ^ signed 16-bit samples, in big-endian byte order. 302 | | FormatU16_Sys -- ^ Unsigned 16-bit samples, in system byte order. 303 | | FormatS16_Sys -- ^ Signed 16-bit samples, in system byte order. 304 | deriving (Eq, Ord, Bounded, Read, Show) 305 | 306 | formatToWord :: Format -> SDL.Raw.Mixer.Format 307 | formatToWord = \case 308 | FormatU8 -> SDL.Raw.Mixer.AUDIO_U8 309 | FormatS8 -> SDL.Raw.Mixer.AUDIO_S8 310 | FormatU16_LSB -> SDL.Raw.Mixer.AUDIO_U16LSB 311 | FormatS16_LSB -> SDL.Raw.Mixer.AUDIO_S16LSB 312 | FormatU16_MSB -> SDL.Raw.Mixer.AUDIO_U16MSB 313 | FormatS16_MSB -> SDL.Raw.Mixer.AUDIO_S16MSB 314 | FormatU16_Sys -> SDL.Raw.Mixer.AUDIO_U16SYS 315 | FormatS16_Sys -> SDL.Raw.Mixer.AUDIO_S16SYS 316 | 317 | wordToFormat :: SDL.Raw.Mixer.Format -> Format 318 | wordToFormat = \case 319 | SDL.Raw.Mixer.AUDIO_U8 -> FormatU8 320 | SDL.Raw.Mixer.AUDIO_S8 -> FormatS8 321 | SDL.Raw.Mixer.AUDIO_U16LSB -> FormatU16_LSB 322 | SDL.Raw.Mixer.AUDIO_S16LSB -> FormatS16_LSB 323 | SDL.Raw.Mixer.AUDIO_U16MSB -> FormatU16_MSB 324 | SDL.Raw.Mixer.AUDIO_S16MSB -> FormatS16_MSB 325 | SDL.Raw.Mixer.AUDIO_U16SYS -> FormatU16_Sys 326 | SDL.Raw.Mixer.AUDIO_S16SYS -> FormatS16_Sys 327 | _ -> error "SDL.Mixer.wordToFormat: unknown Format." 328 | 329 | -- | The number of sound channels in output. 330 | data Output = Mono | Stereo 331 | deriving (Eq, Ord, Bounded, Read, Show) 332 | 333 | outputToCInt :: Output -> CInt 334 | outputToCInt = \case 335 | Mono -> 1 336 | Stereo -> 2 337 | 338 | cIntToOutput :: CInt -> Output 339 | cIntToOutput = \case 340 | 1 -> Mono 341 | 2 -> Stereo 342 | _ -> error "SDL.Mixer.cIntToOutput: unknown number of channels." 343 | 344 | -- | Get the audio format in use by the opened audio device. 345 | -- 346 | -- This may or may not match the 'Audio' you asked for when calling 347 | -- 'withAudio'. 348 | queryAudio :: MonadIO m => m Audio 349 | queryAudio = 350 | liftIO . 351 | alloca $ \freq -> 352 | alloca $ \form -> 353 | alloca $ \chan -> do 354 | void . throwIf0 "SDL.Mixer.queryAudio" "Mix_QuerySpec" $ 355 | SDL.Raw.Mixer.querySpec freq form chan 356 | Audio 357 | <$> (fromIntegral <$> peek freq) 358 | <*> (wordToFormat <$> peek form) 359 | <*> (cIntToOutput <$> peek chan) 360 | 361 | -- | Shut down and clean up the @SDL2_mixer@ API. 362 | -- 363 | -- After calling this, all audio stops. 364 | -- 365 | -- You don't have to call this if you're using 'withAudio'. 366 | closeAudio :: MonadIO m => m () 367 | closeAudio = SDL.Raw.Mixer.closeAudio 368 | 369 | -- | A class of all values that can be loaded from some source. You can load 370 | -- both 'Chunk's and 'Music' this way. 371 | -- 372 | -- Note that you must call 'withAudio' before using these, since they have to 373 | -- know the audio configuration to properly convert the data for playback. 374 | class Loadable a where 375 | 376 | -- | Load the value from a 'ByteString'. 377 | decode :: MonadIO m => ByteString -> m a 378 | 379 | -- | Same as 'decode', but loads from a file instead. 380 | load :: MonadIO m => FilePath -> m a 381 | load = (decode =<<) . liftIO . readFile 382 | 383 | -- | Frees the value's memory. It should no longer be used. 384 | -- 385 | -- __Note that you shouldn't free those values that are currently playing.__ 386 | free :: MonadIO m => a -> m () 387 | 388 | -- | A volume, where 0 is silent and 128 loudest. 389 | -- 390 | -- 'Volume's lesser than 0 or greater than 128 function as if they are 0 and 391 | -- 128, respectively. 392 | type Volume = Int 393 | 394 | volumeToCInt :: Volume -> CInt 395 | volumeToCInt = fromIntegral . max 0 . min 128 396 | 397 | -- | A class of all values that have a 'Volume'. 398 | class HasVolume a where 399 | 400 | -- | Gets the value's currently set 'Volume'. 401 | -- 402 | -- If the value is a 'Channel' and 'AllChannels' is used, gets the /average/ 403 | -- 'Volume' of all 'Channel's. 404 | getVolume :: MonadIO m => a -> m Volume 405 | 406 | -- | Sets a value's 'Volume'. 407 | -- 408 | -- If the value is a 'Chunk', the volume setting only takes effect when the 409 | -- 'Chunk' is used on a 'Channel', being mixed into the output. 410 | -- 411 | -- In case of being used on a 'Channel', the volume setting takes effect 412 | -- during the final mix, along with the 'Chunk' volume. For instance, setting 413 | -- the 'Volume' of a certain 'Channel' to 64 will halve the volume of all 414 | -- 'Chunk's played on that 'Channel'. If 'AllChannels' is used, sets all 415 | -- 'Channel's to the given 'Volume' instead. 416 | setVolume :: MonadIO m => Volume -> a -> m () 417 | 418 | -- | Returns the names of all chunk decoders currently available. 419 | -- 420 | -- These depend on the availability of shared libraries for each of the 421 | -- formats. The list may contain any of the following, and possibly others: 422 | -- @WAVE@, @AIFF@, @VOC@, @OFF@, @FLAC@, @MP3@. 423 | chunkDecoders :: MonadIO m => m [String] 424 | chunkDecoders = 425 | liftIO $ do 426 | num <- SDL.Raw.Mixer.getNumChunkDecoders 427 | forM [0 .. num - 1] $ \i -> 428 | SDL.Raw.Mixer.getChunkDecoder i >>= peekCString 429 | 430 | -- | A loaded audio chunk. 431 | newtype Chunk = Chunk (Ptr SDL.Raw.Mixer.Chunk) deriving (Eq, Show) 432 | 433 | instance Loadable Chunk where 434 | decode bytes = liftIO $ do 435 | unsafeUseAsCStringLen bytes $ \(cstr, len) -> do 436 | rw <- rwFromConstMem (castPtr cstr) (fromIntegral len) 437 | fmap Chunk . 438 | throwIfNull "SDL.Mixer.decode" "Mix_LoadWAV_RW" $ 439 | SDL.Raw.Mixer.loadWAV_RW rw 0 440 | 441 | free (Chunk p) = liftIO $ SDL.Raw.Mixer.freeChunk p 442 | 443 | instance HasVolume Chunk where 444 | getVolume (Chunk p) = fmap fromIntegral $ SDL.Raw.Mixer.volumeChunk p (-1) 445 | setVolume v (Chunk p) = void . SDL.Raw.Mixer.volumeChunk p $ volumeToCInt v 446 | 447 | -- | A mixing channel. 448 | -- 449 | -- Use the 'Integral' instance to define these: the first channel is 0, the 450 | -- second 1 and so on. 451 | -- 452 | -- The default number of 'Channel's available at startup is 8, so note that you 453 | -- cannot usemore than these starting 8 if you haven't created more with 454 | -- 'setChannels'. 455 | -- 456 | -- The starting 'Volume' of each 'Channel' is the maximum: 128. 457 | newtype Channel = Channel CInt deriving (Eq, Ord, Enum, Integral, Real, Num) 458 | 459 | instance Show Channel where 460 | show = \case 461 | AllChannels -> "AllChannels" 462 | Channel c -> "Channel " ++ show c 463 | 464 | -- The lowest-numbered channel is CHANNEL_POST, or -2, for post processing 465 | -- effects. This function makes sure a channel is higher than CHANNEL_POST. 466 | clipChan :: CInt -> CInt 467 | clipChan = max SDL.Raw.Mixer.CHANNEL_POST 468 | 469 | -- | Prepares a given number of 'Channel's for use. 470 | -- 471 | -- There are 8 such 'Channel's already prepared for use after 'withAudio' is 472 | -- called. 473 | -- 474 | -- You may call this multiple times, even with sounds playing. If setting a 475 | -- lesser number of 'Channel's than are currently in use, the higher 'Channel's 476 | -- will be stopped, their finish callbacks invoked, and their memory freed. 477 | -- Passing in 0 or less will therefore stop and free all mixing channels. 478 | -- 479 | -- Any 'Music' playing is not affected by this function. 480 | setChannels :: MonadIO m => Int -> m () 481 | setChannels = void . SDL.Raw.Mixer.allocateChannels . fromIntegral . max 0 482 | 483 | -- | Gets the number of 'Channel's currently in use. 484 | getChannels :: MonadIO m => m Int 485 | getChannels = fromIntegral <$> SDL.Raw.Mixer.allocateChannels (-1) 486 | 487 | -- | Reserve a given number of 'Channel's, starting from 'Channel' 0. 488 | -- 489 | -- A reserved 'Channel' is considered not to be available for playing samples 490 | -- when using any 'play' or 'fadeIn' function variant with 'AllChannels'. In 491 | -- other words, whenever you let 'SDL.Mixer' pick the first available 'Channel' 492 | -- itself, these reserved 'Channel's will not be considered. 493 | reserveChannels :: MonadIO m => Int -> m Int 494 | reserveChannels = 495 | fmap fromIntegral . SDL.Raw.Mixer.reserveChannels . fromIntegral 496 | 497 | -- | Gets the most recent 'Chunk' played on a 'Channel', if any. 498 | -- 499 | -- Using 'AllChannels' is not valid here, and will return 'Nothing'. 500 | -- 501 | -- Note that the returned 'Chunk' might be invalid if it was already 'free'd. 502 | playedLast :: MonadIO m => Channel -> m (Maybe Chunk) 503 | playedLast (Channel c) = do 504 | p <- SDL.Raw.Mixer.getChunk $ clipChan c 505 | return $ if p == nullPtr then Nothing else Just (Chunk p) 506 | 507 | -- | Use this value when you wish to perform an operation on /all/ 'Channel's. 508 | -- 509 | -- For more information, see each of the functions accepting a 'Channel'. 510 | pattern AllChannels :: Channel 511 | pattern AllChannels = -1 512 | 513 | instance HasVolume Channel where 514 | setVolume v (Channel c) = 515 | void . SDL.Raw.Mixer.volume (clipChan c) $ volumeToCInt v 516 | getVolume (Channel c) = 517 | fmap fromIntegral $ SDL.Raw.Mixer.volume (clipChan c) (-1) 518 | 519 | -- | Play a 'Chunk' once, using the first available 'Channel'. 520 | play :: MonadIO m => Chunk -> m () 521 | play = void . playOn (-1) Once 522 | 523 | -- | Same as 'play', but keeps playing the 'Chunk' forever. 524 | playForever :: MonadIO m => Chunk -> m () 525 | playForever = void . playOn (-1) Forever 526 | 527 | -- | How many times should a certain 'Chunk' be played? 528 | newtype Times = Times CInt deriving (Eq, Ord, Enum, Integral, Real, Num) 529 | 530 | -- | A shorthand for playing once. 531 | pattern Once :: Times 532 | pattern Once = 1 533 | 534 | -- | A shorthand for looping a 'Chunk' forever. 535 | pattern Forever :: Times 536 | pattern Forever = 0 537 | 538 | -- | Same as 'play', but plays the 'Chunk' using a given 'Channel' a certain 539 | -- number of 'Times'. 540 | -- 541 | -- If 'AllChannels' is used, then plays the 'Chunk' using the first available 542 | -- 'Channel' instead. 543 | -- 544 | -- Returns the 'Channel' that was used. 545 | playOn :: MonadIO m => Channel -> Times -> Chunk -> m Channel 546 | playOn = playLimit NoLimit 547 | 548 | -- | A time in milliseconds. 549 | type Milliseconds = Int 550 | 551 | -- | An upper limit of time, in milliseconds. 552 | type Limit = Milliseconds 553 | 554 | -- | A lack of an upper limit. 555 | pattern NoLimit :: Limit 556 | pattern NoLimit = -1 557 | 558 | -- | Same as 'playOn', but imposes an upper limit in 'Milliseconds' to how long 559 | -- the 'Chunk' can play. 560 | -- 561 | -- The playing may still stop before the limit is reached. 562 | -- 563 | -- This is the most generic play function variant. 564 | playLimit :: MonadIO m => Limit -> Channel -> Times -> Chunk -> m Channel 565 | playLimit l (Channel c) (Times t) (Chunk p) = 566 | throwIfNeg "SDL.Mixer.playLimit" "Mix_PlayChannelTimed" $ 567 | fmap fromIntegral $ 568 | SDL.Raw.Mixer.playChannelTimed 569 | (clipChan c) p (max (-1) $ t - 1) (fromIntegral l) 570 | 571 | -- | Same as 'play', but fades in the 'Chunk' by making the 'Channel' 'Volume' 572 | -- start at 0 and rise to a full 128 over the course of a given number of 573 | -- 'Milliseconds'. 574 | -- 575 | -- The 'Chunk' may end playing before the fade-in is complete, if it doesn't 576 | -- last as long as the given fade-in time. 577 | fadeIn :: MonadIO m => Milliseconds -> Chunk -> m () 578 | fadeIn ms = void . fadeInOn AllChannels Once ms 579 | 580 | -- | Same as 'fadeIn', but allows you to specify the 'Channel' to play on and 581 | -- how many 'Times' to play it, similar to 'playOn'. 582 | -- 583 | -- If 'AllChannels' is used, will play the 'Chunk' on the first available 584 | -- 'Channel'. 585 | -- 586 | -- Returns the 'Channel' that was used. 587 | fadeInOn :: MonadIO m => Channel -> Times -> Milliseconds -> Chunk -> m Channel 588 | fadeInOn = fadeInLimit NoLimit 589 | 590 | -- | Same as 'fadeInOn', but imposes an upper 'Limit' to how long the 'Chunk' 591 | -- can play, similar to 'playLimit'. 592 | -- 593 | -- This is the most generic fade-in function variant. 594 | fadeInLimit 595 | :: MonadIO m => 596 | Limit -> Channel -> Times -> Milliseconds -> Chunk -> m Channel 597 | fadeInLimit l (Channel c) (Times t) ms (Chunk p) = 598 | throwIfNeg "SDL.Mixer.fadeInLimit" "Mix_FadeInChannelTimed" $ 599 | fromIntegral <$> 600 | SDL.Raw.Mixer.fadeInChannelTimed 601 | (clipChan c) p (max (-1) $ t - 1) (fromIntegral ms) (fromIntegral l) 602 | 603 | -- | Gradually fade out a given playing 'Channel' during the next 604 | -- 'Milliseconds', even if it is 'pause'd. 605 | -- 606 | -- If 'AllChannels' is used, fades out all the playing 'Channel's instead. 607 | fadeOut :: MonadIO m => Milliseconds -> Channel -> m () 608 | fadeOut ms (Channel c) = 609 | void $ SDL.Raw.Mixer.fadeOutChannel (clipChan c) $ fromIntegral ms 610 | 611 | -- | Same as 'fadeOut', but fades out an entire 'Group' instead. 612 | -- 613 | -- Using 'DefaultGroup' here is the same as calling 'fadeOut' with 614 | -- 'AllChannels'. 615 | fadeOutGroup :: MonadIO m => Milliseconds -> Group -> m () 616 | fadeOutGroup ms = \case 617 | DefaultGroup -> fadeOut ms AllChannels 618 | Group g -> void $ SDL.Raw.Mixer.fadeOutGroup g $ fromIntegral ms 619 | 620 | -- | Pauses the given 'Channel', if it is actively playing. 621 | -- 622 | -- If 'AllChannels' is used, will pause all actively playing 'Channel's 623 | -- instead. 624 | -- 625 | -- Note that 'pause'd 'Channel's may still be 'halt'ed. 626 | pause :: MonadIO m => Channel -> m () 627 | pause (Channel c) = SDL.Raw.Mixer.pause $ clipChan c 628 | 629 | -- | Resumes playing a 'Channel', or all 'Channel's if 'AllChannels' is used. 630 | resume :: MonadIO m => Channel -> m () 631 | resume (Channel c) = SDL.Raw.Mixer.resume $ clipChan c 632 | 633 | -- | Halts playback on a 'Channel', or all 'Channel's if 'AllChannels' is used. 634 | halt :: MonadIO m => Channel -> m () 635 | halt (Channel c) = void $ SDL.Raw.Mixer.haltChannel $ clipChan c 636 | 637 | -- | Same as 'halt', but only does so after a certain number of 'Milliseconds'. 638 | -- 639 | -- If 'AllChannels' is used, it will halt all the 'Channel's after the given 640 | -- time instead. 641 | haltAfter :: MonadIO m => Milliseconds -> Channel -> m () 642 | haltAfter ms (Channel c) = 643 | void . SDL.Raw.Mixer.expireChannel (clipChan c) $ fromIntegral ms 644 | 645 | -- | Same as 'halt', but halts an entire 'Group' instead. 646 | -- 647 | -- Note that using 'DefaultGroup' here is the same as calling 'halt' 648 | -- 'AllChannels'. 649 | haltGroup :: MonadIO m => Group -> m () 650 | haltGroup = \case 651 | DefaultGroup -> halt AllChannels 652 | Group g -> void $ SDL.Raw.Mixer.haltGroup $ max 0 g 653 | 654 | -- Quackery of the highest order! We keep track of a pointer we gave SDL_mixer, 655 | -- so we can free it at a later time. May the gods have mercy... 656 | {-# NOINLINE channelFinishedFunPtr #-} 657 | channelFinishedFunPtr :: IORef (FunPtr (SDL.Raw.Mixer.Channel -> IO ())) 658 | channelFinishedFunPtr = unsafePerformIO $ newIORef nullFunPtr 659 | 660 | -- | Sets a callback that gets invoked each time a 'Channel' finishes playing. 661 | -- 662 | -- A 'Channel' finishes playing both when playback ends normally and when it is 663 | -- 'halt'ed (also possibly via 'setChannels'). 664 | -- 665 | -- __Note: don't call other 'SDL.Mixer' functions within this callback.__ 666 | whenChannelFinished :: MonadIO m => (Channel -> IO ()) -> m () 667 | whenChannelFinished callback = liftIO $ do 668 | 669 | -- Sets the callback. 670 | let callback' = callback . Channel 671 | callbackRaw <- SDL.Raw.Mixer.wrapChannelCallback callback' 672 | SDL.Raw.Mixer.channelFinished callbackRaw 673 | 674 | -- Free the function we set last time, if any. 675 | lastFunPtr <- readIORef channelFinishedFunPtr 676 | when (lastFunPtr /= nullFunPtr) $ freeHaskellFunPtr lastFunPtr 677 | 678 | -- Then remember the new one. And weep in shame. 679 | writeIORef channelFinishedFunPtr callbackRaw 680 | 681 | -- | Returns whether the given 'Channel' is playing or not. 682 | -- 683 | -- If 'AllChannels' is used, this returns whether /any/ of the channels is 684 | -- currently playing. 685 | playing :: MonadIO m => Channel -> m Bool 686 | playing (Channel c) = (> 0) <$> SDL.Raw.Mixer.playing (clipChan c) 687 | 688 | -- | Returns how many 'Channel's are currently playing. 689 | playingCount :: MonadIO m => m Int 690 | playingCount = fromIntegral <$> SDL.Raw.Mixer.playing (-1) 691 | 692 | -- | Returns whether the given 'Channel' is paused or not. 693 | -- 694 | -- If 'AllChannels' is used, this returns whether /any/ of the channels is 695 | -- currently paused. 696 | paused :: MonadIO m => Channel -> m Bool 697 | paused (Channel c) = (> 0) <$> SDL.Raw.Mixer.paused (clipChan c) 698 | 699 | -- | Returns how many 'Channel's are currently paused. 700 | pausedCount :: MonadIO m => m Int 701 | pausedCount = fromIntegral <$> SDL.Raw.Mixer.paused (-1) 702 | 703 | -- | Describes whether a 'Channel' is fading in, out, or not at all. 704 | data Fading = NoFading | FadingIn | FadingOut 705 | deriving (Eq, Ord, Show, Read) 706 | 707 | wordToFading :: SDL.Raw.Mixer.Fading -> Fading 708 | wordToFading = \case 709 | SDL.Raw.Mixer.NO_FADING -> NoFading 710 | SDL.Raw.Mixer.FADING_IN -> FadingIn 711 | SDL.Raw.Mixer.FADING_OUT -> FadingOut 712 | _ -> error "SDL.Mixer.wordToFading: unknown Fading value." 713 | 714 | -- | Returns a `Channel`'s 'Fading' status. 715 | -- 716 | -- Note that using 'AllChannels' here is not valid, and will simply return the 717 | -- 'Fading' status of the first 'Channel' instead. 718 | fading :: MonadIO m => Channel -> m Fading 719 | fading (Channel c) = 720 | wordToFading <$> SDL.Raw.Mixer.fadingChannel (clipChan c) 721 | 722 | -- | A group of 'Channel's. 723 | -- 724 | -- Grouping 'Channel's together allows you to perform some operations on all of 725 | -- them at once. 726 | -- 727 | -- By default, all 'Channel's are members of the 'DefaultGroup'. 728 | newtype Group = Group CInt deriving (Eq, Ord, Enum, Integral, Real, Num) 729 | 730 | -- | The default 'Group' all 'Channel's are in the moment they are created. 731 | pattern DefaultGroup :: Group 732 | pattern DefaultGroup = -1 733 | 734 | -- | Assigns a given 'Channel' to a certain 'Group'. 735 | -- 736 | -- If 'DefaultGroup' is used, assigns the 'Channel' the the default starting 737 | -- 'Group' (essentially /ungrouping/ them). 738 | -- 739 | -- If 'AllChannels' is used, assigns all 'Channel's to the given 'Group'. 740 | -- 741 | -- Returns whether the 'Channel' was successfully grouped or not. Failure is 742 | -- poosible if the 'Channel' does not exist, for instance. 743 | group :: MonadIO m => Group -> Channel -> m Bool 744 | group wrapped@(Group g) channel = 745 | case channel of 746 | AllChannels -> do 747 | total <- getChannels 748 | if total > 0 then 749 | (> 0) <$> groupSpan wrapped 0 (Channel $ fromIntegral $ total - 1) 750 | else 751 | return True -- No channels available -- still a success probably. 752 | Channel c -> 753 | if c >= 0 then 754 | (== 1) <$> SDL.Raw.Mixer.groupChannel c g 755 | else 756 | return False -- Can't group the post-processing channel or below. 757 | 758 | -- | Same as 'groupChannel', but groups all 'Channel's between the first and 759 | -- last given, inclusive. 760 | -- 761 | -- If 'DefaultGroup' is used, assigns the entire 'Channel' span to the default 762 | -- starting 'Group' (essentially /ungrouping/ them). 763 | -- 764 | -- Using 'AllChannels' is invalid. 765 | -- 766 | -- Returns the number of 'Channel's successfully grouped. This number may be 767 | -- less than the number of 'Channel's given, for instance if some of them do 768 | -- not exist. 769 | groupSpan :: MonadIO m => Group -> Channel -> Channel -> m Int 770 | groupSpan wrap@(Group g) from@(Channel c1) to@(Channel c2) 771 | | c1 < 0 || c2 < 0 = return 0 772 | | c1 > c2 = groupSpan wrap to from 773 | | otherwise = fromIntegral <$> SDL.Raw.Mixer.groupChannels c1 c2 g 774 | 775 | -- | Returns the number of 'Channels' within a 'Group'. 776 | -- 777 | -- If 'DefaultGroup' is used, will return the number of all 'Channel's, since 778 | -- all of them are within the default 'Group'. 779 | groupCount :: MonadIO m => Group -> m Int 780 | groupCount (Group g) = fromIntegral <$> SDL.Raw.Mixer.groupCount g 781 | 782 | -- | Gets the first inactive (not playing) 'Channel' within a given 'Group', 783 | -- if any. 784 | -- 785 | -- Using 'DefaultGroup' will give you the first inactive 'Channel' out of all 786 | -- that exist. 787 | getAvailable :: MonadIO m => Group -> m (Maybe Channel) 788 | getAvailable (Group g) = do 789 | found <- SDL.Raw.Mixer.groupAvailable g 790 | return $ if found >= 0 then Just $ fromIntegral found else Nothing 791 | 792 | -- | Gets the oldest actively playing 'Channel' within a given 'Group'. 793 | -- 794 | -- Returns 'Nothing' when the 'Group' is empty or no 'Channel's within it are 795 | -- playing. 796 | getOldest :: MonadIO m => Group -> m (Maybe Channel) 797 | getOldest (Group g) = do 798 | found <- SDL.Raw.Mixer.groupOldest g 799 | return $ if found >= 0 then Just $ fromIntegral found else Nothing 800 | 801 | -- | Gets the newest actively playing 'Channel' within a given 'Group'. 802 | -- 803 | -- Returns 'Nothing' when the 'Group' is empty or no 'Channel's within it are 804 | -- playing. 805 | getNewest :: MonadIO m => Group -> m (Maybe Channel) 806 | getNewest (Group g) = do 807 | found <- SDL.Raw.Mixer.groupNewer g 808 | return $ if found >= 0 then Just $ fromIntegral found else Nothing 809 | 810 | -- | Returns the names of all music decoders currently available. 811 | -- 812 | -- These depend on the availability of shared libraries for each of the 813 | -- formats. The list may contain any of the following, and possibly others: 814 | -- @WAVE@, @MODPLUG@, @MIKMOD@, @TIMIDITY@, @FLUIDSYNTH@, @NATIVEMIDI@, @OGG@, 815 | -- @FLAC@, @MP3@. 816 | musicDecoders :: MonadIO m => m [String] 817 | musicDecoders = 818 | liftIO $ do 819 | num <- SDL.Raw.Mixer.getNumMusicDecoders 820 | forM [0 .. num - 1] $ \i -> 821 | SDL.Raw.Mixer.getMusicDecoder i >>= peekCString 822 | 823 | -- | A loaded music file. 824 | -- 825 | -- 'Music' is played on a separate channel different from the normal mixing 826 | -- 'Channel's. 827 | -- 828 | -- To manipulate 'Music' outside of post-processing callbacks, use the music 829 | -- variant functions listed below. 830 | newtype Music = Music (Ptr SDL.Raw.Mixer.Music) deriving (Eq, Show) 831 | 832 | instance Loadable Music where 833 | decode bytes = liftIO $ do 834 | unsafeUseAsCStringLen bytes $ \(cstr, len) -> do 835 | rw <- rwFromConstMem (castPtr cstr) (fromIntegral len) 836 | fmap Music . 837 | throwIfNull "SDL.Mixer.decode" "Mix_LoadMUS_RW" $ 838 | SDL.Raw.Mixer.loadMUS_RW rw 0 839 | 840 | free (Music p) = liftIO $ SDL.Raw.Mixer.freeMusic p 841 | 842 | -- | Plays a given 'Music' a certain number of 'Times'. 843 | -- 844 | -- The previously playing 'Music' will be halted, unless it is fading out in 845 | -- which case a blocking wait occurs until it fades out completely. 846 | playMusic :: MonadIO m => Times -> Music -> m () 847 | playMusic times (Music p) = 848 | throwIfNeg_ "SDL.Mixer.playMusic" "Mix_PlayMusic" $ 849 | SDL.Raw.Mixer.playMusic p $ 850 | case times of 851 | Forever -> (-1) 852 | Times t -> max 1 t -- Interpretation differs from normal play? :/ 853 | 854 | -- | Pauses 'Music' playback, if it is actively playing. 855 | -- 856 | -- You may still 'haltMusic' paused 'Music'. 857 | pauseMusic :: MonadIO m => m () 858 | pauseMusic = SDL.Raw.Mixer.pauseMusic 859 | 860 | -- | Halts 'Music' playback. 861 | haltMusic :: MonadIO m => m () 862 | haltMusic = void SDL.Raw.Mixer.haltMusic 863 | 864 | -- | Resumes 'Music' playback. 865 | -- 866 | -- This works on both paused and halted 'Music'. 867 | -- 868 | -- If 'Music' is currently actively playing, this has no effect. 869 | resumeMusic :: MonadIO m => m () 870 | resumeMusic = SDL.Raw.Mixer.resumeMusic 871 | 872 | -- | Returns whether a 'Music' is currently playing or not. 873 | -- 874 | -- Note that this returns 'True' even if the 'Music' is currently paused. 875 | playingMusic :: MonadIO m => m Bool 876 | playingMusic = (> 0) <$> SDL.Raw.Mixer.playingMusic 877 | 878 | -- | Returns whether a 'Music' is currently paused or not. 879 | -- 880 | -- Note that this returns 'False' if the 'Music' is currently halted. 881 | pausedMusic :: MonadIO m => m Bool 882 | pausedMusic = (> 0) <$> SDL.Raw.Mixer.pausedMusic 883 | 884 | -- | Rewinds the 'Music' to the beginning. 885 | -- 886 | -- When playing new 'Music', it starts at the beginning by default. 887 | -- 888 | -- This function only works with @MOD@, @OGG@, @MP3@ and @NATIVEMIDI@ streams. 889 | rewindMusic :: MonadIO m => m () 890 | rewindMusic = SDL.Raw.Mixer.rewindMusic 891 | 892 | -- | Plays a given 'Music' a number of 'Times', but fading it in during a 893 | -- certain number of 'Milliseconds'. 894 | -- 895 | -- The fading only occurs during the first time the 'Music' is played. 896 | fadeInMusic :: MonadIO m => Milliseconds -> Times -> Music -> m () 897 | fadeInMusic ms times (Music p) = 898 | throwIfNeg_ "SDL.Mixer.fadeInMusic" "Mix_FadeInMusic" $ 899 | SDL.Raw.Mixer.fadeInMusic p t' (fromIntegral ms) 900 | where 901 | t' = case times of 902 | Forever -> (-1) 903 | Times t -> max 1 t 904 | 905 | -- | Gradually fade out the 'Music' over a given number of 'Milliseconds'. 906 | -- 907 | -- The 'Music' is set to fade out only when it is playing and not fading 908 | -- already. 909 | -- 910 | -- Returns whether the 'Music' was successfully set to fade out. 911 | fadeOutMusic :: MonadIO m => Milliseconds -> m Bool 912 | fadeOutMusic = fmap (== 1) . SDL.Raw.Mixer.fadeOutMusic . fromIntegral 913 | 914 | -- | A position in milliseconds within a piece of 'Music'. 915 | type Position = Milliseconds 916 | 917 | -- | Set the 'Position' for currently playing 'Music'. 918 | -- 919 | -- Note: this only works for @OGG@ and @MP3@ 'Music'. 920 | setMusicPosition :: MonadIO m => Position -> m () 921 | setMusicPosition at = do 922 | rewindMusic -- Due to weird behaviour for MP3s... 923 | throwIfNeg_ "SDL.Mixer.setMusicPosition" "Mix_SetMusicPosition" $ 924 | SDL.Raw.Mixer.setMusicPosition $ realToFrac at / 1000.0 925 | 926 | -- | Similar to 'setMusicPosition', but works only with @MOD@ 'Music'. 927 | -- 928 | -- Pass in the pattern number. 929 | setMusicPositionMOD :: MonadIO m => Int -> m () 930 | setMusicPositionMOD n = do 931 | throwIfNeg_ "SDL.Mixer.setMusicPositionMOD" "Mix_SetMusicPosition" $ 932 | SDL.Raw.Mixer.setMusicPosition $ realToFrac n 933 | 934 | -- | Same as 'fadeInMusic', but with a custom starting `Music`'s 'Position'. 935 | -- 936 | -- Note that this only works on 'Music' that 'setMusicPosition' works on. 937 | fadeInMusicAt :: MonadIO m => Position -> Milliseconds -> Times -> Music -> m () 938 | fadeInMusicAt at ms times (Music p) = 939 | throwIfNeg_ "SDL.Mixer.fadeInMusicAt" "Mix_FadeInMusicPos" $ 940 | SDL.Raw.Mixer.fadeInMusicPos 941 | p t' (fromIntegral ms) (realToFrac at / 1000.0) 942 | where 943 | t' = case times of 944 | Forever -> (-1) 945 | Times t -> max 1 t 946 | 947 | -- | Same as 'fadeInMusicAt', but works with @MOD@ 'Music'. 948 | -- 949 | -- Instead of milliseconds, specify the position with a pattern number. 950 | fadeInMusicAtMOD :: MonadIO m => Int -> Milliseconds -> Times -> Music -> m () 951 | fadeInMusicAtMOD at ms times (Music p) = 952 | throwIfNeg_ "SDL.Mixer.fadeInMusicAtMOD" "Mix_FadeInMusicPos" $ 953 | SDL.Raw.Mixer.fadeInMusicPos 954 | p t' (fromIntegral ms) (realToFrac at) 955 | where 956 | t' = case times of 957 | Forever -> (-1) 958 | Times t -> max 1 t 959 | 960 | -- | Returns the `Music`'s 'Fading' status. 961 | fadingMusic :: MonadIO m => m Fading 962 | fadingMusic = wordToFading <$> SDL.Raw.Mixer.fadingMusic 963 | 964 | -- | Gets the current 'Volume' setting for 'Music'. 965 | getMusicVolume :: MonadIO m => m Volume 966 | getMusicVolume = fmap fromIntegral $ SDL.Raw.Mixer.volumeMusic (-1) 967 | 968 | -- | Sets the 'Volume' for 'Music'. 969 | -- 970 | -- Note that this won't work if any 'Music' is currently fading. 971 | setMusicVolume :: MonadIO m => Volume -> m () 972 | setMusicVolume v = void . SDL.Raw.Mixer.volumeMusic $ volumeToCInt v 973 | 974 | -- | A `Music`'s type. 975 | data MusicType 976 | = CMD 977 | | WAV 978 | | MOD 979 | | MID 980 | | OGG 981 | | MP3 982 | | FLAC 983 | deriving (Eq, Show, Read, Ord, Bounded) 984 | 985 | wordToMusicType :: SDL.Raw.Mixer.MusicType -> Maybe MusicType 986 | wordToMusicType = \case 987 | SDL.Raw.Mixer.MUS_NONE -> Nothing 988 | SDL.Raw.Mixer.MUS_CMD -> Just CMD 989 | SDL.Raw.Mixer.MUS_WAV -> Just WAV 990 | SDL.Raw.Mixer.MUS_MOD -> Just MOD 991 | SDL.Raw.Mixer.MUS_MID -> Just MID 992 | SDL.Raw.Mixer.MUS_OGG -> Just OGG 993 | SDL.Raw.Mixer.MUS_MP3 -> Just MP3 994 | SDL.Raw.Mixer.MUS_FLAC -> Just FLAC 995 | _ -> Nothing 996 | 997 | -- | Gets the 'MusicType' of a given 'Music'. 998 | musicType :: Music -> Maybe MusicType 999 | musicType (Music p) = 1000 | wordToMusicType $ unsafePerformIO (SDL.Raw.Mixer.getMusicType p) 1001 | 1002 | -- | Gets the 'MusicType' of currently playing 'Music', if any. 1003 | playingMusicType :: MonadIO m => m (Maybe MusicType) 1004 | playingMusicType = wordToMusicType <$> SDL.Raw.Mixer.getMusicType nullPtr 1005 | 1006 | -- More quackery, but this time for the music finished callback. 1007 | {-# NOINLINE musicFinishedFunPtr #-} 1008 | musicFinishedFunPtr :: IORef (FunPtr (IO ())) 1009 | musicFinishedFunPtr = unsafePerformIO $ newIORef nullFunPtr 1010 | 1011 | -- | Sets a callback that gets invoked each time a 'Music' finishes playing. 1012 | -- 1013 | -- __Note: don't call other 'SDL.Mixer' functions within this callback.__ 1014 | whenMusicFinished :: MonadIO m => IO () -> m () 1015 | whenMusicFinished callback = liftIO $ do 1016 | callbackRaw <- SDL.Raw.Mixer.wrapMusicCallback callback 1017 | SDL.Raw.Mixer.hookMusicFinished callbackRaw 1018 | lastFunPtr <- readIORef musicFinishedFunPtr 1019 | when (lastFunPtr /= nullFunPtr) $ freeHaskellFunPtr lastFunPtr 1020 | writeIORef musicFinishedFunPtr callbackRaw 1021 | 1022 | -- | A post-processing effect as a function operating on a mutable stream. 1023 | -- 1024 | -- __Note that, at the moment, this is a stream of bytes. Depending on the__ 1025 | -- __'Audio' 'Format' you're using, you're probably going to want to treat is__ 1026 | -- __as a stream of 16-bit values instead.__ 1027 | type Effect = Channel -> IOVector Word8 -> IO () -- TODO: Don't hardcode Word8. 1028 | 1029 | -- | A function called when a processor is finished being used. 1030 | -- 1031 | -- This allows you to clean up any state you might have had. 1032 | type EffectFinished = Channel -> IO () 1033 | 1034 | -- | A way to refer to the special 'Channel' used for post-processing effects. 1035 | -- 1036 | -- You can only use this value with 'effect' and the other in-built effect 1037 | -- functions such as 'effectPan' and 'effectDistance'. 1038 | pattern PostProcessing :: Channel 1039 | pattern PostProcessing = SDL.Raw.Mixer.CHANNEL_POST 1040 | 1041 | -- | Adds a post-processing 'Effect' to a certain 'Channel'. 1042 | -- 1043 | -- A `Channel`'s 'Effect's are called in the order they were added. 1044 | -- 1045 | -- Returns an action that, when executed, removes this 'Effect'. __Note: do__ 1046 | -- __execute this returned action more than once.__ 1047 | effect :: MonadIO m => Channel -> EffectFinished -> Effect -> m (m ()) 1048 | effect (Channel channel) fin ef = do 1049 | 1050 | ef' <- liftIO $ SDL.Raw.Mixer.wrapEffect $ \c p len _ -> do 1051 | fp <- castForeignPtr <$> newForeignPtr_ p 1052 | ef (Channel c) . unsafeFromForeignPtr0 fp $ fromIntegral len 1053 | 1054 | fin' <- liftIO $ SDL.Raw.Mixer.wrapEffectFinished $ \c _ -> 1055 | fin $ Channel c 1056 | 1057 | result <- SDL.Raw.Mixer.registerEffect channel ef' fin' nullPtr 1058 | 1059 | if result == 0 then do 1060 | liftIO $ do 1061 | freeHaskellFunPtr ef' >> freeHaskellFunPtr fin' 1062 | err <- getError 1063 | throwIO $ SDLCallFailed "SDL.Raw.Mixer.addEffect" "Mix_RegisterEffect" err 1064 | else 1065 | return . liftIO $ do -- The unregister action. 1066 | removed <- SDL.Raw.Mixer.unregisterEffect channel ef' 1067 | freeHaskellFunPtr ef' >> freeHaskellFunPtr fin' 1068 | when (removed == 0) $ do 1069 | err <- getError 1070 | throwIO $ 1071 | SDLCallFailed "SDL.Raw.Mixer.removeEffect" "Mix_UnregisterEffect" err 1072 | 1073 | -- | Applies an in-built effect implementing panning. 1074 | -- 1075 | -- Sets the left-channel and right-channel 'Volume' to the given values. 1076 | -- 1077 | -- This only works when `Audio`'s 'Output' is 'Stereo', which is the default. 1078 | -- 1079 | -- Returns an action that, when executed, removes this effect. That action 1080 | -- simply calls 'effectPan' with 'Volumes' 128 and 128. 1081 | effectPan :: MonadIO m => Channel -> Volume -> Volume -> m (m ()) 1082 | effectPan channel@(Channel c) lVol rVol = do 1083 | void . throwIf0 "SDL.Raw.Mixer.effectPan" "Mix_SetPanning" $ 1084 | SDL.Raw.Mixer.setPanning c (wordVol lVol) (wordVol rVol) 1085 | return . void $ effectPan channel 128 128 1086 | 1087 | wordVol :: Volume -> Word8 1088 | wordVol = fromIntegral . min 255 . (*2) . volumeToCInt 1089 | 1090 | -- | Applies a different volume based on the distance (as 'Word8') specified. 1091 | -- 1092 | -- The volume is loudest at distance 0, quietest at distance 255. 1093 | -- 1094 | -- Returns an action that, when executed, removes this effect. That action 1095 | -- simply calls 'effectDistance' with a distance of 0. 1096 | effectDistance :: MonadIO m => Channel -> Word8 -> m (m ()) 1097 | effectDistance channel@(Channel c) dist = do 1098 | void . throwIf0 "SDL.Raw.Mixer.effectDistance" "Mix_SetDistance" $ 1099 | SDL.Raw.Mixer.setDistance c dist 1100 | return . void $ effectDistance channel 0 1101 | 1102 | -- | Simulates a simple 3D audio effect. 1103 | -- 1104 | -- Accepts the angle in degrees (as 'Int16') in relation to the source of the 1105 | -- sound (0 is directly in front, 90 directly to the right, and so on) and a 1106 | -- distance (as 'Word8') from the source of the sound (where 255 is very far 1107 | -- away, and 0 extremely close). 1108 | -- 1109 | -- Returns an action that, when executed, removes this effect. That action 1110 | -- simply calls 'effectPosition' with both angle and distance set to 0. 1111 | effectPosition :: MonadIO m => Channel -> Int16 -> Word8 -> m (m ()) 1112 | effectPosition channel@(Channel c) angle dist = do 1113 | void . throwIf0 "SDL.Raw.Mixer.effectPosition" "Mix_SetPosition" $ 1114 | SDL.Raw.Mixer.setPosition c angle dist 1115 | return . void $ effectPosition channel 0 0 1116 | 1117 | -- | Swaps the left and right channel sound. 1118 | -- 1119 | -- If given 'True', will swap the sound channels. 1120 | -- 1121 | -- Returns an action that, when executed, removes this effect. That action 1122 | -- simply calls 'effectReverseStereo' with 'False'. 1123 | effectReverseStereo :: MonadIO m => Channel -> Bool -> m (m ()) 1124 | effectReverseStereo channel@(Channel c) rev = do 1125 | void . throwIf0 "SDL.Raw.Mixer.effectReverseStereo" "Mix_SetReverseStereo" $ 1126 | SDL.Raw.Mixer.setReverseStereo c (if rev then 1 else 0) 1127 | return . void $ effectReverseStereo channel False 1128 | 1129 | -- Music 1130 | -- TODO: hookMusic 1131 | -- TODO: setMusicCMD 1132 | -- TODO: getMusicHookData 1133 | 1134 | -- Effects 1135 | -- TODO: setPostMix 1136 | 1137 | -- SoundFonts 1138 | -- TODO: setSynchroValue 1139 | -- TODO: getSynchroValue 1140 | -- TODO: setSoundFonts 1141 | -- TODO: getSoundFonts 1142 | -- TODO: eachSoundFont 1143 | --------------------------------------------------------------------------------