{-# LANGUAGE OverloadedStrings, CPP #-} -- | High-ish level bindings to the HTML5 audio tag and JS API. module Haste.Audio ( module Events, Audio, AudioSettings (..), AudioType (..), AudioSource (..), AudioPreload (..), AudioState (..), Seek (..), defaultAudioSettings, mkSource, newAudio, setSource, getState, setMute, isMute, toggleMute, setLooping, isLooping, toggleLooping, getVolume, setVolume, modVolume, play, pause, stop, togglePlaying, seek, getDuration, getCurrentTime ) where import Haste.Audio.Events as Events import Haste.DOM.JSString import Haste.Foreign import Haste.Prim.JSType import Haste.Prim #if __GLASGOW_HASKELL__ < 710 import Control.Applicative #endif import Control.Monad import Control.Monad.IO.Class import Data.String -- | Represents an audio player. data Audio = Audio Elem instance IsElem Audio where elemOf (Audio e) = e fromElem e = do tn <- getProp e "tagName" return $ case tn of "AUDIO" -> Just $ Audio e _ -> Nothing data AudioState = Playing | Paused | Ended deriving (Show, Eq) data AudioType = MP3 | OGG | WAV deriving (Show, Eq) data AudioSource = AudioSource !AudioType !JSString deriving (Show, Eq) data AudioPreload = None | Metadata | Auto deriving Eq data Seek = Start | End | Seconds Double deriving Eq instance JSType AudioPreload where toJSString None = "none" toJSString Metadata = "metadata" toJSString Auto = "auto" fromJSString "none" = Just None fromJSString "metadata" = Just Metadata fromJSString "auto" = Just Auto fromJSString _ = Nothing data AudioSettings = AudioSettings { -- | Show controls? -- Default: False audioControls :: !Bool, -- | Immediately start playing? -- Default: False audioAutoplay :: !Bool, -- | Initially looping? -- Default: False audioLooping :: !Bool, -- | How much audio to preload. -- Default: Auto audioPreload :: !AudioPreload, -- | Initially muted? -- Default: False audioMuted :: !Bool, -- | Initial volume -- Default: 0 audioVolume :: !Double } defaultAudioSettings :: AudioSettings defaultAudioSettings = AudioSettings { audioControls = False, audioAutoplay = False, audioLooping = False, audioPreload = Auto, audioMuted = False, audioVolume = 0 } -- | Create an audio source with automatically detected media type, based on -- the given URL's file extension. -- Returns Nothing if the given URL has an unrecognized media type. mkSource :: JSString -> Maybe AudioSource mkSource url = case take 3 $ reverse $ fromJSStr url of "3pm" -> Just $ AudioSource MP3 url "ggo" -> Just $ AudioSource OGG url "vaw" -> Just $ AudioSource WAV url _ -> Nothing instance IsString AudioSource where fromString s = case mkSource $ Data.String.fromString s of Just src -> src _ -> error $ "Not a valid audio source: " ++ s mimeStr :: AudioType -> JSString mimeStr MP3 = "audio/mpeg" mimeStr OGG = "audio/ogg" mimeStr WAV = "audio/wav" -- | Create a new audio element. newAudio :: MonadIO m => AudioSettings -> [AudioSource] -> m Audio newAudio cfg sources = liftIO $ do srcs <- forM sources $ \(AudioSource t url) -> do newElem "source" `with` ["type" =: mimeStr t, "src" =: toJSString url] Audio <$> newElem "audio" `with` [ "controls" =: falseAsEmpty (audioControls cfg), "autoplay" =: falseAsEmpty (audioAutoplay cfg), "loop" =: falseAsEmpty (audioLooping cfg), "muted" =: falseAsEmpty (audioMuted cfg), "volume" =: toJSString (audioVolume cfg), "preload" =: toJSString (audioPreload cfg), children srcs ] -- | Returns "true" or "", depending on the given boolean. falseAsEmpty :: Bool -> JSString falseAsEmpty True = "true" falseAsEmpty _ = "" -- | (Un)mute the given audio object. setMute :: MonadIO m => Audio -> Bool -> m () setMute (Audio e) = setAttr e "muted" . falseAsEmpty -- | Is the given audio object muted? isMute :: MonadIO m => Audio -> m Bool isMute (Audio e) = liftIO $ maybe False id . fromJSString <$> getProp e "muted" -- | Mute/unmute. toggleMute :: MonadIO m => Audio -> m () toggleMute a = isMute a >>= setMute a . not -- | Set whether the given sound should loop upon completion or not. setLooping :: MonadIO m => Audio -> Bool -> m () setLooping (Audio e) = setAttr e "loop" . falseAsEmpty -- | Is the given audio object looping? isLooping :: MonadIO m => Audio -> m Bool isLooping (Audio e) = liftIO $ maybe False id . fromJSString <$> getProp e "looping" -- | Toggle looping on/off. toggleLooping :: MonadIO m => Audio -> m () toggleLooping a = isLooping a >>= setLooping a . not -- | Starts playing audio from the given element. play :: MonadIO m => Audio -> m () play a@(Audio e) = do st <- getState a when (st == Ended) $ seek a Start liftIO $ play' e where play' :: Elem -> IO () play' = ffi "(function(x){x.play();})" -- | Get the current state of the given audio object. getState :: MonadIO m => Audio -> m AudioState getState (Audio e) = liftIO $ do ended <- maybe False id . fromJSString <$> getProp e "ended" if ended then return Ended else maybe Playing paused . fromJSString <$> getProp e "paused" where paused True = Paused paused _ = Playing -- | Pause the given audio element. pause :: MonadIO m => Audio -> m () pause (Audio e) = liftIO $ pause' e pause' :: Elem -> IO () pause' = ffi "(function(x){x.pause();})" -- | If playing, stop. Otherwise, start playing. togglePlaying :: MonadIO m => Audio -> m () togglePlaying a = do st <- getState a case st of Playing -> pause a Ended -> seek a Start >> play a Paused -> play a -- | Stop playing a track, and seek back to its beginning. stop :: MonadIO m => Audio -> m () stop a = pause a >> seek a Start -- | Get the volume for the given audio element as a value between 0 and 1. getVolume :: MonadIO m => Audio -> m Double getVolume (Audio e) = liftIO $ maybe 0 id . fromJSString <$> getProp e "volume" -- | Set the volume for the given audio element. The value will be clamped to -- [0, 1]. setVolume :: MonadIO m => Audio -> Double -> m () setVolume (Audio e) = setProp e "volume" . toJSString . clamp -- | Modify the volume for the given audio element. The resulting volume will -- be clamped to [0, 1]. modVolume :: MonadIO m => Audio -> Double -> m () modVolume a diff = getVolume a >>= setVolume a . (+ diff) -- | Clamp a value to [0, 1]. clamp :: Double -> Double clamp = max 0 . min 1 -- | Seek to the specified time. seek :: MonadIO m => Audio -> Seek -> m () seek a@(Audio e) st = liftIO $ do case st of Start -> seek' e 0 End -> getDuration a >>= seek' e Seconds s -> seek' e s where seek' :: Elem -> Double -> IO () seek' = ffi "(function(e,t) {e.currentTime = t;})" -- | Get the duration of the loaded sound, in seconds. getDuration :: MonadIO m => Audio -> m Double getDuration (Audio e) = do dur <- getProp e "duration" case fromJSString dur of Just d -> return d _ -> return 0 -- | Get the current play time of the loaded sound, in seconds. getCurrentTime :: MonadIO m => Audio -> m Double getCurrentTime (Audio e) = do dur <- getProp e "currentTime" case fromJSString dur of Just d -> return d _ -> return 0 -- | Set the source of the given audio element. setSource :: MonadIO m => Audio -> AudioSource -> m () setSource (Audio e) (AudioSource _ url) = setProp e "src" (toJSString url)