{-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-| Module: Web.Audio License: BSD(see LICENSE file) Maintainer: Nicholas Shaheed Stability: Alpha @wahsp@ (Web Audio HaSkell Protocol) is a binding for Haskell to the ala @blank-canvas@. Audio sources, effects, etc. can be combined, manipulated, and otherwise controlled using haskell and are then rendered in the the browser (see the above link for browser compatibility). -} module Web.Audio ( -- * Set-Up webAudio , WAOptions(..) , send -- , AudioGraph(..) -- , AudNode(..) -- * Connecting nodes, params, and the audio context -- | The Web Audio API is comprised of nodes ('AudioNode's, 'AudioParam's, and the 'AudioContext') -- that are connected, input to output, to form a chain -- comprised of sources, effects, and a destination. -- -- This chain is typically organized as a /source -> effects -> destination/, where /destination/ -- is either the 'AudioContext' (if you actually want to produce sound in this chain), some -- 'AudioParam' (if you want to control a param with an audio signal, e.g. a low-frequency -- oscillator (lfo)), or some 'AudioNode'. -- -- To chain together 'AudioNode's and 'AudioParam's, use '.|.' and end the chain with '.||.' -- For example: -- -- @ -- osc1 <- 'createOscillator' 200 0 'Sine' -- gain1 <- 'createGain' 0.5 -- -- 'connect' $ osc1 .|. gain1 .||. 'eCtx' -- 'start' osc1 -- @ -- See the for a more -- detailed overview. -- , connect , (.|.) , (.||.) , connector , connectorLast , eNode , eParam , eCtx -- * Data Types , WebAudio(..) , AudioNode(..) , OscillatorNode(..) , OscillatorNodeType(..) , GainNode(..) , AudioParam(..) , ChannelCountMode(..) , AudioParamType(..) -- , showtJS -- * Procedures -- ** Instantiation functions , createOscillator , createGain -- ** Other Procedures , audioContext , maxValue , minValue , value , currentTime -- * Playback control -- Time in the api is the number of seconds since the session has been instantiated. -- ** 'OscillatorNode' specific playback controls , start , startWhen , stop , stopWhen -- ** Disconnecting functions , disconnect , disconnectOutput , disconnectOutputInput , disconnectDestNode , disconnectDestNodeSpec , disconnectDestParam , disconnectDestParamSpec -- * Change 'AudioParam' value -- | Different functions for altering the values of 'AudioParam's (immediately, ramping, etc.) , setValue , setValueAtTime , linearRampToValueAtTime , exponentialRampToValueAtTime , setTargetAtTime , cancelScheduledValues ) where -- wahsp - web audio haskell package -- to add a command: -- add to Command data type -- create a function -> WebAudio () -- add pattern to formatCommand -- add pattern to sendProcedure -- what I want to do: -- send over to js (map to web audio functionality) -- first step: connect to js import Control.Concurrent.STM import Control.Natural import qualified Control.Remote.Applicative as APP import Control.Remote.Monad -- import Control.Remote.Monad.Packet.Weak as WP -- import Control.Remote.Monad.Packet.Strong as SP import Control.Remote.Monad.Packet.Applicative as AP -- import Control.Remote.Monad.Packet.Alternative as Alt -- import qualified Data.Semigroup as SG import Data.Aeson (FromJSON(..),Value(..),withText) import Data.Aeson.Types (Parser,parse,Result(..)) import Data.Monoid ((<>)) import qualified Data.Text as T -- import Debug.Trace import Network.Wai.Middleware.Static import Paths_wahsp import System.IO.Unsafe (unsafePerformIO) import Web.Audio.JavaScript import Web.Audio.Packets import Web.Audio.WebAudio import qualified Web.Scotty.Comet as KC import Web.Scotty -- | 'webAudio' is the starting point for connecting and interacting with the API. A simple -- example of how to use this is: -- -- @ -- module Main where -- import Web.Audio -- -- main :: IO () -- main = do -- 'webAudio' 3000 $ \\doc -> do -- 'send' doc $ do -- osc1 <- 'createOscillator' 200 0 'Sine' -- create an 'OscillatorNode' -- gain1 <- 'createGain' 0.5 -- create a 'GainNode' -- -- 'connect' $ osc1 '.|.' gain1 '.||.' 'eCtx' -- connect these nodes together, and then connect them to the audio context -- -- 'start' osc1 -- make sounds! -- @ -- -- When running, go to in a browser to hear a 200Hz sine wave! -- -- More examples can be found . webAudio :: WAOptions -> (KC.Document -> IO ()) -> IO () webAudio opts actions = do kcomet <- KC.kCometPlugin -- get comet file path dataDir <- getDataDir -- get data (index.html, etc) file path let pol = only [ ("",dataDir ++ "/index.html") , ("js/kansas-comet.js",kcomet) , ("js/jquery.js",dataDir ++ "/js/jquery.js") , ("js/jquery-json.js",dataDir ++ "/js/jquery-json.js") ] <|> (hasPrefix "js/") >-> addBase "." let kcopts = KC.Options {KC.prefix = "/example", KC.verbose = if debug opts then 3 else 0} connectApp <- KC.connect kcopts $ \kc_doc -> do actions kc_doc scotty (port opts) $ do middleware $ staticPolicy pol connectApp return() -- | Connect the front of the chain of nodes together, end the chain with '.||.' (.|.) :: forall b a. AudioNode a => a -> AudioGraph AudNode b -> AudioGraph AudNode b (.|.) a b = connector a b infix 7 .|. -- | End the chain of 'AudioNode's. -- -- To end the chain at the audio context: -- -- > connect $ osc1 .|. gain1 .||. eCtx -- -- To end with an 'AudioParam' (that is located in the 'AudioNode' /gain1/): -- -- > connect $ osc1 .|. gain1 .||. eParam (gain gain1) -- -- To end with the 'AudioNode' /gain1/: -- -- > connect $ osc1 .|. gain1 .||. eNode gain1 (.||.) :: forall b a. AudioNode a => a -> AudioGraph AudNode b -> AudioGraph AudNode b (.||.) = connectorLast -- | function implementation of '.|.' connector :: forall b a. AudioNode a => a -> AudioGraph AudNode b -> AudioGraph AudNode b connector node = Node (AudNode node) -- | function implementation of '.||.' connectorLast :: forall b a. AudioNode a => a -> AudioGraph AudNode b -> AudioGraph AudNode b connectorLast a b = Node (AudNode a) b infix 8 .||. -- | Set the ending node to an 'AudioNode' eNode :: AudioNode a => a -> AudioGraph AudNode AudNode eNode a = EndNode (AudNode a) -- | Set the ending node to an 'AudioParam' eParam :: AudioParam -> AudioGraph AudNode AudioParam eParam = EndParam -- | Set the ending node to the 'AudioContext' eCtx :: AudioGraph AudNode AudioContext eCtx = EndCtx AudioContext -- TODO: decide if it's appropriate to have this exposed -- audioParamIdx :: AudioParam -> Int -- audioParamIdx (AudioParam _ i) = i -- | A function that returns an 'AudioContext' audioContext = AudioContext -- | creates an oscillator with a frequency (in hertz), a detuning value (in cents), and an 'OscillatorNodeType' (e.g. a sine wave, square wave, etc.) createOscillator :: Double -- ^ Frequency (in hertz) -> Double -- ^ Detuning (in cents) -> OscillatorNodeType -- ^ Waveform type -> WebAudio OscillatorNode createOscillator freq det osctype = WebAudio $ procedure (CreateOscillator freq det osctype) -- | Create a gain node with a gain value, typically between /0.0/ and /1.0/ createGain :: Double -> WebAudio GainNode createGain val = WebAudio $ procedure (CreateGain val) -- | Get the default value of an 'AudioParam' (this could vary from browser to browser) defaultValue :: AudioParam -> WebAudio Double defaultValue p = WebAudio $ procedure (DefaultValue p) -- | Get the maximum value of an 'AudioParam' maxValue :: AudioParam -> WebAudio Double maxValue p = WebAudio $ procedure (MaxValue p) -- | Get the minimum value of an 'AudioParam' minValue :: AudioParam -> WebAudio Double minValue p = WebAudio $ procedure (MinValue p) -- | Get the current value of an 'AudioParam' value :: AudioParam -> WebAudio Double value p = WebAudio $ procedure (Value p) -- | Get the current time in the sessions (in seconds). This represents the amount of time that has -- passed since the session was instantiated currentTime :: WebAudio Double currentTime = WebAudio $ procedure (CurrentTime) -- | Immediately start playback of an 'OscillatorNode' start :: OscillatorNode -> WebAudio () start = WebAudio . command . Start -- | Start playing an 'OscillatorNode' at \t\ seconds. If \t\ has already passed, it will immediately stop startWhen :: OscillatorNode -> Double -> WebAudio () startWhen o t = WebAudio . command $ StartWhen o t -- | Immediately stop playback of an 'OscillatorNode' stop :: OscillatorNode -> WebAudio () stop = WebAudio . command . Stop -- | Stop playing an 'OscillatorNode' at \t\ seconds. If \t\ has already passed, it will immediately stop stopWhen :: OscillatorNode -> Double -> WebAudio () stopWhen o t = WebAudio . command $ StopWhen o t -- disconnect functions -- | Disconnect all outgoing connections from AudioNode n disconnect :: AudioNode a => a -> WebAudio () disconnect src = WebAudio . command $ Disconnect src -- | Disconnect a specific output disconnectOutput :: AudioNode a => a -> Int -> WebAudio () disconnectOutput src idx = WebAudio . command $ DisconnectOutput src idx disconnectOutputInput :: AudioNode a => a -> a -> Int -> Int -> WebAudio () disconnectOutputInput src dest output input = WebAudio . command $ DisconnectOutputInput src dest output input disconnectDestNode :: AudioNode a => a -> a -> WebAudio () disconnectDestNode src dest = WebAudio . command $ DisconnectDestNode src dest disconnectDestNodeSpec :: AudioNode a => a -> a -> Int -> WebAudio () disconnectDestNodeSpec src dest idx = WebAudio . command $ DisconnectDestNodeSpec src dest idx disconnectDestParam :: AudioNode a => a -> AudioParam -> WebAudio () disconnectDestParam src dest = WebAudio . command $ DisconnectDestParam src dest disconnectDestParamSpec :: AudioNode a => a -> AudioParam -> Int -> WebAudio () disconnectDestParamSpec src dest idx = WebAudio . command $ DisconnectDestParamSpec src dest idx -- | Connects the 'AudioGraph' chain (made by connecting 'AudioNode's, 'AudioParam's, and 'AudioContext's -- with '.|.' and '.||.') connect :: AudioGraph AudNode b -> WebAudio () connect g = WebAudio . command $ Connect g -- Set Value functions setValue :: AudioParam -> Double -> WebAudio () setValue p val = WebAudio . command $ SetValue p val setValueAtTime :: AudioParam -> Double -> Double -> WebAudio () setValueAtTime p val startTime = WebAudio . command $ SetValueAtTime p val startTime linearRampToValueAtTime :: AudioParam -> Double -> Double -> WebAudio () linearRampToValueAtTime p val endTime = WebAudio . command $ LinearRampToValueAtTime p val endTime exponentialRampToValueAtTime :: AudioParam -> Double -> Double -> WebAudio () exponentialRampToValueAtTime p val endTime = WebAudio . command $ ExponentialRampToValueAtTime p val endTime setTargetAtTime :: AudioParam -> Double -> Double -> Double -> WebAudio () setTargetAtTime p target startTime timeConstant = WebAudio . command $ SetTargetAtTime p target startTime timeConstant cancelScheduledValues :: AudioParam -> Double -> WebAudio () cancelScheduledValues p startTime = WebAudio . command $ CancelScheduledValues p startTime send :: KC.Document -> WebAudio a -> IO a send = sendApp sendApp :: KC.Document -> WebAudio a -> IO a sendApp d (WebAudio m) = (run $ runMonad $ nat (runAP d)) m -- runAP :: KC.Document -> WebAudio a -> IO a runAP :: KC.Document -> ApplicativePacket Command Procedure a -> IO a runAP d pkt = case AP.superCommand pkt of Just a -> do -- is only commands -- putStrLn "" cmds <- handlePacket d pkt "" KC.send d cmds return a Nothing -> case pkt of AP.Command cmd -> do putStrLn "" cmds <- formatCommand cmd "" KC.send d cmds AP.Procedure p -> sendProcedure d p "" AP.Zip f g h -> f <$> runAP d g <*> runAP d h AP.Pure p -> pure p where handlePacket :: KC.Document -> ApplicativePacket Command Procedure a -> T.Text -> IO T.Text handlePacket doc pkt cmds = case pkt of AP.Command cmd -> formatCommand cmd cmds AP.Procedure p -> return cmds AP.Pure a -> return cmds AP.Zip f g h -> do gcmds <- handlePacket doc g cmds hcmds <- handlePacket doc h gcmds return hcmds -- refactor to be easier to add stuff sendProcedure :: KC.Document -> Procedure a -> T.Text -> IO a sendProcedure d p@(CreateOscillator freq det nodetype) _ = formatProcedure d p $ "CreateOscillator(" <> tshow freq <> "," <> tshow det <> ",'" <> tshow nodetype <> "')" sendProcedure d p@(CreateGain val) _ = formatProcedure d p $ "CreateGain(" <> tshow val <> ")" sendProcedure d p@(DefaultValue audioParam) _ = formatProcedure d p $ "DefaultValue(" <> showtJS audioParam <> ")" sendProcedure d p@(MaxValue audioParam) _ = formatProcedure d p $ "MaxValue(" <> showtJS audioParam <> ")" sendProcedure d p@(MinValue audioParam) _ = formatProcedure d p $ "MinValue(" <> showtJS audioParam <> ")" sendProcedure d p@(Value audioParam) _ = formatProcedure d p $ "Value(" <> showtJS audioParam <> ")" sendProcedure d p@(CurrentTime) _ = formatProcedure d p "GetCurrentTime()" -- take text for function calls to be sent and add generate unique for port formatProcedure :: KC.Document -> Procedure a -> T.Text -> IO a formatProcedure d p call = do uq <- atomically getUniq KC.send d $ call <> "(" <> tshow uq <> ");" v <- KC.getReply d uq case parse (parseProcedure p) v of Error msg -> fail msg Success a -> return a parseProcedure :: Procedure a -> Value -> Parser a parseProcedure (CreateOscillator {}) o = uncurry9 OscillatorNode <$> parseJSON o parseProcedure (CreateGain {}) o = uncurry7 GainNode <$> parseJSON o parseProcedure (DefaultValue {}) o = parseJSON o parseProcedure (MaxValue {}) o = parseJSON o parseProcedure (MinValue {}) o = parseJSON o parseProcedure (Value {}) o = parseJSON o parseProcedure (CurrentTime {}) o = parseJSON o formatCommand :: Command -> T.Text -> IO T.Text formatCommand (Start osc) cmds = return $ cmds <> showtJS osc <> ".start();" formatCommand (StartWhen osc t) cmds = return $ cmds <> showtJS osc <> ".start(" <> tshow t <> ");" formatCommand (Stop osc) cmds = return $ cmds <> showtJS osc <> ".stop();" formatCommand (StopWhen osc t) cmds = return $ cmds <> showtJS osc <> ".stop(" <> tshow t <> ");" formatCommand (Connect g) cmds = return $ cmds <> audioGraphConnect g <> ";" formatCommand (Disconnect src) cmds = return $ cmds <> showtJS src <> ".disconnect();" formatCommand (DisconnectOutput src idx) cmds = return $ cmds <> showtJS src <> ".disconnect(" <> showtJS idx <> ");" formatCommand (DisconnectOutputInput src dest output input) cmds = return $ cmds <> showtJS src <> ".disconnect(" <> showtJS dest <> "," <> showtJS output <> "," <> showtJS input <> ");" formatCommand (DisconnectDestNode src dest) cmds = return $ cmds <> showtJS src <> ".disconnect(" <> showtJS dest <> ");" formatCommand (DisconnectDestNodeSpec src dest idx) cmds = return $ cmds <> showtJS src <> ".disconnect(" <> showtJS dest <> "," <> showtJS idx <> ");" formatCommand (DisconnectDestParam src dest) cmds = return $ cmds <> showtJS src <> ".disconnect(" <> showtJS dest <> ");" formatCommand (DisconnectDestParamSpec src dest idx) cmds = return $ cmds <> showtJS src <> ".disconnect(" <> showtJS dest <> "," <> showtJS idx <> ");" formatCommand (SetValue p val) cmds = return $ cmds <> showtJS p <> ".value = " <> showtJS val <> ";" formatCommand (SetValueAtTime p val startTime) cmds = return $ cmds <> showtJS p <> ".setValueAtTime(" <> showtJS val <> "," <> showtJS startTime <> ");" formatCommand (LinearRampToValueAtTime p val endTime) cmds = return $ cmds <> showtJS p <> ".linearRampToValueAtTime(" <> showtJS val <> "," <> showtJS endTime <> ");" formatCommand (ExponentialRampToValueAtTime p val endTime) cmds = return $ cmds <> showtJS p <> ".exponentialRampToValueAtTime(" <> showtJS val <> "," <> showtJS endTime <> ");" formatCommand (SetTargetAtTime p target startTime timeConstant) cmds = return $ cmds <> showtJS p <> ".setTargetAtTime(" <> showtJS target <> "," <> showtJS startTime <> "," <> showtJS timeConstant <> ");" formatCommand (CancelScheduledValues p startTime ) cmds = return $ cmds <> showtJS p <> ".cancelScheduledValues(" <> showtJS startTime <> ");" {-# NOINLINE uniqVar #-} uniqVar :: TVar Int uniqVar = unsafePerformIO $ newTVarIO 0 getUniq :: STM Int getUniq = do u <- readTVar uniqVar writeTVar uniqVar (u + 1) return u