diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8cb0ed4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/.retool_types/**": true, + "**/*tsconfig.json": true, + ".cache": true, + "retool.config.json": true + }, + "nixEnvSelector.nixFile": "${workspaceFolder}/shell.nix" +} \ No newline at end of file diff --git a/app/Main.hs b/app/Main.hs index 6b751fb..306a6ec 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -11,62 +11,62 @@ import qualified Data.ByteString.Lazy.Char8 as LC8 import Data.IORef import Data.Maybe import Ensemble.Config -import Ensemble.Handler import Ensemble.Env +import Ensemble.Handler import Network.HTTP.Types (status400) import Network.Wai (responseLBS) -import qualified Network.Wai.Handler.WebSockets as WaiWs import qualified Network.Wai.Handler.Warp as Warp +import qualified Network.Wai.Handler.WebSockets as WaiWs import qualified Network.WebSockets as WS import Options.Generic import System.IO main :: IO () main = do - hSetBuffering stdout NoBuffering - config <- getRecord "Ensemble Audio Engine" - env <- createEnv config - runWebSocketInterface env (fromMaybe 3000 $ port config) - - where + hSetBuffering stdout NoBuffering + config <- getRecord "Ensemble Audio Engine" + env <- createEnv config + runWebSocketInterface env (fromMaybe 3000 $ port config) + where runWebSocketInterface env port' = do - let warpSettings = Warp.setPort port' Warp.defaultSettings - let websocketApp pendingConnection = do - connection <- WS.acceptRequest pendingConnection - sendThread <- forkIO $ forever $ do - outgoingMessage <- readChan $ env_messageChannel env - WS.sendTextData connection (A.encode outgoingMessage) - isOpen <- newIORef True - whileM $ do - incomingMessage <- fmap Just (WS.receiveData connection) `catch` \case - WS.CloseRequest _ _ -> do - killThread sendThread - writeIORef isOpen False - pure Nothing - WS.ConnectionClosed -> do - killThread sendThread - writeIORef isOpen False - pure Nothing - WS.ParseException message -> do - putStrLn $ "PARSE EXCEPTION: " <> message - pure Nothing - WS.UnicodeException message -> do - putStrLn $ "UNICODE EXCEPTION: " <> message - pure Nothing - whenJust incomingMessage $ handleIncomingMessage env - readIORef isOpen - let backupApp _ respond = respond $ responseLBS status400 [] "Not a WebSocket request" - Warp.runSettings warpSettings $ WaiWs.websocketsOr WS.defaultConnectionOptions websocketApp backupApp + let warpSettings = Warp.setPort port' Warp.defaultSettings + let websocketApp pendingConnection = do + connection <- WS.acceptRequest pendingConnection + sendThread <- forkIO $ forever $ do + outgoingMessage <- readChan $ env_messageChannel env + WS.sendTextData connection (A.encode outgoingMessage) + isOpen <- newIORef True + whileM $ do + incomingMessage <- + fmap Just (WS.receiveData connection) `catch` \case + WS.CloseRequest _ _ -> do + killThread sendThread + writeIORef isOpen False + pure Nothing + WS.ConnectionClosed -> do + killThread sendThread + writeIORef isOpen False + pure Nothing + WS.ParseException message -> do + putStrLn $ "PARSE EXCEPTION: " <> message + pure Nothing + WS.UnicodeException message -> do + putStrLn $ "UNICODE EXCEPTION: " <> message + pure Nothing + whenJust incomingMessage $ handleIncomingMessage env + readIORef isOpen + let backupApp _ respond = respond $ responseLBS status400 [] "Not a WebSocket request" + Warp.runSettings warpSettings $ WaiWs.websocketsOr WS.defaultConnectionOptions websocketApp backupApp - handleIncomingMessage env message = - case A.eitherDecodeStrict message of - Right incomingMessage -> do - outgoingMessage <- receiveMessage env incomingMessage - writeChan (env_messageChannel env) outgoingMessage - Left parseError -> - hPutStrLn stderr $ "Parse error: " <> parseError + handleIncomingMessage env message = + case A.eitherDecodeStrict message of + Right incomingMessage -> do + outgoingMessage <- receiveMessage env incomingMessage + writeChan (env_messageChannel env) outgoingMessage + Left parseError -> + hPutStrLn stderr $ "Parse error: " <> parseError - handleOutgoingMessages env = - void $ forkIO $ forever $ do - outgoingMessage <- readChan $ env_messageChannel env - putStrLn $ LC8.unpack (A.encode outgoingMessage) + handleOutgoingMessages env = + void $ forkIO $ forever $ do + outgoingMessage <- readChan $ env_messageChannel env + putStrLn $ LC8.unpack (A.encode outgoingMessage) diff --git a/shell.nix b/shell.nix index f304874..a6f45ff 100644 --- a/shell.nix +++ b/shell.nix @@ -15,6 +15,7 @@ in pkgs.haskellPackages.ghc pkgs.haskellPackages.hoogle pkgs.haskellPackages.hlint + pkgs.haskellPackages.ormolu pkgs.cabal2nix ]; shellHook = '' diff --git a/src/Ensemble.hs b/src/Ensemble.hs index bdd18a4..f852675 100644 --- a/src/Ensemble.hs +++ b/src/Ensemble.hs @@ -1,7 +1,8 @@ -module Ensemble - ( module Ensemble.API - , module Ensemble.Env - ) where +module Ensemble + ( module Ensemble.API, + module Ensemble.Env, + ) +where import Ensemble.API import Ensemble.Env diff --git a/src/Ensemble/API.hs b/src/Ensemble/API.hs index 9abf852..41ecee6 100644 --- a/src/Ensemble/API.hs +++ b/src/Ensemble/API.hs @@ -1,28 +1,29 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} + module Ensemble.API where import qualified Clap import qualified Clap.Host as Clap -import Clap.Library (PluginInfo (..)) import qualified Clap.Interface.Extension.Gui as Gui -import Clap.Interface.Extension.Params (ParameterInfo(..)) +import Clap.Interface.Extension.Params (ParameterInfo (..)) import qualified Clap.Interface.Extension.Params as Params import Clap.Interface.Id (ClapId (..)) +import Clap.Library (PluginInfo (..)) import qualified Clap.Library as Clap import Control.Concurrent import Control.Monad.Extra (whenJust) import Control.Monad.Reader import Data.IORef import Data.Maybe -import Data.Text (Text, unpack, pack) -import Ensemble.Engine (AudioDevice, MidiDevice, AudioOutput) +import Data.Text (Text, pack, unpack) +import Ensemble.Engine (AudioDevice, AudioOutput, MidiDevice) import qualified Ensemble.Engine as Engine +import Ensemble.Env +import Ensemble.Event (SequencerEvent (..)) import Ensemble.Node -import Ensemble.Event (SequencerEvent(..)) import Ensemble.Schema.TH import qualified Ensemble.Sequencer as Sequencer -import Ensemble.Env import Ensemble.Tick import Ensemble.Window import Foreign.Ptr @@ -38,182 +39,185 @@ getMidiDevices = liftIO Engine.getMidiDevices startEngine :: Ensemble Ok startEngine = do - engine <- asks env_engine - liftIO $ Engine.start engine - pure Ok + engine <- asks env_engine + liftIO $ Engine.start engine + pure Ok stopEngine :: Ensemble Ok stopEngine = do - engine <- asks env_engine - liftIO $ Engine.stop engine - pure Ok + engine <- asks env_engine + liftIO $ Engine.stop engine + pure Ok createMidiDeviceNode :: Argument "deviceId" Int -> Ensemble NodeId createMidiDeviceNode (Argument deviceId) = do - engine <- asks env_engine - liftIO $ Engine.createMidiDeviceNode engine deviceId + engine <- asks env_engine + liftIO $ Engine.createMidiDeviceNode engine deviceId deleteNode :: Argument "nodeId" NodeId -> Ensemble Ok deleteNode (Argument nodeId) = do - engine <- asks env_engine - liftIO $ Engine.deleteNode engine nodeId - pure Ok + engine <- asks env_engine + liftIO $ Engine.deleteNode engine nodeId + pure Ok -- CLAP getPluginLocations :: Ensemble [Text] -getPluginLocations = - liftIO $ fmap pack <$> Clap.pluginLibraryPaths +getPluginLocations = + liftIO $ fmap pack <$> Clap.pluginLibraryPaths scanForPlugins :: Argument "filePaths" [Text] -> Ensemble [PluginInfo] scanForPlugins (Argument filePaths) = - liftIO $ Clap.scanForPluginsIn $ unpack <$> filePaths - + liftIO $ Clap.scanForPluginsIn $ unpack <$> filePaths + createPluginNode :: Argument "filePath" Text -> Argument "pluginIndex" Int -> Ensemble NodeId createPluginNode (Argument filePath) (Argument pluginIndex) = do - engine <- asks env_engine - liftIO $ Engine.createPluginNode engine $ Clap.PluginLocation (unpack filePath) pluginIndex + engine <- asks env_engine + liftIO $ Engine.createPluginNode engine $ Clap.PluginLocation (unpack filePath) pluginIndex data Size = Size - { size_width :: Int - , size_height :: Int - } deriving (Show) + { size_width :: Int, + size_height :: Int + } + deriving (Show) data WindowInfo = WindowInfo - { windowInfo_parentHandle :: Int - , windowInfo_handle :: Int - , windowInfo_width :: Int - , windowInfo_height :: Int - } deriving (Show) + { windowInfo_parentHandle :: Int, + windowInfo_handle :: Int, + windowInfo_width :: Int, + windowInfo_height :: Int + } + deriving (Show) openPluginGUI :: Argument "nodeId" NodeId -> Argument "name" Text -> Argument "parentWindow" (Maybe Int) -> Argument "scale" Double -> Argument "preferredSize" (Maybe Size) -> Ensemble Size openPluginGUI (Argument nodeId) (Argument name) (Argument maybeParentWindow) (Argument scale) (Argument maybePreferredSize) = do - engine <- asks env_engine - maybeNode <- liftIO $ Engine.lookupNode engine nodeId - case maybeNode of - Just (Node_Plugin pluginNode) -> do - let plugin = pluginNode_plugin pluginNode - let pluginHandle = Clap.plugin_handle plugin - case Clap.pluginExtensions_gui (Clap.plugin_extensions plugin) of - Just pluginGuiHandle -> do - createResult <- liftIO $ Gui.createEmbedded pluginGuiHandle pluginHandle Gui.Win32 - unless createResult $ Engine.throwApiError "Error creating plugin GUI" - _setScaleResult <- liftIO $ Gui.setScale pluginGuiHandle pluginHandle scale - canResize <- liftIO $ Gui.canResize pluginGuiHandle pluginHandle - actualSize <- - case (canResize, maybePreferredSize) of - (True, Just preferredSize) -> do - setSizeResult <- liftIO $ Gui.setClosestUsableSize pluginGuiHandle pluginHandle (size_width preferredSize) (size_height preferredSize) - case setSizeResult of - Just (actualWidth, actualHeight) -> pure $ Size actualWidth actualHeight - Nothing -> Engine.throwApiError "Error setting size of plugin GUI" - _ -> do - maybeSize <- liftIO $ Gui.getSize pluginGuiHandle pluginHandle - case maybeSize of - Just (width, height) -> pure $ Size width height - Nothing -> Engine.throwApiError "Error setting size of plugin GUI" - let maybeParentWindowPtr = intPtrToPtr . IntPtr <$> maybeParentWindow - guiParentWindow <- liftIO $ createParentWindow maybeParentWindowPtr (unpack name) (size_width actualSize) (size_height actualSize) - liftIO $ showWindow guiParentWindow - guiWindowHandle <- liftIO $ Gui.createWindow Gui.Win32 guiParentWindow - setParentResult <- liftIO $ Gui.setParent pluginGuiHandle pluginHandle guiWindowHandle - unless setParentResult $ Engine.throwApiError "Error setting parent window of plugin GUI" - showResult <- liftIO $ Gui.show pluginGuiHandle pluginHandle - unless showResult $ Engine.throwApiError "Error showing plugin GUI" - pluginGuiThreadIdIORef <- asks env_pluginGuiThreadId - pluginGuiThreadId <- liftIO $ readIORef pluginGuiThreadIdIORef - unless (isJust pluginGuiThreadId) $ do - newPluginGuiThreadId <- liftIO $ forkIO messagePump - liftIO $ writeIORef pluginGuiThreadIdIORef $ Just newPluginGuiThreadId - pure actualSize - Nothing -> Engine.throwApiError "Plugin does not support GUI extension" - Just _ -> Engine.throwApiError "Invalid node type" - Nothing -> Engine.throwApiError $ "Node " <> show (nodeId_id nodeId) <> " not found" + engine <- asks env_engine + maybeNode <- liftIO $ Engine.lookupNode engine nodeId + case maybeNode of + Just (Node_Plugin pluginNode) -> do + let plugin = pluginNode_plugin pluginNode + let pluginHandle = Clap.plugin_handle plugin + case Clap.pluginExtensions_gui (Clap.plugin_extensions plugin) of + Just pluginGuiHandle -> do + createResult <- liftIO $ Gui.createEmbedded pluginGuiHandle pluginHandle Gui.Win32 + unless createResult $ Engine.throwApiError "Error creating plugin GUI" + _setScaleResult <- liftIO $ Gui.setScale pluginGuiHandle pluginHandle scale + canResize <- liftIO $ Gui.canResize pluginGuiHandle pluginHandle + actualSize <- + case (canResize, maybePreferredSize) of + (True, Just preferredSize) -> do + setSizeResult <- liftIO $ Gui.setClosestUsableSize pluginGuiHandle pluginHandle (size_width preferredSize) (size_height preferredSize) + case setSizeResult of + Just (actualWidth, actualHeight) -> pure $ Size actualWidth actualHeight + Nothing -> Engine.throwApiError "Error setting size of plugin GUI" + _ -> do + maybeSize <- liftIO $ Gui.getSize pluginGuiHandle pluginHandle + case maybeSize of + Just (width, height) -> pure $ Size width height + Nothing -> Engine.throwApiError "Error setting size of plugin GUI" + let maybeParentWindowPtr = intPtrToPtr . IntPtr <$> maybeParentWindow + guiParentWindow <- liftIO $ createParentWindow maybeParentWindowPtr (unpack name) (size_width actualSize) (size_height actualSize) + liftIO $ showWindow guiParentWindow + guiWindowHandle <- liftIO $ Gui.createWindow Gui.Win32 guiParentWindow + setParentResult <- liftIO $ Gui.setParent pluginGuiHandle pluginHandle guiWindowHandle + unless setParentResult $ Engine.throwApiError "Error setting parent window of plugin GUI" + showResult <- liftIO $ Gui.show pluginGuiHandle pluginHandle + unless showResult $ Engine.throwApiError "Error showing plugin GUI" + pluginGuiThreadIdIORef <- asks env_pluginGuiThreadId + pluginGuiThreadId <- liftIO $ readIORef pluginGuiThreadIdIORef + unless (isJust pluginGuiThreadId) $ do + newPluginGuiThreadId <- liftIO $ forkIO messagePump + liftIO $ writeIORef pluginGuiThreadIdIORef $ Just newPluginGuiThreadId + pure actualSize + Nothing -> Engine.throwApiError "Plugin does not support GUI extension" + Just _ -> Engine.throwApiError "Invalid node type" + Nothing -> Engine.throwApiError $ "Node " <> show (nodeId_id nodeId) <> " not found" getPluginParameters :: Argument "nodeId" NodeId -> Ensemble [ParameterInfo] -getPluginParameters (Argument nodeId) = do - engine <- asks env_engine - maybeNode <- liftIO $ Engine.lookupNode engine nodeId - case maybeNode of - Just (Node_Plugin pluginNode) -> do - let plugin = pluginNode_plugin pluginNode - let pluginHandle = Clap.plugin_handle plugin - case Clap.pluginExtensions_params (Clap.plugin_extensions plugin) of - Just pluginParamsHandle -> liftIO $ do - count <- Params.count pluginParamsHandle pluginHandle - parameterInfos <- traverse (Params.getInfo pluginParamsHandle pluginHandle) [0 .. count - 1] - pure $ catMaybes parameterInfos - Nothing -> Engine.throwApiError "Plugin does not support params extension" - Just _ -> Engine.throwApiError "Invalid node type" - Nothing -> Engine.throwApiError $ "Node " <> show (nodeId_id nodeId) <> " not found" +getPluginParameters (Argument nodeId) = do + engine <- asks env_engine + maybeNode <- liftIO $ Engine.lookupNode engine nodeId + case maybeNode of + Just (Node_Plugin pluginNode) -> do + let plugin = pluginNode_plugin pluginNode + let pluginHandle = Clap.plugin_handle plugin + case Clap.pluginExtensions_params (Clap.plugin_extensions plugin) of + Just pluginParamsHandle -> liftIO $ do + count <- Params.count pluginParamsHandle pluginHandle + parameterInfos <- traverse (Params.getInfo pluginParamsHandle pluginHandle) [0 .. count - 1] + pure $ catMaybes parameterInfos + Nothing -> Engine.throwApiError "Plugin does not support params extension" + Just _ -> Engine.throwApiError "Invalid node type" + Nothing -> Engine.throwApiError $ "Node " <> show (nodeId_id nodeId) <> " not found" getPluginParameterValue :: Argument "nodeId" NodeId -> Argument "parameterId" Int -> Ensemble (Maybe Double) -getPluginParameterValue (Argument nodeId) (Argument parameterId) = do - engine <- asks env_engine - maybeNode <- liftIO $ Engine.lookupNode engine nodeId - case maybeNode of - Just (Node_Plugin pluginNode) -> do - let plugin = pluginNode_plugin pluginNode - let pluginHandle = Clap.plugin_handle plugin - case Clap.pluginExtensions_params (Clap.plugin_extensions plugin) of - Just pluginParamsHandle -> liftIO $ Params.getValue pluginParamsHandle pluginHandle (ClapId parameterId) - Nothing -> Engine.throwApiError "Plugin does not support params extension" - Just _ -> Engine.throwApiError "Invalid node type" - Nothing -> Engine.throwApiError $ "Node " <> show (nodeId_id nodeId) <> " not found" +getPluginParameterValue (Argument nodeId) (Argument parameterId) = do + engine <- asks env_engine + maybeNode <- liftIO $ Engine.lookupNode engine nodeId + case maybeNode of + Just (Node_Plugin pluginNode) -> do + let plugin = pluginNode_plugin pluginNode + let pluginHandle = Clap.plugin_handle plugin + case Clap.pluginExtensions_params (Clap.plugin_extensions plugin) of + Just pluginParamsHandle -> liftIO $ Params.getValue pluginParamsHandle pluginHandle (ClapId parameterId) + Nothing -> Engine.throwApiError "Plugin does not support params extension" + Just _ -> Engine.throwApiError "Invalid node type" + Nothing -> Engine.throwApiError $ "Node " <> show (nodeId_id nodeId) <> " not found" -- Sequencer sendEvent :: Argument "sequencerEvent" SequencerEvent -> Ensemble Ok sendEvent (Argument event) = do - engine <- asks env_engine - liftIO $ Engine.sendEventNow engine event - pure Ok + engine <- asks env_engine + liftIO $ Engine.sendEventNow engine event + pure Ok scheduleEvent :: Argument "tick" Tick -> Argument "sequencerEvent" SequencerEvent -> Ensemble Ok scheduleEvent (Argument tick) (Argument event) = do - sequencer <- asks env_sequencer - liftIO $ Sequencer.sendAt sequencer tick event - pure Ok + sequencer <- asks env_sequencer + liftIO $ Sequencer.sendAt sequencer tick event + pure Ok playSequence :: Argument "startTick" Tick -> Argument "endTick" (Maybe Tick) -> Argument "loop" Bool -> Ensemble Ok playSequence (Argument startTick) (Argument maybeEndTick) (Argument loop) = do - sequencer <- asks env_sequencer - engine <- asks env_engine - void $ liftIO $ do - maybeThreadId <- readIORef (Engine.engine_playbackThread engine) - unless (isJust maybeThreadId) $ do - threadId <- forkFinally - (void $ Sequencer.playSequenceOffline sequencer engine startTick maybeEndTick loop) - (\_ -> writeIORef (Engine.engine_playbackThread engine) Nothing) - writeIORef (Engine.engine_playbackThread engine) (Just threadId) - pure Ok + sequencer <- asks env_sequencer + engine <- asks env_engine + void $ liftIO $ do + maybeThreadId <- readIORef (Engine.engine_playbackThread engine) + unless (isJust maybeThreadId) $ do + threadId <- + forkFinally + (void $ Sequencer.playSequenceOffline sequencer engine startTick maybeEndTick loop) + (\_ -> writeIORef (Engine.engine_playbackThread engine) Nothing) + writeIORef (Engine.engine_playbackThread engine) (Just threadId) + pure Ok renderSequence :: Argument "startTick" Tick -> Argument "endTick" (Maybe Tick) -> Ensemble AudioOutput renderSequence (Argument startTick) (Argument maybeEndTick) = do - sequencer <- asks env_sequencer - engine <- asks env_engine - endTick <- case maybeEndTick of - Just endTick -> pure endTick - Nothing -> liftIO $ Sequencer.getEndTick sequencer - liftIO $ Sequencer.renderSequence sequencer engine startTick endTick - + sequencer <- asks env_sequencer + engine <- asks env_engine + endTick <- case maybeEndTick of + Just endTick -> pure endTick + Nothing -> liftIO $ Sequencer.getEndTick sequencer + liftIO $ Sequencer.renderSequence sequencer engine startTick endTick + clearSequence :: Ensemble Ok clearSequence = do - eventQueue <- asks (Sequencer.sequencer_eventQueue . env_sequencer) - liftIO $ writeIORef eventQueue [] - pure Ok + eventQueue <- asks (Sequencer.sequencer_eventQueue . env_sequencer) + liftIO $ writeIORef eventQueue [] + pure Ok stopPlayback :: Ensemble Ok stopPlayback = do - engine <- asks env_engine - liftIO $ do - maybePlaybackThreadId <- readIORef (Engine.engine_playbackThread engine) - whenJust maybePlaybackThreadId killThread - writeIORef (Engine.engine_steadyTime engine) (-1) - pure Ok + engine <- asks env_engine + liftIO $ do + maybePlaybackThreadId <- readIORef (Engine.engine_playbackThread engine) + whenJust maybePlaybackThreadId killThread + writeIORef (Engine.engine_steadyTime engine) (-1) + pure Ok getCurrentTick :: Ensemble Tick getCurrentTick = do - engine <- asks env_engine - liftIO $ Engine.getCurrentTick engine + engine <- asks env_engine + liftIO $ Engine.getCurrentTick engine ping :: Ensemble Ok ping = pure Ok @@ -221,8 +225,8 @@ ping = pure Ok echo :: Argument "string" Text -> Ensemble Text echo (Argument string) = pure string -deriveJSONs - [ ''Ok - , ''Size - , ''WindowInfo - ] +deriveJSONs + [ ''Ok, + ''Size, + ''WindowInfo + ] diff --git a/src/Ensemble/Config.hs b/src/Ensemble/Config.hs index b5657c7..07af124 100644 --- a/src/Ensemble/Config.hs +++ b/src/Ensemble/Config.hs @@ -4,14 +4,16 @@ import GHC.Generics import Options.Generic data Config = Config - { port :: Maybe Int - , logFile :: Maybe FilePath - } deriving (Show, Generic) + { port :: Maybe Int, + logFile :: Maybe FilePath + } + deriving (Show, Generic) defaultConfig :: Config -defaultConfig = Config - { port = Nothing - , logFile = Nothing +defaultConfig = + Config + { port = Nothing, + logFile = Nothing } instance ParseRecord Config diff --git a/src/Ensemble/Engine.hs b/src/Ensemble/Engine.hs index 78b03bf..164595d 100644 --- a/src/Ensemble/Engine.hs +++ b/src/Ensemble/Engine.hs @@ -4,16 +4,17 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MonoLocalBinds #-} {-# LANGUAGE TemplateHaskell #-} + module Ensemble.Engine where -import Clap.Interface.AudioBuffer (BufferData (..)) -import Clap.Interface.Events (Event (..), MidiEvent(..), MidiData(..), defaultEventConfig) -import Clap.Interface.Host (HostConfig) import Clap.Host (PluginHost (..), PluginLocation) import qualified Clap.Host as CLAP +import Clap.Interface.AudioBuffer (BufferData (..)) +import Clap.Interface.Events (Event (..), MidiData (..), MidiEvent (..), defaultEventConfig) +import Clap.Interface.Host (HostConfig) import Control.Concurrent -import Control.Exception import Control.DeepSeq (NFData) +import Control.Exception import Control.Monad import Control.Monad.Extra (whenJust) import Control.Monad.Reader @@ -24,424 +25,445 @@ import Data.List import Data.Map (Map) import qualified Data.Map as Map import Data.Maybe -import Data.Word import Data.Traversable (for) +import Data.Word import Ensemble.Error import Ensemble.Event import Ensemble.Node import Ensemble.Schema.TH import Ensemble.Tick import Foreign.C.Types +import Foreign.ForeignPtr import Foreign.Marshal.Alloc import Foreign.Marshal.Array -import Foreign.ForeignPtr import Foreign.Ptr +import Sound.PortAudio (Stream, StreamCallbackFlag, StreamResult) import qualified Sound.PortAudio as PortAudio +import Sound.PortAudio.Base (PaDeviceIndex (..), PaDeviceInfo (..), PaStreamCallbackTimeInfo, PaStreamInfo (..), PaTime (..)) import qualified Sound.PortAudio.Base as PortAudio -import Sound.PortAudio (Stream, StreamResult, StreamCallbackFlag) -import Sound.PortAudio.Base (PaDeviceIndex(..), PaDeviceInfo(..), PaStreamCallbackTimeInfo, PaStreamInfo(..), PaTime(..)) -import qualified Sound.PortMidi as PortMidi import Sound.PortMidi (PMEvent) +import qualified Sound.PortMidi as PortMidi data Engine = Engine - { engine_state :: IORef EngineState - , engine_pluginHost :: PluginHost - , engine_nodeCounter :: IORef Int - , engine_nodes :: IORef (Map NodeId Node) - , engine_steadyTime :: IORef Int64 - , engine_sampleRate :: Double - , engine_numberOfFrames :: Word32 - , engine_inputs :: Ptr (Ptr CFloat) - , engine_outputs :: Ptr (Ptr CFloat) - , engine_audioStream :: IORef (Maybe (Stream CFloat CFloat)) - , engine_eventBuffer :: IORef [SequencerEvent] - , engine_playbackThread :: IORef (Maybe ThreadId) - } + { engine_state :: IORef EngineState, + engine_pluginHost :: PluginHost, + engine_nodeCounter :: IORef Int, + engine_nodes :: IORef (Map NodeId Node), + engine_steadyTime :: IORef Int64, + engine_sampleRate :: Double, + engine_numberOfFrames :: Word32, + engine_inputs :: Ptr (Ptr CFloat), + engine_outputs :: Ptr (Ptr CFloat), + engine_audioStream :: IORef (Maybe (Stream CFloat CFloat)), + engine_eventBuffer :: IORef [SequencerEvent], + engine_playbackThread :: IORef (Maybe ThreadId) + } data EngineState - = StateStopped - | StateRunning - | StateStopping + = StateStopped + | StateRunning + | StateStopping createEngine :: HostConfig -> IO Engine createEngine hostConfig = do - state <- newIORef StateStopped - pluginHost <- CLAP.createPluginHost hostConfig - nodeCounter <- newIORef 1 - nodes <- newIORef mempty - steadyTime <- newIORef (-1) - inputs <- newArray [nullPtr, nullPtr] - outputs <- newArray [nullPtr, nullPtr] - audioStream <- newIORef Nothing - eventBuffer <- newIORef [] - playbackThread <- newIORef Nothing - pure $ Engine - { engine_state = state - , engine_pluginHost = pluginHost - , engine_nodeCounter = nodeCounter - , engine_nodes = nodes - , engine_steadyTime = steadyTime - , engine_sampleRate = 44100 - , engine_numberOfFrames = 1024 - , engine_inputs = inputs - , engine_outputs = outputs - , engine_audioStream = audioStream - , engine_eventBuffer = eventBuffer - , engine_playbackThread = playbackThread - } + state <- newIORef StateStopped + pluginHost <- CLAP.createPluginHost hostConfig + nodeCounter <- newIORef 1 + nodes <- newIORef mempty + steadyTime <- newIORef (-1) + inputs <- newArray [nullPtr, nullPtr] + outputs <- newArray [nullPtr, nullPtr] + audioStream <- newIORef Nothing + eventBuffer <- newIORef [] + playbackThread <- newIORef Nothing + pure $ + Engine + { engine_state = state, + engine_pluginHost = pluginHost, + engine_nodeCounter = nodeCounter, + engine_nodes = nodes, + engine_steadyTime = steadyTime, + engine_sampleRate = 44100, + engine_numberOfFrames = 1024, + engine_inputs = inputs, + engine_outputs = outputs, + engine_audioStream = audioStream, + engine_eventBuffer = eventBuffer, + engine_playbackThread = playbackThread + } data AudioDevice = AudioDevice - { audioDevice_index :: Int - , audioDevice_name :: String - } deriving (Show) - -data MidiDevice = MidiDevice - { midiDevice_index :: Int - , midiDevice_interface :: String - , midiDevice_name :: String - , midiDevice_input :: Bool - , midiDevice_output :: Bool - , midiDevice_opened :: Bool - } deriving (Show) + { audioDevice_index :: Int, + audioDevice_name :: String + } + deriving (Show) + +data MidiDevice = MidiDevice + { midiDevice_index :: Int, + midiDevice_interface :: String, + midiDevice_name :: String, + midiDevice_input :: Bool, + midiDevice_output :: Bool, + midiDevice_opened :: Bool + } + deriving (Show) getAudioDevices :: IO [AudioDevice] getAudioDevices = do - eitherResult <- PortAudio.withPortAudio $ do - count <- PortAudio.getNumDevices - let indices = fromIntegral <$> [0 .. count] - devices <- for indices $ \index -> do - eitherInfo <- PortAudio.getDeviceInfo index - pure $ case eitherInfo of - Right info -> Just $ AudioDevice - { audioDevice_index = fromIntegral $ unPaDeviceIndex index - , audioDevice_name = name_PaDeviceInfo info - } - Left _deviceError -> Nothing - pure $ Right $ catMaybes devices - case eitherResult of - Right devices -> pure devices - Left portAudioError -> throwApiError $ "Error getting audio devices: " <> show portAudioError + eitherResult <- PortAudio.withPortAudio $ do + count <- PortAudio.getNumDevices + let indices = fromIntegral <$> [0 .. count] + devices <- for indices $ \index -> do + eitherInfo <- PortAudio.getDeviceInfo index + pure $ case eitherInfo of + Right info -> + Just $ + AudioDevice + { audioDevice_index = fromIntegral $ unPaDeviceIndex index, + audioDevice_name = name_PaDeviceInfo info + } + Left _deviceError -> Nothing + pure $ Right $ catMaybes devices + case eitherResult of + Right devices -> pure devices + Left portAudioError -> throwApiError $ "Error getting audio devices: " <> show portAudioError getMidiDevices :: IO [MidiDevice] getMidiDevices = do - deviceCount <- PortMidi.countDevices - for [0 .. deviceCount - 1] $ \index -> do - deviceInfo <- PortMidi.getDeviceInfo index - pure $ toMidiDevice index deviceInfo - where - toMidiDevice deviceId deviceInfo = MidiDevice - { midiDevice_index = deviceId - , midiDevice_interface = PortMidi.interface deviceInfo - , midiDevice_name = PortMidi.name deviceInfo - , midiDevice_input = PortMidi.input deviceInfo - , midiDevice_output = PortMidi.output deviceInfo - , midiDevice_opened = PortMidi.opened deviceInfo - } + deviceCount <- PortMidi.countDevices + for [0 .. deviceCount - 1] $ \index -> do + deviceInfo <- PortMidi.getDeviceInfo index + pure $ toMidiDevice index deviceInfo + where + toMidiDevice deviceId deviceInfo = + MidiDevice + { midiDevice_index = deviceId, + midiDevice_interface = PortMidi.interface deviceInfo, + midiDevice_name = PortMidi.name deviceInfo, + midiDevice_input = PortMidi.input deviceInfo, + midiDevice_output = PortMidi.output deviceInfo, + midiDevice_opened = PortMidi.opened deviceInfo + } start :: Engine -> IO () start engine = startAudio >> startMidi - where - startAudio = do - maybeAudioStream <- liftIO $ readIORef (engine_audioStream engine) - when (isNothing maybeAudioStream) $ do - initializeResult <- liftIO PortAudio.initialize - whenJust initializeResult $ \initializeError -> - throwApiError $ "Error when initializing audio driver: " <> show initializeError - eitherStream <- liftIO $ PortAudio.openDefaultStream - 0 -- Number of input channels - 2 -- Number of output channels - (engine_sampleRate engine) -- Sample rate - (Just $ fromIntegral $ engine_numberOfFrames engine) -- Frames per buffer - Nothing -- Callback - Nothing -- Callback on completion - case eitherStream of - Left portAudioError -> - throwApiError $ "Error when opening audio stream: " <> show portAudioError - Right stream -> do - maybeError <- do - liftIO $ writeIORef (engine_audioStream engine) (Just stream) - liftIO $ allocateBuffers engine (64 * 1024) - setState engine StateRunning - liftIO $ PortAudio.startStream stream - whenJust maybeError $ \startError -> - throwApiError $ "Error when starting audio stream: " <> show startError - - startMidi = do - initializeResult <- liftIO PortMidi.initialize - case initializeResult of - Right _success -> pure () - Left portMidiError -> do - errorText <- liftIO $ PortMidi.getErrorText portMidiError - throwApiError $ "Error when initializing PortMidi: " <> errorText + where + startAudio = do + maybeAudioStream <- liftIO $ readIORef (engine_audioStream engine) + when (isNothing maybeAudioStream) $ do + initializeResult <- liftIO PortAudio.initialize + whenJust initializeResult $ \initializeError -> + throwApiError $ "Error when initializing audio driver: " <> show initializeError + eitherStream <- + liftIO $ + PortAudio.openDefaultStream + 0 -- Number of input channels + 2 -- Number of output channels + (engine_sampleRate engine) -- Sample rate + (Just $ fromIntegral $ engine_numberOfFrames engine) -- Frames per buffer + Nothing -- Callback + Nothing -- Callback on completion + case eitherStream of + Left portAudioError -> + throwApiError $ "Error when opening audio stream: " <> show portAudioError + Right stream -> do + maybeError <- do + liftIO $ writeIORef (engine_audioStream engine) (Just stream) + liftIO $ allocateBuffers engine (64 * 1024) + setState engine StateRunning + liftIO $ PortAudio.startStream stream + whenJust maybeError $ \startError -> + throwApiError $ "Error when starting audio stream: " <> show startError + + startMidi = do + initializeResult <- liftIO PortMidi.initialize + case initializeResult of + Right _success -> pure () + Left portMidiError -> do + errorText <- liftIO $ PortMidi.getErrorText portMidiError + throwApiError $ "Error when initializing PortMidi: " <> errorText audioCallback :: Engine -> PaStreamCallbackTimeInfo -> [StreamCallbackFlag] -> CULong -> Ptr CFloat -> Ptr CFloat -> IO StreamResult audioCallback engine _timeInfo _flags numberOfInputSamples inputPtr outputPtr = do - receiveInputs engine numberOfInputSamples inputPtr - audioOutput <- generateOutputs engine (fromIntegral numberOfInputSamples) - unless (outputPtr == nullPtr) $ do - let !output = interleave (audioOutput_left audioOutput) (audioOutput_right audioOutput) - pokeArray outputPtr output - atomicModifyIORef' (engine_steadyTime engine) $ \steadyTime -> - (steadyTime + fromIntegral numberOfInputSamples, ()) - state <- readIORef (engine_state engine) - pure $ case state of - StateRunning -> PortAudio.Continue - StateStopping -> PortAudio.Complete - StateStopped -> PortAudio.Abort + receiveInputs engine numberOfInputSamples inputPtr + audioOutput <- generateOutputs engine (fromIntegral numberOfInputSamples) + unless (outputPtr == nullPtr) $ do + let !output = interleave (audioOutput_left audioOutput) (audioOutput_right audioOutput) + pokeArray outputPtr output + atomicModifyIORef' (engine_steadyTime engine) $ \steadyTime -> + (steadyTime + fromIntegral numberOfInputSamples, ()) + state <- readIORef (engine_state engine) + pure $ case state of + StateRunning -> PortAudio.Continue + StateStopping -> PortAudio.Complete + StateStopped -> PortAudio.Abort receiveInputs :: Engine -> CULong -> Ptr CFloat -> IO () receiveInputs engine numberOfInputSamples inputPtr = - unless (inputPtr == nullPtr) $ do - input <- peekArray (fromIntegral $ numberOfInputSamples * 2) inputPtr - let (leftInput, rightInput) = partition (\(i, _) -> even (i * 2)) (zip [0 :: Int ..] input) - [!leftInputBuffer, !rightInputBuffer] <- peekArray 2 $ engine_inputs engine - pokeArray leftInputBuffer (snd <$> leftInput) - pokeArray rightInputBuffer (snd <$> rightInput) - -generateOutputs :: Engine -> Int -> IO AudioOutput + unless (inputPtr == nullPtr) $ do + input <- peekArray (fromIntegral $ numberOfInputSamples * 2) inputPtr + let (leftInput, rightInput) = partition (\(i, _) -> even (i * 2)) (zip [0 :: Int ..] input) + [!leftInputBuffer, !rightInputBuffer] <- peekArray 2 $ engine_inputs engine + pokeArray leftInputBuffer (snd <$> leftInput) + pokeArray rightInputBuffer (snd <$> rightInput) + +generateOutputs :: Engine -> Int -> IO AudioOutput generateOutputs engine frameCount = do - events <- atomicModifyIORef' (engine_eventBuffer engine) $ \eventBuffer -> ([], eventBuffer) - sendEvents engine events - receiveOutputs engine frameCount + events <- atomicModifyIORef' (engine_eventBuffer engine) $ \eventBuffer -> ([], eventBuffer) + sendEvents engine events + receiveOutputs engine frameCount sendEventNow :: Engine -> SequencerEvent -> IO () sendEventNow engine event = - sendEvents engine [event] + sendEvents engine [event] sendEvents :: Engine -> [SequencerEvent] -> IO () sendEvents engine events = do - let clapHost = engine_pluginHost engine - for_ events $ \(SequencerEvent nodeId eventConfig event) -> do - maybeNode <- lookupNode engine nodeId - whenJust maybeNode $ \case - Node_MidiDevice midiDeviceNode -> - liftIOidiDevice midiDeviceNode event - Node_Plugin pluginNode -> - CLAP.processEvent clapHost (pluginNode_id pluginNode) (fromMaybe defaultEventConfig eventConfig) event - where - liftIOidiDevice :: MidiDeviceNode -> Event -> IO () - liftIOidiDevice midiDeviceNode event = do - maybeStartTime <- readIORef (midiDeviceNode_startTime midiDeviceNode) - startTime <- case maybeStartTime of - Just startTime -> pure startTime - Nothing -> do - let (PaTime currentTime) = PortAudio.currentTime undefined - let startTime = round $ currentTime * 1000 - fromIntegral (midiDeviceNode_latency midiDeviceNode) - writeIORef (midiDeviceNode_startTime midiDeviceNode) (Just startTime) - pure startTime - steadyTime <- readIORef (engine_steadyTime engine) - let maybePortMidiEvent = toPortMidiEvent event startTime (steadyTimeToTick (engine_sampleRate engine) steadyTime) - whenJust maybePortMidiEvent $ \portMidiEvent -> do - void $ PortMidi.writeShort (midiDeviceNode_stream midiDeviceNode) portMidiEvent - - toPortMidiEvent :: Event -> Int -> Tick -> Maybe PMEvent - toPortMidiEvent event startTime (Tick currentTick) = - case toMidiData event of - Just midiData -> - Just $ PortMidi.PMEvent - { PortMidi.message = PortMidi.encodeMsg $ PortMidi.PMMsg - { PortMidi.status = fromIntegral $ midiData_first midiData - , PortMidi.data1 = fromIntegral $ midiData_second midiData - , PortMidi.data2 = fromIntegral $ midiData_third midiData - } - , PortMidi.timestamp = fromIntegral $ startTime + currentTick - } - Nothing -> Nothing - - toMidiData :: Event -> Maybe MidiData - toMidiData = \case - Event_Midi midiEvent -> Just (midiEvent_data midiEvent) - _ -> Nothing + let clapHost = engine_pluginHost engine + for_ events $ \(SequencerEvent nodeId eventConfig event) -> do + maybeNode <- lookupNode engine nodeId + whenJust maybeNode $ \case + Node_MidiDevice midiDeviceNode -> + liftIOidiDevice midiDeviceNode event + Node_Plugin pluginNode -> + CLAP.processEvent clapHost (pluginNode_id pluginNode) (fromMaybe defaultEventConfig eventConfig) event + where + liftIOidiDevice :: MidiDeviceNode -> Event -> IO () + liftIOidiDevice midiDeviceNode event = do + maybeStartTime <- readIORef (midiDeviceNode_startTime midiDeviceNode) + startTime <- case maybeStartTime of + Just startTime -> pure startTime + Nothing -> do + let (PaTime currentTime) = PortAudio.currentTime undefined + let startTime = round $ currentTime * 1000 - fromIntegral (midiDeviceNode_latency midiDeviceNode) + writeIORef (midiDeviceNode_startTime midiDeviceNode) (Just startTime) + pure startTime + steadyTime <- readIORef (engine_steadyTime engine) + let maybePortMidiEvent = toPortMidiEvent event startTime (steadyTimeToTick (engine_sampleRate engine) steadyTime) + whenJust maybePortMidiEvent $ \portMidiEvent -> do + void $ PortMidi.writeShort (midiDeviceNode_stream midiDeviceNode) portMidiEvent + + toPortMidiEvent :: Event -> Int -> Tick -> Maybe PMEvent + toPortMidiEvent event startTime (Tick currentTick) = + case toMidiData event of + Just midiData -> + Just $ + PortMidi.PMEvent + { PortMidi.message = + PortMidi.encodeMsg $ + PortMidi.PMMsg + { PortMidi.status = fromIntegral $ midiData_first midiData, + PortMidi.data1 = fromIntegral $ midiData_second midiData, + PortMidi.data2 = fromIntegral $ midiData_third midiData + }, + PortMidi.timestamp = fromIntegral $ startTime + currentTick + } + Nothing -> Nothing + + toMidiData :: Event -> Maybe MidiData + toMidiData = \case + Event_Midi midiEvent -> Just (midiEvent_data midiEvent) + _ -> Nothing receiveOutputs :: Engine -> Int -> IO AudioOutput receiveOutputs engine frameCount = do - let clapHost = engine_pluginHost engine - steadyTime <- readIORef (engine_steadyTime engine) - CLAP.processBeginAll clapHost (fromIntegral frameCount) steadyTime - pluginOutputs <- CLAP.processAll clapHost - pure $ mixOutputs pluginOutputs - + let clapHost = engine_pluginHost engine + steadyTime <- readIORef (engine_steadyTime engine) + CLAP.processBeginAll clapHost (fromIntegral frameCount) steadyTime + pluginOutputs <- CLAP.processAll clapHost + pure $ mixOutputs pluginOutputs + data AudioOutput = AudioOutput - { audioOutput_left :: [CFloat] - , audioOutput_right :: [CFloat] - } deriving (Eq, Ord, Show) + { audioOutput_left :: [CFloat], + audioOutput_right :: [CFloat] + } + deriving (Eq, Ord, Show) instance Semigroup AudioOutput where - a <> b = AudioOutput - { audioOutput_left = audioOutput_left a <> audioOutput_left b - , audioOutput_right = audioOutput_right a <> audioOutput_right b - } + a <> b = + AudioOutput + { audioOutput_left = audioOutput_left a <> audioOutput_left b, + audioOutput_right = audioOutput_right a <> audioOutput_right b + } instance Monoid AudioOutput where - mempty = AudioOutput [] [] + mempty = AudioOutput [] [] size :: AudioOutput -> Int -size (AudioOutput left right) = min (length left) (length right) +size (AudioOutput left right) = min (length left) (length right) isEmpty :: AudioOutput -> Bool isEmpty audioOutput = size audioOutput == 0 mixOutputs :: [CLAP.PluginOutput] -> AudioOutput mixOutputs [] = mempty -mixOutputs pluginOutputs = AudioOutput - { audioOutput_left = foldl1 (zipWith (+)) (CLAP.pluginOutput_leftChannel <$> pluginOutputs) - , audioOutput_right = foldl1 (zipWith (+)) (CLAP.pluginOutput_rightChannel <$> pluginOutputs) +mixOutputs pluginOutputs = + AudioOutput + { audioOutput_left = foldl1 (zipWith (+)) (CLAP.pluginOutput_leftChannel <$> pluginOutputs), + audioOutput_right = foldl1 (zipWith (+)) (CLAP.pluginOutput_rightChannel <$> pluginOutputs) } playAudio :: Engine -> Tick -> Bool -> AudioOutput -> IO () playAudio engine startTick loop audioOutput = do - liftIO $ writeIORef (engine_steadyTime engine) 0 - maybeAudioStream <- liftIO $ do - writeIORef (engine_steadyTime engine) (tickToSteadyTime (engine_sampleRate engine) startTick) - readIORef (engine_audioStream engine) - whenJust maybeAudioStream $ \audioStream -> - let play = do - writeChunks audioStream audioOutput - liftIO $ writeIORef (engine_steadyTime engine) (-1) - in if loop then forever play else play - where - writeChunks stream output = do - eitherAvailableChunkSize <- liftIO $ PortAudio.writeAvailable stream - case eitherAvailableChunkSize of - Right availableChunkSize -> do - let (chunk, remaining) = takeChunk availableChunkSize output - let actualChunkSize = size chunk - sendOutputs engine stream (fromIntegral actualChunkSize) chunk - _steadyTime <- liftIO $ atomicModifyIORef' (engine_steadyTime engine) $ \steadyTime -> - let newSteadyTime = steadyTime + fromIntegral actualChunkSize - in (newSteadyTime, newSteadyTime) - -- TODO: Why is this necessary? Without it, the current tick events are extremely desychronized. - liftIO $ threadDelay 1 - unless (size remaining == 0) $ writeChunks stream remaining - Left audioPortError -> - throwApiError $ "Error getting available frames of audio stream: " <> show audioPortError - + liftIO $ writeIORef (engine_steadyTime engine) 0 + maybeAudioStream <- liftIO $ do + writeIORef (engine_steadyTime engine) (tickToSteadyTime (engine_sampleRate engine) startTick) + readIORef (engine_audioStream engine) + whenJust maybeAudioStream $ \audioStream -> + let play = do + writeChunks audioStream audioOutput + liftIO $ writeIORef (engine_steadyTime engine) (-1) + in if loop then forever play else play + where + writeChunks stream output = do + eitherAvailableChunkSize <- liftIO $ PortAudio.writeAvailable stream + case eitherAvailableChunkSize of + Right availableChunkSize -> do + let (chunk, remaining) = takeChunk availableChunkSize output + let actualChunkSize = size chunk + sendOutputs engine stream (fromIntegral actualChunkSize) chunk + _steadyTime <- liftIO $ atomicModifyIORef' (engine_steadyTime engine) $ \steadyTime -> + let newSteadyTime = steadyTime + fromIntegral actualChunkSize + in (newSteadyTime, newSteadyTime) + -- TODO: Why is this necessary? Without it, the current tick events are extremely desychronized. + liftIO $ threadDelay 1 + unless (size remaining == 0) $ writeChunks stream remaining + Left audioPortError -> + throwApiError $ "Error getting available frames of audio stream: " <> show audioPortError + takeChunk :: Int -> AudioOutput -> (AudioOutput, AudioOutput) -takeChunk chunkSize (AudioOutput left right) = - let (chunkLeft, remainingLeft) = splitAt chunkSize left - (chunkRight, remainingRight) = splitAt chunkSize right - in (AudioOutput chunkLeft chunkRight, AudioOutput remainingLeft remainingRight) - -sendOutputs :: Engine -> Stream CFloat CFloat -> CULong -> AudioOutput -> IO () -sendOutputs _engine stream frameCount audioOutput = do - let output = interleave (audioOutput_left audioOutput) (audioOutput_right audioOutput) - maybeError <- liftIO $ withArray output $ \outputPtr -> do - outputForeignPtr <- newForeignPtr_ outputPtr - PortAudio.writeStream stream frameCount outputForeignPtr - whenJust maybeError $ \writeError -> - throwApiError $ "Error writing to audio stream: " <> show writeError - +takeChunk chunkSize (AudioOutput left right) = + let (chunkLeft, remainingLeft) = splitAt chunkSize left + (chunkRight, remainingRight) = splitAt chunkSize right + in (AudioOutput chunkLeft chunkRight, AudioOutput remainingLeft remainingRight) + +sendOutputs :: Engine -> Stream CFloat CFloat -> CULong -> AudioOutput -> IO () +sendOutputs _engine stream frameCount audioOutput = do + let output = interleave (audioOutput_left audioOutput) (audioOutput_right audioOutput) + maybeError <- liftIO $ withArray output $ \outputPtr -> do + outputForeignPtr <- newForeignPtr_ outputPtr + PortAudio.writeStream stream frameCount outputForeignPtr + whenJust maybeError $ \writeError -> + throwApiError $ "Error writing to audio stream: " <> show writeError + getCurrentTick :: Engine -> IO Tick getCurrentTick engine = do - steadyTime <- readIORef (engine_steadyTime engine) - pure $ steadyTimeToTick (engine_sampleRate engine) steadyTime + steadyTime <- readIORef (engine_steadyTime engine) + pure $ steadyTimeToTick (engine_sampleRate engine) steadyTime stop :: Engine -> IO () stop engine = do - maybeStream <- liftIO $ readIORef (engine_audioStream engine) - case maybeStream of - Just stream -> do - maybeError <- liftIO $ do - CLAP.deactivateAll (engine_pluginHost engine) - void $ PortAudio.stopStream stream - void $ PortAudio.closeStream stream - freeBuffers engine - PortAudio.terminate - case maybeError of - Just stopError -> - throwApiError $ "Error when stopping audio stream: " <> show stopError - Nothing -> - liftIO $ writeIORef (engine_audioStream engine) Nothing - setState engine StateStopped - Nothing -> pure () - terminateResult <- liftIO PortMidi.terminate - case terminateResult of - Right _success -> pure () - Left portMidiError -> do - errorText <- liftIO $ PortMidi.getErrorText portMidiError - throwApiError $ "Error when terminating PortMidi: " <> errorText + maybeStream <- liftIO $ readIORef (engine_audioStream engine) + case maybeStream of + Just stream -> do + maybeError <- liftIO $ do + CLAP.deactivateAll (engine_pluginHost engine) + void $ PortAudio.stopStream stream + void $ PortAudio.closeStream stream + freeBuffers engine + PortAudio.terminate + case maybeError of + Just stopError -> + throwApiError $ "Error when stopping audio stream: " <> show stopError + Nothing -> + liftIO $ writeIORef (engine_audioStream engine) Nothing + setState engine StateStopped + Nothing -> pure () + terminateResult <- liftIO PortMidi.terminate + case terminateResult of + Right _success -> pure () + Left portMidiError -> do + errorText <- liftIO $ PortMidi.getErrorText portMidiError + throwApiError $ "Error when terminating PortMidi: " <> errorText createMidiDeviceNode :: Engine -> Int -> IO NodeId createMidiDeviceNode engine deviceId = do - nodes <- liftIO $ readIORef (engine_nodes engine) - let existingNode = find (\(_, node) -> case node of - Node_MidiDevice midiDeviceNode -> + nodes <- liftIO $ readIORef (engine_nodes engine) + let existingNode = + find + ( \(_, node) -> case node of + Node_MidiDevice midiDeviceNode -> let (DeviceId existingDeviceId) = midiDeviceNode_deviceId midiDeviceNode - in deviceId == existingDeviceId - _ -> False - ) (Map.assocs nodes) - case existingNode of - Just (nodeId, _) -> pure nodeId - Nothing -> do - deviceInfo <- liftIO $ PortMidi.getDeviceInfo deviceId - if PortMidi.output deviceInfo - then do - _stream <- getAudioStream engine - streamInfo <- getAudioStreamInfo engine - let (PaTime (CDouble streamOutputLatency)) = PortAudio.outputLatency streamInfo - let streamLatency = streamOutputLatency * 1000 - sampleLatency = fromIntegral (engine_numberOfFrames engine * 1000 * 2) / engine_sampleRate engine - latency = round $ maximum [streamLatency, sampleLatency, 10] - openOutputResult <- liftIO $ PortMidi.openOutput deviceId latency - case openOutputResult of - Right outputStream -> do - startTime <- liftIO $ newIORef Nothing - let node = Node_MidiDevice $ MidiDeviceNode - { midiDeviceNode_deviceId = DeviceId deviceId - , midiDeviceNode_latency = latency - , midiDeviceNode_startTime = startTime - , midiDeviceNode_stream = outputStream - } - nodeId <- liftIO $ createNodeId engine - liftIO $ modifyIORef' (engine_nodes engine) $ \nodeMap -> - Map.insert nodeId node nodeMap - pure nodeId - Left portMidiError -> do - errorText <- liftIO $ PortMidi.getErrorText portMidiError - throwApiError $ "Error when initializing PortMidi: " <> errorText - else throwApiError $ PortMidi.name deviceInfo <> " is not an output device" + in deviceId == existingDeviceId + _ -> False + ) + (Map.assocs nodes) + case existingNode of + Just (nodeId, _) -> pure nodeId + Nothing -> do + deviceInfo <- liftIO $ PortMidi.getDeviceInfo deviceId + if PortMidi.output deviceInfo + then do + _stream <- getAudioStream engine + streamInfo <- getAudioStreamInfo engine + let (PaTime (CDouble streamOutputLatency)) = PortAudio.outputLatency streamInfo + let streamLatency = streamOutputLatency * 1000 + sampleLatency = fromIntegral (engine_numberOfFrames engine * 1000 * 2) / engine_sampleRate engine + latency = round $ maximum [streamLatency, sampleLatency, 10] + openOutputResult <- liftIO $ PortMidi.openOutput deviceId latency + case openOutputResult of + Right outputStream -> do + startTime <- liftIO $ newIORef Nothing + let node = + Node_MidiDevice $ + MidiDeviceNode + { midiDeviceNode_deviceId = DeviceId deviceId, + midiDeviceNode_latency = latency, + midiDeviceNode_startTime = startTime, + midiDeviceNode_stream = outputStream + } + nodeId <- liftIO $ createNodeId engine + liftIO $ modifyIORef' (engine_nodes engine) $ \nodeMap -> + Map.insert nodeId node nodeMap + pure nodeId + Left portMidiError -> do + errorText <- liftIO $ PortMidi.getErrorText portMidiError + throwApiError $ "Error when initializing PortMidi: " <> errorText + else throwApiError $ PortMidi.name deviceInfo <> " is not an output device" getAudioStream :: Engine -> IO (Stream CFloat CFloat) getAudioStream engine = do - maybeAudioStream <- liftIO $ readIORef (engine_audioStream engine) - case maybeAudioStream of - Just audioStream -> pure audioStream - Nothing -> throwApiError "Audio Stream not available" + maybeAudioStream <- liftIO $ readIORef (engine_audioStream engine) + case maybeAudioStream of + Just audioStream -> pure audioStream + Nothing -> throwApiError "Audio Stream not available" getAudioStreamInfo :: Engine -> IO PaStreamInfo getAudioStreamInfo engine = do - audioStream <- getAudioStream engine - eitherStreamInfo <- liftIO $ PortAudio.getStreamInfo audioStream - case eitherStreamInfo of - Right streamInfo -> pure streamInfo - Left portAudioError -> do - throwApiError $ "Error when retrieving stream info: " <> show portAudioError + audioStream <- getAudioStream engine + eitherStreamInfo <- liftIO $ PortAudio.getStreamInfo audioStream + case eitherStreamInfo of + Right streamInfo -> pure streamInfo + Left portAudioError -> do + throwApiError $ "Error when retrieving stream info: " <> show portAudioError deleteNode :: Engine -> NodeId -> IO () deleteNode engine nodeId = - modifyIORef' (engine_nodes engine) $ Map.delete nodeId + modifyIORef' (engine_nodes engine) $ Map.delete nodeId createNodeId :: Engine -> IO NodeId createNodeId engine = atomicModifyIORef' (engine_nodeCounter engine) $ \nodeCounter -> - (nodeCounter + 1, NodeId nodeCounter) + (nodeCounter + 1, NodeId nodeCounter) lookupNode :: Engine -> NodeId -> IO (Maybe Node) lookupNode engine nodeId = do - nodes <- readIORef $ engine_nodes engine - pure $ Map.lookup nodeId nodes + nodes <- readIORef $ engine_nodes engine + pure $ Map.lookup nodeId nodes createPluginNode :: Engine -> PluginLocation -> IO NodeId createPluginNode engine pluginLocation = liftIO $ do - (pluginId, plugin) <- CLAP.load (engine_pluginHost engine) pluginLocation - nodeId <- createNodeId engine - let node = Node_Plugin $ PluginNode - { pluginNode_id = pluginId - , pluginNode_plugin = plugin + (pluginId, plugin) <- CLAP.load (engine_pluginHost engine) pluginLocation + nodeId <- createNodeId engine + let node = + Node_Plugin $ + PluginNode + { pluginNode_id = pluginId, + pluginNode_plugin = plugin } - CLAP.setPorts plugin (Data32 $ engine_inputs engine) (Data32 $ engine_outputs engine) - CLAP.activate plugin (engine_sampleRate engine) (engine_numberOfFrames engine) - modifyIORef' (engine_nodes engine) $ Map.insert nodeId node - pure nodeId + CLAP.setPorts plugin (Data32 $ engine_inputs engine) (Data32 $ engine_outputs engine) + CLAP.activate plugin (engine_sampleRate engine) (engine_numberOfFrames engine) + modifyIORef' (engine_nodes engine) $ Map.insert nodeId node + pure nodeId -- unloadPlugin :: Engine -> PluginId -> IO () @@ -449,28 +471,28 @@ createPluginNode engine pluginLocation = liftIO $ do allocateBuffers :: Engine -> Int -> IO () allocateBuffers engine bufferSize = do - freeBuffers engine - - inputLeft <- newArray $ replicate bufferSize 0 - inputRight <- newArray $ replicate bufferSize 0 - pokeArray (engine_inputs engine) [inputLeft, inputRight] - - outputLeft <- newArray $ replicate bufferSize 0 - outputRight <- newArray $ replicate bufferSize 0 - pokeArray (engine_outputs engine) [outputLeft, outputRight] + freeBuffers engine + + inputLeft <- newArray $ replicate bufferSize 0 + inputRight <- newArray $ replicate bufferSize 0 + pokeArray (engine_inputs engine) [inputLeft, inputRight] + + outputLeft <- newArray $ replicate bufferSize 0 + outputRight <- newArray $ replicate bufferSize 0 + pokeArray (engine_outputs engine) [outputLeft, outputRight] freeBuffers :: Engine -> IO () freeBuffers engine = do - [inputLeft, inputRight] <- peekArray 2 $ engine_inputs engine - free inputLeft - free inputRight + [inputLeft, inputRight] <- peekArray 2 $ engine_inputs engine + free inputLeft + free inputRight - [outputLeft, outputRight] <- peekArray 2 $ engine_outputs engine - free outputLeft - free outputRight + [outputLeft, outputRight] <- peekArray 2 $ engine_outputs engine + free outputLeft + free outputRight - pokeArray (engine_inputs engine) [nullPtr, nullPtr] - pokeArray (engine_outputs engine) [nullPtr, nullPtr] + pokeArray (engine_inputs engine) [nullPtr, nullPtr] + pokeArray (engine_outputs engine) [nullPtr, nullPtr] setState :: Engine -> EngineState -> IO () setState engine = liftIO . writeIORef (engine_state engine) @@ -482,12 +504,12 @@ interleave xs ys = concat (transpose [xs, ys]) throwApiError :: String -> a throwApiError message = - throw $ ApiError { apiError_message = message } + throw $ ApiError {apiError_message = message} deriveJSONs - [ ''AudioDevice - , ''AudioOutput - , ''MidiDevice - ] + [ ''AudioDevice, + ''AudioOutput, + ''MidiDevice + ] deriving instance NFData AudioOutput diff --git a/src/Ensemble/Env.hs b/src/Ensemble/Env.hs index a6c2ccb..e7e656b 100644 --- a/src/Ensemble/Env.hs +++ b/src/Ensemble/Env.hs @@ -1,4 +1,3 @@ - {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} @@ -13,15 +12,15 @@ import Ensemble.Config import Ensemble.Engine import Ensemble.Sequencer -newtype Ensemble a = Ensemble { unEnsemble :: ReaderT Env IO a } - deriving newtype (Monad, Applicative, Functor, MonadReader Env, MonadIO) +newtype Ensemble a = Ensemble {unEnsemble :: ReaderT Env IO a} + deriving newtype (Monad, Applicative, Functor, MonadReader Env, MonadIO) -data Env = Env - { env_config :: Config - , env_sequencer :: Sequencer - , env_engine :: Engine - , env_messageChannel :: Chan Value - , env_pluginGuiThreadId :: IORef (Maybe ThreadId) +data Env = Env + { env_config :: Config, + env_sequencer :: Sequencer, + env_engine :: Engine, + env_messageChannel :: Chan Value, + env_pluginGuiThreadId :: IORef (Maybe ThreadId) } createEnv :: Config -> IO Env @@ -30,13 +29,14 @@ createEnv config = do engine <- createEngine defaultHostConfig messageChannel <- newChan pluginGuiThreadId <- newIORef Nothing - pure $ Env - { env_config = config - , env_sequencer = sequencer - , env_engine = engine - , env_messageChannel = messageChannel - , env_pluginGuiThreadId = pluginGuiThreadId - } + pure $ + Env + { env_config = config, + env_sequencer = sequencer, + env_engine = engine, + env_messageChannel = messageChannel, + env_pluginGuiThreadId = pluginGuiThreadId + } runEnsemble :: Env -> Ensemble a -> IO a runEnsemble env action = runReaderT (unEnsemble action) env diff --git a/src/Ensemble/Error.hs b/src/Ensemble/Error.hs index 543f3d7..fa6ce70 100644 --- a/src/Ensemble/Error.hs +++ b/src/Ensemble/Error.hs @@ -1,11 +1,12 @@ {-# LANGUAGE DeriveAnyClass #-} -module Ensemble.Error where +module Ensemble.Error where import Control.Exception -data ApiError = ApiError - { apiError_message :: String - } deriving (Show, Eq) +data ApiError = ApiError + { apiError_message :: String + } + deriving (Show, Eq) deriving instance Exception ApiError diff --git a/src/Ensemble/Event.hs b/src/Ensemble/Event.hs index a93c225..88a0cff 100644 --- a/src/Ensemble/Event.hs +++ b/src/Ensemble/Event.hs @@ -1,77 +1,78 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} module Ensemble.Event where -import qualified Data.Aeson as A import qualified Clap.Host as Clap -import qualified Clap.Interface.Id as Clap import qualified Clap.Interface.Events as Clap -import qualified Clap.Interface.Plugin as Clap import qualified Clap.Interface.Extension.Params as Clap +import qualified Clap.Interface.Id as Clap +import qualified Clap.Interface.Plugin as Clap import qualified Clap.Interface.Version as Clap import qualified Clap.Library as Clap +import qualified Data.Aeson as A import Data.Text (pack) import Ensemble.Node import Ensemble.Schema.TH import Ensemble.Tick -import Foreign.Ptr import Foreign.C.Types +import Foreign.Ptr -instance A.ToJSON (Ptr a) where - toJSON ptr = A.String (pack $ show ptr) +instance A.ToJSON (Ptr a) where + toJSON ptr = A.String (pack $ show ptr) instance A.FromJSON (Ptr a) where - parseJSON _ = pure nullPtr + parseJSON _ = pure nullPtr data SequencerEvent = SequencerEvent - { sequencerEvent_nodeId :: NodeId - , sequencerEvent_eventConfig :: Maybe Clap.EventConfig - , sequencerEvent_event :: Clap.Event - } deriving (Show) + { sequencerEvent_nodeId :: NodeId, + sequencerEvent_eventConfig :: Maybe Clap.EventConfig, + sequencerEvent_event :: Clap.Event + } + deriving (Show) -data PlaybackEvent - = PlaybackEvent_Rendering - | PlaybackEvent_Started - | PlaybackEvent_Stopped - | PlaybackEvent_Looped - | PlaybackEvent_CurrentTick Tick - deriving (Show) +data PlaybackEvent + = PlaybackEvent_Rendering + | PlaybackEvent_Started + | PlaybackEvent_Stopped + | PlaybackEvent_Looped + | PlaybackEvent_CurrentTick Tick + deriving (Show) deriveJSONs - [ ''CFloat - , ''Clap.ClapId - , ''Clap.ParamId - , ''Clap.PluginId - , ''Clap.ClapVersion - , ''Clap.PluginDescriptor - , ''Clap.PluginInfo - , ''Clap.EventFlag - , ''Clap.NoteEvent - , ''Clap.NoteKillEvent - , ''Clap.NoteExpression - , ''Clap.NoteExpressionEvent - , ''Clap.ParameterFlag - , ''Clap.ParameterInfo - , ''Clap.ParamValueEvent - , ''Clap.ParamModEvent - , ''Clap.ParamGestureEvent - , ''Clap.TransportFlag - , ''Clap.TransportEvent - , ''Clap.MidiData - , ''Clap.MidiEvent - , ''Clap.MidiSysexEvent - , ''Clap.Midi2Data - , ''Clap.Midi2Event - , ''Clap.EventConfig - ] + [ ''CFloat, + ''Clap.ClapId, + ''Clap.ParamId, + ''Clap.PluginId, + ''Clap.ClapVersion, + ''Clap.PluginDescriptor, + ''Clap.PluginInfo, + ''Clap.EventFlag, + ''Clap.NoteEvent, + ''Clap.NoteKillEvent, + ''Clap.NoteExpression, + ''Clap.NoteExpressionEvent, + ''Clap.ParameterFlag, + ''Clap.ParameterInfo, + ''Clap.ParamValueEvent, + ''Clap.ParamModEvent, + ''Clap.ParamGestureEvent, + ''Clap.TransportFlag, + ''Clap.TransportEvent, + ''Clap.MidiData, + ''Clap.MidiEvent, + ''Clap.MidiSysexEvent, + ''Clap.Midi2Data, + ''Clap.Midi2Event, + ''Clap.EventConfig + ] deriveCustomJSONs - [ ''Clap.Event - ] + [ ''Clap.Event + ] deriveJSONs - [ ''SequencerEvent - , ''PlaybackEvent - ] + [ ''SequencerEvent, + ''PlaybackEvent + ] diff --git a/src/Ensemble/Handler.hs b/src/Ensemble/Handler.hs index 8f56196..fcda315 100644 --- a/src/Ensemble/Handler.hs +++ b/src/Ensemble/Handler.hs @@ -1,7 +1,8 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + module Ensemble.Handler where import Control.Exception @@ -9,32 +10,32 @@ import qualified Data.Aeson as A import Data.Aeson.KeyMap (KeyMap) import qualified Data.Aeson.KeyMap as KeyMap import Data.Maybe +import Ensemble.Env import Ensemble.Error import Ensemble.Schema (handleMessage) import Ensemble.Schema.TH import Ensemble.Schema.TaggedJSON (toTaggedJSON) -import Ensemble.Env import GHC.Stack deriveJSON ''ApiError -handler :: HasCallStack => Env -> KeyMap A.Value -> IO (KeyMap A.Value) +handler :: (HasCallStack) => Env -> KeyMap A.Value -> IO (KeyMap A.Value) handler env object = runEnsemble env $ - case KeyMap.lookup "@type" object of - Just (A.String messageType) -> - handleMessage messageType object - Just _ -> - throw $ ApiError "Invalid '@type' field" - Nothing -> - throw $ ApiError "Message is missing '@type' field" + case KeyMap.lookup "@type" object of + Just (A.String messageType) -> + handleMessage messageType object + Just _ -> + throw $ ApiError "Invalid '@type' field" + Nothing -> + throw $ ApiError "Message is missing '@type' field" -receiveMessage :: HasCallStack => Env -> A.Value -> IO A.Value +receiveMessage :: (HasCallStack) => Env -> A.Value -> IO A.Value receiveMessage env = \case - A.Object object -> do - let extraValue = KeyMap.lookup "@extra" object - outMessage <- handler env object - pure $ A.Object $ KeyMap.insert "@extra" (fromMaybe A.Null extraValue) outMessage - _ -> pure $ makeError Nothing $ ApiError "Invalid JSON input, needs to be object" - where - makeError extraValue apiError = - A.Object $ KeyMap.insert "@extra" (fromMaybe A.Null extraValue) (toTaggedJSON apiError) + A.Object object -> do + let extraValue = KeyMap.lookup "@extra" object + outMessage <- handler env object + pure $ A.Object $ KeyMap.insert "@extra" (fromMaybe A.Null extraValue) outMessage + _ -> pure $ makeError Nothing $ ApiError "Invalid JSON input, needs to be object" + where + makeError extraValue apiError = + A.Object $ KeyMap.insert "@extra" (fromMaybe A.Null extraValue) (toTaggedJSON apiError) diff --git a/src/Ensemble/Node.hs b/src/Ensemble/Node.hs index 532d9dc..cbd28b4 100644 --- a/src/Ensemble/Node.hs +++ b/src/Ensemble/Node.hs @@ -7,26 +7,26 @@ import Data.IORef import Ensemble.Schema.TH import Sound.PortMidi (PMStream) -newtype NodeId = NodeId { nodeId_id :: Int } - deriving (Show, Ord, Eq) +newtype NodeId = NodeId {nodeId_id :: Int} + deriving (Show, Ord, Eq) -newtype DeviceId = DeviceId { deviceId_id :: Int } - deriving (Show, Ord, Eq) +newtype DeviceId = DeviceId {deviceId_id :: Int} + deriving (Show, Ord, Eq) -data Node - = Node_MidiDevice MidiDeviceNode - | Node_Plugin PluginNode +data Node + = Node_MidiDevice MidiDeviceNode + | Node_Plugin PluginNode data MidiDeviceNode = MidiDeviceNode - { midiDeviceNode_deviceId :: DeviceId - , midiDeviceNode_latency :: Int - , midiDeviceNode_startTime :: IORef (Maybe Int) - , midiDeviceNode_stream :: PMStream - } + { midiDeviceNode_deviceId :: DeviceId, + midiDeviceNode_latency :: Int, + midiDeviceNode_startTime :: IORef (Maybe Int), + midiDeviceNode_stream :: PMStream + } data PluginNode = PluginNode - { pluginNode_id :: PluginId - , pluginNode_plugin :: Plugin - } + { pluginNode_id :: PluginId, + pluginNode_plugin :: Plugin + } deriveJSON ''NodeId diff --git a/src/Ensemble/Schema.hs b/src/Ensemble/Schema.hs index 889a036..b62a587 100644 --- a/src/Ensemble/Schema.hs +++ b/src/Ensemble/Schema.hs @@ -3,8 +3,6 @@ module Ensemble.Schema where -import Prelude hiding (FilePath) - import qualified Clap.Host as Clap import qualified Clap.Interface.Events as Clap import qualified Clap.Interface.Extension.Params as Clap @@ -12,55 +10,56 @@ import qualified Clap.Interface.Plugin as Clap import qualified Clap.Interface.Version as Clap import qualified Clap.Library as Clap import Ensemble.API -import Ensemble.Engine (AudioDevice(..), AudioOutput(..), MidiDevice(..)) +import Ensemble.Engine (AudioDevice (..), AudioOutput (..), MidiDevice (..)) +import Ensemble.Env import Ensemble.Error import Ensemble.Event import Ensemble.Schema.TH -import Ensemble.Env +import Prelude hiding (FilePath) makeAPI - -- types - [ ''ApiError - , ''AudioDevice - , ''AudioOutput - , ''Clap.ClapVersion - , ''Clap.Event - , ''Clap.EventConfig - , ''Clap.EventFlag - , ''Clap.ParameterInfo - , ''Clap.PluginDescriptor - , ''Clap.PluginId - , ''Clap.PluginInfo - , ''Clap.MidiData - , ''Clap.Midi2Data - , ''Clap.NoteExpression - , ''Clap.TransportFlag - , ''MidiDevice - , ''Ok - , ''PlaybackEvent - , ''SequencerEvent - , ''Size - , ''WindowInfo - ] - -- functions - [ 'clearSequence - , 'createMidiDeviceNode - , 'createPluginNode - , 'deleteNode - , 'echo - , 'getAudioDevices - , 'getCurrentTick - , 'getMidiDevices - , 'getPluginLocations - , 'getPluginParameters - , 'getPluginParameterValue - , 'openPluginGUI - , 'ping - , 'playSequence - , 'scanForPlugins - , 'sendEvent - , 'scheduleEvent - , 'startEngine - , 'stopEngine - , 'stopPlayback - ] + -- types + [ ''ApiError, + ''AudioDevice, + ''AudioOutput, + ''Clap.ClapVersion, + ''Clap.Event, + ''Clap.EventConfig, + ''Clap.EventFlag, + ''Clap.ParameterInfo, + ''Clap.PluginDescriptor, + ''Clap.PluginId, + ''Clap.PluginInfo, + ''Clap.MidiData, + ''Clap.Midi2Data, + ''Clap.NoteExpression, + ''Clap.TransportFlag, + ''MidiDevice, + ''Ok, + ''PlaybackEvent, + ''SequencerEvent, + ''Size, + ''WindowInfo + ] + -- functions + [ 'clearSequence, + 'createMidiDeviceNode, + 'createPluginNode, + 'deleteNode, + 'echo, + 'getAudioDevices, + 'getCurrentTick, + 'getMidiDevices, + 'getPluginLocations, + 'getPluginParameters, + 'getPluginParameterValue, + 'openPluginGUI, + 'ping, + 'playSequence, + 'scanForPlugins, + 'sendEvent, + 'scheduleEvent, + 'startEngine, + 'stopEngine, + 'stopPlayback + ] diff --git a/src/Ensemble/Schema/TH.hs b/src/Ensemble/Schema/TH.hs index 80346ec..f21b043 100644 --- a/src/Ensemble/Schema/TH.hs +++ b/src/Ensemble/Schema/TH.hs @@ -6,8 +6,6 @@ module Ensemble.Schema.TH where -import Prelude hiding (break) - import Control.Exception import Control.Monad.Reader import qualified Data.Aeson as A @@ -16,305 +14,359 @@ import Data.Aeson.KeyMap (KeyMap) import qualified Data.Aeson.KeyMap as KeyMap import qualified Data.Aeson.TH as A import qualified Data.Aeson.Types as A -import Data.Foldable (traverse_, foldlM, foldl') +import Data.Foldable (foldl', foldlM, traverse_) import Data.Text (Text, unpack) import Data.Traversable (for) import Ensemble.Error -import Ensemble.Util import Ensemble.Schema.TaggedJSON +import Ensemble.Util import Foreign.Ptr import GHC.Generics (Generic) import GHC.Stack import GHC.TypeLits import Language.Haskell.TH import Language.Haskell.TH.Datatype +import Prelude hiding (break) data Argument (name :: Symbol) t = Argument t encodingOptions :: A.Options -encodingOptions = - let modifier field = case split '_' field of - [name] -> name - _prefix:[name] -> name - _ -> field - in A.defaultOptions - { A.fieldLabelModifier = modifier - , A.constructorTagModifier = modifier - , A.sumEncoding = A.UntaggedValue - , A.unwrapUnaryRecords = True +encodingOptions = + let modifier field = case split '_' field of + [name] -> name + _prefix : [name] -> name + _ -> field + in A.defaultOptions + { A.fieldLabelModifier = modifier, + A.constructorTagModifier = modifier, + A.sumEncoding = A.UntaggedValue, + A.unwrapUnaryRecords = True } -deriveHasTypeTag :: HasCallStack => Name -> DecQ +deriveHasTypeTag :: (HasCallStack) => Name -> DecQ deriveHasTypeTag name = do - let classType = ConT ''HasTypeTag - let forType = ConT name - let functionName = 'typeTag - datatype <- reifyDatatype name - case datatypeCons datatype of - [single] -> - case datatypeVariant datatype of - Newtype -> do - resolvedTypeName <- showResolvedType $ head $ constructorFields single - let body = NormalB $ LitE $ StringL $ uncapitalise resolvedTypeName - pure $ InstanceD Nothing [] (AppT classType forType) - [FunD functionName [Clause [VarP (mkName "_")] body []]] - _ -> do - let body = NormalB $ LitE $ StringL $ toSubclassName (constructorName single) - pure $ InstanceD Nothing [] (AppT classType forType) - [FunD functionName [Clause [VarP (mkName "_")] body []]] - multiple -> do - let objectName = mkName "object" - let matches = - (\constructor -> - let pattern = ConP (constructorName constructor) [] $ VarP (mkName "_") <$ constructorFields constructor - caseBody = NormalB $ LitE $ StringL $ toSubclassName (constructorName constructor) - in Match pattern caseBody [] - ) <$> multiple - let body = NormalB $ CaseE (VarE objectName) matches - pure $ InstanceD Nothing [] (AppT classType forType) - [FunD functionName - [Clause [VarP objectName] body []]] - + let classType = ConT ''HasTypeTag + let forType = ConT name + let functionName = 'typeTag + datatype <- reifyDatatype name + case datatypeCons datatype of + [single] -> + case datatypeVariant datatype of + Newtype -> do + resolvedTypeName <- showResolvedType $ head $ constructorFields single + let body = NormalB $ LitE $ StringL $ uncapitalise resolvedTypeName + pure $ + InstanceD + Nothing + [] + (AppT classType forType) + [FunD functionName [Clause [VarP (mkName "_")] body []]] + _ -> do + let body = NormalB $ LitE $ StringL $ toSubclassName (constructorName single) + pure $ + InstanceD + Nothing + [] + (AppT classType forType) + [FunD functionName [Clause [VarP (mkName "_")] body []]] + multiple -> do + let objectName = mkName "object" + let matches = + ( \constructor -> + let pattern = ConP (constructorName constructor) [] $ VarP (mkName "_") <$ constructorFields constructor + caseBody = NormalB $ LitE $ StringL $ toSubclassName (constructorName constructor) + in Match pattern caseBody [] + ) + <$> multiple + let body = NormalB $ CaseE (VarE objectName) matches + pure $ + InstanceD + Nothing + [] + (AppT classType forType) + [ FunD + functionName + [Clause [VarP objectName] body []] + ] deriveCustomJSONs :: [Name] -> DecsQ deriveCustomJSONs names = do - decs <- for names $ \name -> do - fromJson <- deriveCustomFromJSON name - toJson <- deriveCustomToJSON name - toTaggedJson <- deriveHasTypeTag name - pure [fromJson, toJson, toTaggedJson] - pure $ join decs + decs <- for names $ \name -> do + fromJson <- deriveCustomFromJSON name + toJson <- deriveCustomToJSON name + toTaggedJson <- deriveHasTypeTag name + pure [fromJson, toJson, toTaggedJson] + pure $ join decs -deriveCustomToJSON :: HasCallStack => Name -> DecQ +deriveCustomToJSON :: (HasCallStack) => Name -> DecQ deriveCustomToJSON name = do - constructors <- datatypeCons <$> reifyDatatype name - let objectName = mkName "object" - let matches = (\constructor -> + constructors <- datatypeCons <$> reifyDatatype name + let objectName = mkName "object" + let matches = + ( \constructor -> let valueName = mkName "value" pattern = ConP (constructorName constructor) [] [VarP valueName] - caseBody = NormalB $ multiAppE (VarE 'toTaggedCustomJSON) - [ LitE $ StringL $ toSubclassName $ constructorName constructor - , VarE valueName - ] - in Match pattern caseBody [] - ) <$> constructors - let body = NormalB $ CaseE (VarE objectName) matches - pure $ InstanceD Nothing [] (AppT (ConT ''A.ToJSON) (ConT name)) - [FunD 'A.toJSON [Clause [VarP objectName] body []]] + caseBody = + NormalB $ + multiAppE + (VarE 'toTaggedCustomJSON) + [ LitE $ StringL $ toSubclassName $ constructorName constructor, + VarE valueName + ] + in Match pattern caseBody [] + ) + <$> constructors + let body = NormalB $ CaseE (VarE objectName) matches + pure $ + InstanceD + Nothing + [] + (AppT (ConT ''A.ToJSON) (ConT name)) + [FunD 'A.toJSON [Clause [VarP objectName] body []]] -deriveCustomFromJSON :: HasCallStack => Name -> DecQ +deriveCustomFromJSON :: (HasCallStack) => Name -> DecQ deriveCustomFromJSON name = do - constructors <- datatypeCons <$> reifyDatatype name - let objectName = mkName "object" - let eventTypeName = mkName "eventType" - let otherName = mkName "other" - let failureMessage = AppE (AppE (VarE '(<>)) (LitE $ StringL $ "Invalid " <> nameBase name <> " type: ")) (VarE otherName) - let failureCase = Match (VarP otherName) (NormalB $ AppE (VarE 'A.parseFail) failureMessage) [] - let matches = fmap (\constructor -> - let pattern = LitP $ StringL $ toSubclassName (constructorName constructor) - parsed = AppE (VarE 'A.parseJSON) (AppE (ConE 'A.Object) (VarE objectName)) - caseBody = NormalB $ AppE (AppE (VarE 'fmap) (ConE $ constructorName constructor)) parsed - in Match pattern caseBody [] - ) constructors - let body = NormalB $ AppE (AppE (VarE 'A.withObject) (LitE $ StringL $ nameBase name)) (LamE [VarP objectName] $ - DoE Nothing - [ BindS (VarP eventTypeName ) (AppE (AppE (VarE 'A.parseField) (VarE objectName)) (LitE $ StringL "@type")) - , NoBindS $ CaseE (AppE (VarE 'A.toString) (VarE eventTypeName)) (matches <> [failureCase]) - ]) - pure $ InstanceD Nothing [] (AppT (ConT ''A.FromJSON) (ConT name)) - [FunD 'A.parseJSON [Clause [] body []]] + constructors <- datatypeCons <$> reifyDatatype name + let objectName = mkName "object" + let eventTypeName = mkName "eventType" + let otherName = mkName "other" + let failureMessage = AppE (AppE (VarE '(<>)) (LitE $ StringL $ "Invalid " <> nameBase name <> " type: ")) (VarE otherName) + let failureCase = Match (VarP otherName) (NormalB $ AppE (VarE 'A.parseFail) failureMessage) [] + let matches = + fmap + ( \constructor -> + let pattern = LitP $ StringL $ toSubclassName (constructorName constructor) + parsed = AppE (VarE 'A.parseJSON) (AppE (ConE 'A.Object) (VarE objectName)) + caseBody = NormalB $ AppE (AppE (VarE 'fmap) (ConE $ constructorName constructor)) parsed + in Match pattern caseBody [] + ) + constructors + let body = + NormalB $ + AppE + (AppE (VarE 'A.withObject) (LitE $ StringL $ nameBase name)) + ( LamE [VarP objectName] $ + DoE + Nothing + [ BindS (VarP eventTypeName) (AppE (AppE (VarE 'A.parseField) (VarE objectName)) (LitE $ StringL "@type")), + NoBindS $ CaseE (AppE (VarE 'A.toString) (VarE eventTypeName)) (matches <> [failureCase]) + ] + ) + pure $ + InstanceD + Nothing + [] + (AppT (ConT ''A.FromJSON) (ConT name)) + [FunD 'A.parseJSON [Clause [] body []]] -deriveJSON :: HasCallStack => Name -> DecsQ +deriveJSON :: (HasCallStack) => Name -> DecsQ deriveJSON name = do - let generic = StandaloneDerivD Nothing [] (ConT ''Generic `AppT` ConT name) - fromJson <- A.deriveToJSON encodingOptions name - toJson <- A.deriveFromJSON encodingOptions name - toTaggedJson <- deriveHasTypeTag name - pure $ [generic] <> fromJson <> toJson <> [toTaggedJson] + let generic = StandaloneDerivD Nothing [] (ConT ''Generic `AppT` ConT name) + fromJson <- A.deriveToJSON encodingOptions name + toJson <- A.deriveFromJSON encodingOptions name + toTaggedJson <- deriveHasTypeTag name + pure $ [generic] <> fromJson <> toJson <> [toTaggedJson] deriveJSONs :: [Name] -> DecsQ deriveJSONs names = do - deriveSchemaDecs <- traverse deriveJSON names - pure $ join deriveSchemaDecs + deriveSchemaDecs <- traverse deriveJSON names + pure $ join deriveSchemaDecs schemaHeader :: String schemaHeader = "vector {t:Type} # [ t ] = Vector t;" writeSchema :: [String] -> [String] -> IO () writeSchema types functions = do - let fileName = "ensemble.tl" - writeFile fileName schemaHeader - traverse_ (appendFile fileName . (<>) "\n\n") types - appendFile fileName "\n\n---functions---" - traverse_ (appendFile fileName . (<>) "\n\n") functions + let fileName = "ensemble.tl" + writeFile fileName schemaHeader + traverse_ (appendFile fileName . (<>) "\n\n") types + appendFile fileName "\n\n---functions---" + traverse_ (appendFile fileName . (<>) "\n\n") functions -showResolvedType :: HasCallStack => Type -> Q String +showResolvedType :: (HasCallStack) => Type -> Q String showResolvedType type' = resolveTypeSynonyms type' >>= showType True -showUnresolvedType :: HasCallStack => Type -> Q String +showUnresolvedType :: (HasCallStack) => Type -> Q String showUnresolvedType = showType False -showReturnType :: HasCallStack => Type -> Q String +showReturnType :: (HasCallStack) => Type -> Q String showReturnType = \case - AppT _ returnType -> showResolvedType returnType - other -> error $ "Invalid return type: " <> show other + AppT _ returnType -> showResolvedType returnType + other -> error $ "Invalid return type: " <> show other -showType :: HasCallStack => Bool -> Type -> Q String +showType :: (HasCallStack) => Bool -> Type -> Q String showType resolve = \case - ConT name -> if - | nameBase name == "CFloat" -> pure "Float" - | nameBase name == "Text" -> pure "String" - | not resolve -> pure $ nameBase name - | otherwise -> do - typeInfo <- reifyDatatype name - if | datatypeVariant typeInfo == Newtype -> showType resolve $ head (constructorFields (head (datatypeCons typeInfo))) - | otherwise -> pure $ nameBase name - TupleT 0 -> pure "Void" - AppT ListT itemType -> if - | itemType == ConT ''Char -> pure "String" - | otherwise -> do - itemTypeString <- showType resolve itemType - pure $ "vector<" <> itemTypeString <> ">" - AppT (ConT name) innerType -> if - | name == ''Ptr -> pure "Void" - | name == ''Maybe -> showType resolve innerType - | otherwise -> error $ "Unrepresentable higher order type: " <> show name - other -> error $ "Unrepresentable type: " <> show other + ConT name -> + if + | nameBase name == "CFloat" -> pure "Float" + | nameBase name == "Text" -> pure "String" + | not resolve -> pure $ nameBase name + | otherwise -> do + typeInfo <- reifyDatatype name + if + | datatypeVariant typeInfo == Newtype -> showType resolve $ head (constructorFields (head (datatypeCons typeInfo))) + | otherwise -> pure $ nameBase name + TupleT 0 -> pure "Void" + AppT ListT itemType -> + if + | itemType == ConT ''Char -> pure "String" + | otherwise -> do + itemTypeString <- showType resolve itemType + pure $ "vector<" <> itemTypeString <> ">" + AppT (ConT name) innerType -> + if + | name == ''Ptr -> pure "Void" + | name == ''Maybe -> showType resolve innerType + | otherwise -> error $ "Unrepresentable higher order type: " <> show name + other -> error $ "Unrepresentable type: " <> show other -showField :: HasCallStack => (Name, Type) -> Q String +showField :: (HasCallStack) => (Name, Type) -> Q String showField (fieldName, fieldType) = do - let nameString = last (split '_' (last (split '.' $ show fieldName))) - typeString <- showResolvedType fieldType - pure $ nameString <> ":" <> typeString + let nameString = last (split '_' (last (split '.' $ show fieldName))) + typeString <- showResolvedType fieldType + pure $ nameString <> ":" <> typeString getArgumentTypes :: Type -> [Type] getArgumentTypes = \case - AppT (AppT ArrowT argumentType) rest -> argumentType:getArgumentTypes rest - other -> [other] + AppT (AppT ArrowT argumentType) rest -> argumentType : getArgumentTypes rest + other -> [other] -showFunctionType :: HasCallStack => Type -> Q String -showFunctionType functionType = - case getArgumentTypes functionType of - [returnType] -> do - returnTypeString <- showReturnType returnType - pure $ "= " <> returnTypeString - types -> do - returnTypeString <- showReturnType (last types) - arguments <- foldlM (\acc -> \case - AppT (AppT (ConT _) (LitT (StrTyLit argumentName))) argumentType -> do - resolvedTypeString <- showResolvedType argumentType - pure $ acc <> argumentName <> ":" <> resolvedTypeString <> " " - other -> error $ "Invalid argument type: " <> show other) - "" - (init types) - pure $ arguments <> "= " <> returnTypeString +showFunctionType :: (HasCallStack) => Type -> Q String +showFunctionType functionType = + case getArgumentTypes functionType of + [returnType] -> do + returnTypeString <- showReturnType returnType + pure $ "= " <> returnTypeString + types -> do + returnTypeString <- showReturnType (last types) + arguments <- + foldlM + ( \acc -> \case + AppT (AppT (ConT _) (LitT (StrTyLit argumentName))) argumentType -> do + resolvedTypeString <- showResolvedType argumentType + pure $ acc <> argumentName <> ":" <> resolvedTypeString <> " " + other -> error $ "Invalid argument type: " <> show other + ) + "" + (init types) + pure $ arguments <> "= " <> returnTypeString -generateTypeDefinition :: HasCallStack => Name -> Q [String] +generateTypeDefinition :: (HasCallStack) => Name -> Q [String] generateTypeDefinition typeName = do - datatypeInfo <- reifyDatatype typeName - for (datatypeCons datatypeInfo) $ \constructorInfo -> do - let name = toSubclassName $ constructorName constructorInfo - case constructorVariant constructorInfo of - RecordConstructor fieldNames -> do - let fields = zip fieldNames (constructorFields constructorInfo) - fieldsString <- foldlM (\acc field -> do - fieldString <- showField field - pure $ acc <> fieldString <> " ") (name <> " ") fields - pure $ fieldsString <> "= " <> nameBase typeName <> ";" - _ -> - case constructorFields constructorInfo of - [] -> pure $ name <> " = " <> nameBase typeName <> ";" - [ConT singleField] -> do - fieldInfo <- reifyDatatype singleField - case datatypeCons fieldInfo of - [subConstructorInfo] -> - case constructorVariant subConstructorInfo of - RecordConstructor fieldNames -> do - let fields = zip fieldNames (constructorFields subConstructorInfo) - fieldsString <- foldlM (\acc field -> do - fieldString <- showField field - pure $ acc <> fieldString <> " ") (name <> " ") fields - pure $ fieldsString <> " = " <> nameBase typeName <> ";" - _ -> do - typeString <- showResolvedType (ConT singleField) - pure $ name <> " value:" <> typeString <> " = " <> nameBase typeName <> ";" - _ -> error $ "Invalid constructor field: " <> show fieldInfo - _ -> - error $ "Invalid constructor fields: " <> show constructorInfo + datatypeInfo <- reifyDatatype typeName + for (datatypeCons datatypeInfo) $ \constructorInfo -> do + let name = toSubclassName $ constructorName constructorInfo + case constructorVariant constructorInfo of + RecordConstructor fieldNames -> do + let fields = zip fieldNames (constructorFields constructorInfo) + fieldsString <- + foldlM + ( \acc field -> do + fieldString <- showField field + pure $ acc <> fieldString <> " " + ) + (name <> " ") + fields + pure $ fieldsString <> "= " <> nameBase typeName <> ";" + _ -> + case constructorFields constructorInfo of + [] -> pure $ name <> " = " <> nameBase typeName <> ";" + [ConT singleField] -> do + fieldInfo <- reifyDatatype singleField + case datatypeCons fieldInfo of + [subConstructorInfo] -> + case constructorVariant subConstructorInfo of + RecordConstructor fieldNames -> do + let fields = zip fieldNames (constructorFields subConstructorInfo) + fieldsString <- + foldlM + ( \acc field -> do + fieldString <- showField field + pure $ acc <> fieldString <> " " + ) + (name <> " ") + fields + pure $ fieldsString <> " = " <> nameBase typeName <> ";" + _ -> do + typeString <- showResolvedType (ConT singleField) + pure $ name <> " value:" <> typeString <> " = " <> nameBase typeName <> ";" + _ -> error $ "Invalid constructor field: " <> show fieldInfo + _ -> + error $ "Invalid constructor fields: " <> show constructorInfo -generateFunctionDefinition :: HasCallStack => Name -> Q String +generateFunctionDefinition :: (HasCallStack) => Name -> Q String generateFunctionDefinition functionName = do - info <- reify functionName - case info of - VarI _ functionType _ -> do - let name = nameBase functionName - functionTypeString <- showFunctionType functionType - pure $ name <> " " <> functionTypeString <> ";" - _ -> error $ "Invalid function: " <> show functionName + info <- reify functionName + case info of + VarI _ functionType _ -> do + let name = nameBase functionName + functionTypeString <- showFunctionType functionType + pure $ name <> " " <> functionTypeString <> ";" + _ -> error $ "Invalid function: " <> show functionName -makeAPI :: HasCallStack => [Name] -> [Name] -> DecsQ +makeAPI :: (HasCallStack) => [Name] -> [Name] -> DecsQ makeAPI typeNames functionNames = do - generateSchemaDecs <- makeGenerateSchema typeNames functionNames - handleMessageDecs <- makeHandleMessage functionNames - pure $ generateSchemaDecs <> handleMessageDecs + generateSchemaDecs <- makeGenerateSchema typeNames functionNames + handleMessageDecs <- makeHandleMessage functionNames + pure $ generateSchemaDecs <> handleMessageDecs -makeGenerateSchema :: HasCallStack => [Name] -> [Name] -> DecsQ +makeGenerateSchema :: (HasCallStack) => [Name] -> [Name] -> DecsQ makeGenerateSchema typeNames functionNames = do - typeDefinitions <- traverse generateTypeDefinition typeNames - functionDefinitions <- traverse generateFunctionDefinition functionNames - let generateSchemaName = mkName "generateSchema" - let body = NormalB $ DoE Nothing [ NoBindS $ AppE (AppE (VarE 'writeSchema) (ListE (LitE . StringL <$> join typeDefinitions))) (ListE (LitE . StringL <$> functionDefinitions)) ] - let signature = SigD generateSchemaName (AppT (ConT ''IO) (TupleT 0)) - let function = FunD generateSchemaName [Clause [] body []] - pure [signature, function] + typeDefinitions <- traverse generateTypeDefinition typeNames + functionDefinitions <- traverse generateFunctionDefinition functionNames + let generateSchemaName = mkName "generateSchema" + let body = NormalB $ DoE Nothing [NoBindS $ AppE (AppE (VarE 'writeSchema) (ListE (LitE . StringL <$> join typeDefinitions))) (ListE (LitE . StringL <$> functionDefinitions))] + let signature = SigD generateSchemaName (AppT (ConT ''IO) (TupleT 0)) + let function = FunD generateSchemaName [Clause [] body []] + pure [signature, function] -makeHandleMessage :: HasCallStack => [Name] -> DecsQ +makeHandleMessage :: (HasCallStack) => [Name] -> DecsQ makeHandleMessage functionNames = do - let functionName = mkName "handleMessage" - let messageTypeName = mkName "messageType" - let objectName = mkName "object" - cases <- traverse (makeCase objectName) functionNames - let failPattern = - let otherName = mkName "other" - body = AppE (VarE 'throw) (AppE (ConE 'ApiError) (multiAppE (VarE 'mappend) [LitE $ StringL "Unknown function name: ", AppE (VarE 'unpack) (VarE otherName)])) - in Match (VarP otherName) (NormalB body) [] - let body = NormalB $ CaseE (VarE messageTypeName) (cases <> [failPattern]) - let signature = SigD functionName $ AppT (AppT ArrowT (ConT ''Text)) (AppT (AppT ArrowT (AppT (ConT ''KeyMap) (ConT ''A.Value))) (AppT (ConT $ mkName "Ensemble") (AppT (ConT ''KeyMap) (ConT ''A.Value)))) - let function = FunD functionName [Clause [VarP messageTypeName, VarP objectName] body []] - pure [signature, function] + let functionName = mkName "handleMessage" + let messageTypeName = mkName "messageType" + let objectName = mkName "object" + cases <- traverse (makeCase objectName) functionNames + let failPattern = + let otherName = mkName "other" + body = AppE (VarE 'throw) (AppE (ConE 'ApiError) (multiAppE (VarE 'mappend) [LitE $ StringL "Unknown function name: ", AppE (VarE 'unpack) (VarE otherName)])) + in Match (VarP otherName) (NormalB body) [] + let body = NormalB $ CaseE (VarE messageTypeName) (cases <> [failPattern]) + let signature = SigD functionName $ AppT (AppT ArrowT (ConT ''Text)) (AppT (AppT ArrowT (AppT (ConT ''KeyMap) (ConT ''A.Value))) (AppT (ConT $ mkName "Ensemble") (AppT (ConT ''KeyMap) (ConT ''A.Value)))) + let function = FunD functionName [Clause [VarP messageTypeName, VarP objectName] body []] + pure [signature, function] getArgumentInfo :: Type -> (String, Type) getArgumentInfo = \case - AppT (AppT (ConT _) (LitT (StrTyLit argumentName))) argumentType -> (argumentName, argumentType) - other -> error $ "Invalid argument type: " <> show other + AppT (AppT (ConT _) (LitT (StrTyLit argumentName))) argumentType -> (argumentName, argumentType) + other -> error $ "Invalid argument type: " <> show other -makeCase :: HasCallStack => Name -> Name -> Q Match +makeCase :: (HasCallStack) => Name -> Name -> Q Match makeCase objectName functionName = do - info <- reify functionName - case info of - VarI _ functionType _ -> do - let argumentInfos = getArgumentInfo <$> init (getArgumentTypes functionType) - makeLookupStatement = \(argumentName, _argumentType) -> do - let lookupExpression = multiAppE (VarE 'lookupField) [LitE (StringL argumentName), VarE objectName] - pure $ BindS (VarP $ mkName argumentName) lookupExpression - lookupStatements <- traverse makeLookupStatement argumentInfos - let resultName = mkName "result" - let callStatement = BindS (VarP resultName) (multiAppE (VarE functionName) $ AppE (ConE 'Argument) . VarE . mkName <$> (fst <$> argumentInfos)) - returnStatement = NoBindS $ AppE (VarE 'pure) (AppE (VarE 'toTaggedJSON) (VarE resultName)) - statements = lookupStatements <> [callStatement, returnStatement] - body = NormalB $ DoE Nothing statements - pure $ Match (LitP $ StringL $ nameBase functionName) body [] - _ -> error $ "Invalid function: " <> show functionName + info <- reify functionName + case info of + VarI _ functionType _ -> do + let argumentInfos = getArgumentInfo <$> init (getArgumentTypes functionType) + makeLookupStatement = \(argumentName, _argumentType) -> do + let lookupExpression = multiAppE (VarE 'lookupField) [LitE (StringL argumentName), VarE objectName] + pure $ BindS (VarP $ mkName argumentName) lookupExpression + lookupStatements <- traverse makeLookupStatement argumentInfos + let resultName = mkName "result" + let callStatement = BindS (VarP resultName) (multiAppE (VarE functionName) $ AppE (ConE 'Argument) . VarE . mkName <$> (fst <$> argumentInfos)) + returnStatement = NoBindS $ AppE (VarE 'pure) (AppE (VarE 'toTaggedJSON) (VarE resultName)) + statements = lookupStatements <> [callStatement, returnStatement] + body = NormalB $ DoE Nothing statements + pure $ Match (LitP $ StringL $ nameBase functionName) body [] + _ -> error $ "Invalid function: " <> show functionName lookupField :: (A.FromJSON a, Monad m) => A.Key -> KeyMap A.Value -> m a -lookupField key object = - case KeyMap.lookup key object of - Just value -> - case A.fromJSON value of - A.Success a -> pure a - A.Error parseError -> throw $ ApiError $ "Parse error on '" <> show key <> "': " <> parseError - Nothing -> - throw $ ApiError $ "Missing argument: " <> show key +lookupField key object = + case KeyMap.lookup key object of + Just value -> + case A.fromJSON value of + A.Success a -> pure a + A.Error parseError -> throw $ ApiError $ "Parse error on '" <> show key <> "': " <> parseError + Nothing -> + throw $ ApiError $ "Missing argument: " <> show key toSubclassName :: Name -> String toSubclassName = uncapitalise . filter (/= '_') . nameBase diff --git a/src/Ensemble/Schema/TaggedJSON.hs b/src/Ensemble/Schema/TaggedJSON.hs index cee66da..477daac 100644 --- a/src/Ensemble/Schema/TaggedJSON.hs +++ b/src/Ensemble/Schema/TaggedJSON.hs @@ -1,5 +1,5 @@ -{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE OverloadedStrings #-} module Ensemble.Schema.TaggedJSON where @@ -9,33 +9,33 @@ import qualified Data.Aeson.KeyMap as KeyMap import Data.Text (Text, pack) class HasTypeTag a where - typeTag :: a -> String + typeTag :: a -> String instance HasTypeTag Text where - typeTag _ = "String" + typeTag _ = "String" instance HasTypeTag Double where - typeTag _ = "Double" + typeTag _ = "Double" instance HasTypeTag (Maybe Double) where - typeTag _ = "Double" + typeTag _ = "Double" -instance HasTypeTag a => HasTypeTag [a] where - typeTag (first:_) = "vector<" <> typeTag first <> ">" - typeTag [] = "vector" +instance (HasTypeTag a) => HasTypeTag [a] where + typeTag (first : _) = "vector<" <> typeTag first <> ">" + typeTag [] = "vector" toTaggedJSON :: (HasTypeTag a, A.ToJSON a) => a -> KeyMap A.Value toTaggedJSON object = - case A.toJSON object of - A.Object jsonObject -> KeyMap.insert "@type" typeNameValue jsonObject - other -> KeyMap.fromList [("@type", typeNameValue), ("value", other)] - where - typeNameValue = A.String $ pack (typeTag object) + case A.toJSON object of + A.Object jsonObject -> KeyMap.insert "@type" typeNameValue jsonObject + other -> KeyMap.fromList [("@type", typeNameValue), ("value", other)] + where + typeNameValue = A.String $ pack (typeTag object) -toTaggedCustomJSON :: A.ToJSON a => String -> a -> A.Value +toTaggedCustomJSON :: (A.ToJSON a) => String -> a -> A.Value toTaggedCustomJSON typeTag' object = - A.Object $ case A.toJSON object of - A.Object jsonObject -> KeyMap.insert "@type" typeNameValue jsonObject - other -> KeyMap.fromList [("@type", typeNameValue), ("value", other)] - where - typeNameValue = A.String $ pack typeTag' + A.Object $ case A.toJSON object of + A.Object jsonObject -> KeyMap.insert "@type" typeNameValue jsonObject + other -> KeyMap.fromList [("@type", typeNameValue), ("value", other)] + where + typeNameValue = A.String $ pack typeTag' diff --git a/src/Ensemble/Sequencer.hs b/src/Ensemble/Sequencer.hs index 1e801fe..6975bd0 100644 --- a/src/Ensemble/Sequencer.hs +++ b/src/Ensemble/Sequencer.hs @@ -1,10 +1,10 @@ -{-# LANGUAGE BangPatterns #-} {-# LANGUAGE MonoLocalBinds #-} + module Ensemble.Sequencer where import Control.Concurrent -import Control.Exception import Control.DeepSeq +import Control.Exception import Control.Monad.Reader import Data.IORef import Data.List (sortBy) @@ -14,86 +14,88 @@ import Ensemble.Event import Ensemble.Tick data Sequencer = Sequencer - { sequencer_eventQueue :: IORef [(Tick, SequencerEvent)] - } + { sequencer_eventQueue :: IORef [(Tick, SequencerEvent)] + } createSequencer :: IO Sequencer createSequencer = do - eventQueue <- newIORef mempty - pure $ Sequencer { sequencer_eventQueue = eventQueue } - + eventQueue <- newIORef mempty + pure $ Sequencer {sequencer_eventQueue = eventQueue} + playSequenceOffline :: Sequencer -> Engine -> Tick -> Maybe Tick -> Bool -> IO () playSequenceOffline sequencer engine startTick maybeEndTick loop = do - endTick <- case maybeEndTick of - Just endTick -> pure endTick - Nothing -> liftIO $ getEndTick sequencer - audioOutput <- liftIO $ renderSequence sequencer engine startTick endTick - evaluatedAudioOutput <- liftIO $ evaluate $ force audioOutput - playAudio engine startTick loop evaluatedAudioOutput + endTick <- case maybeEndTick of + Just endTick -> pure endTick + Nothing -> liftIO $ getEndTick sequencer + audioOutput <- liftIO $ renderSequence sequencer engine startTick endTick + evaluatedAudioOutput <- liftIO $ evaluate $ force audioOutput + playAudio engine startTick loop evaluatedAudioOutput playSequenceRealtime :: Sequencer -> Engine -> Tick -> Maybe Tick -> Bool -> IO () playSequenceRealtime sequencer engine startTick maybeEndTick loop = do - endTick <- case maybeEndTick of - Just endTick -> pure endTick - Nothing -> liftIO $ getEndTick sequencer - events <- liftIO $ getEventsBetween sequencer startTick endTick - liftIO $ writeIORef (engine_steadyTime engine) 0 - let sortedEvents = sortBy (\(tickA, _) (tickB, _) -> compare tickA tickB) events - runSequence endTick sortedEvents - liftIO $ writeIORef (engine_steadyTime engine) (-1) - where - runSequence endTick [] = do - currentTick <- liftIO $ getCurrentTick engine - when (currentTick < endTick) $ - liftIO $ threadDelay $ (tick_value endTick - tick_value currentTick) * 1000 - when loop $ - playSequenceRealtime sequencer engine startTick maybeEndTick loop - runSequence endTick events = do - currentTick <- liftIO $ getCurrentTick engine - let activeEvents = takeWhile (\(tick, _) -> tick <= currentTick) events - modifyIORef' (engine_eventBuffer engine) (<> fmap snd activeEvents) - threadDelay 10 - runSequence endTick $ drop (length activeEvents) events + endTick <- case maybeEndTick of + Just endTick -> pure endTick + Nothing -> liftIO $ getEndTick sequencer + events <- liftIO $ getEventsBetween sequencer startTick endTick + liftIO $ writeIORef (engine_steadyTime engine) 0 + let sortedEvents = sortBy (\(tickA, _) (tickB, _) -> compare tickA tickB) events + runSequence endTick sortedEvents + liftIO $ writeIORef (engine_steadyTime engine) (-1) + where + runSequence endTick [] = do + currentTick <- liftIO $ getCurrentTick engine + when (currentTick < endTick) $ + liftIO $ + threadDelay $ + (tick_value endTick - tick_value currentTick) * 1000 + when loop $ + playSequenceRealtime sequencer engine startTick maybeEndTick loop + runSequence endTick events = do + currentTick <- liftIO $ getCurrentTick engine + let activeEvents = takeWhile (\(tick, _) -> tick <= currentTick) events + modifyIORef' (engine_eventBuffer engine) (<> fmap snd activeEvents) + threadDelay 10 + runSequence endTick $ drop (length activeEvents) events getEndTick :: Sequencer -> IO Tick getEndTick sequencer = do - eventQueue <- readIORef $ sequencer_eventQueue sequencer - pure $ maximum $ fst <$> eventQueue + eventQueue <- readIORef $ sequencer_eventQueue sequencer + pure $ maximum $ fst <$> eventQueue sendAt :: Sequencer -> Tick -> SequencerEvent -> IO () sendAt sequencer time event = - modifyIORef' (sequencer_eventQueue sequencer) $ (<>) [(time, event)] + modifyIORef' (sequencer_eventQueue sequencer) $ (<>) [(time, event)] type EventCallback = Tick -> SequencerEvent -> IO () renderSequence :: Sequencer -> Engine -> Tick -> Tick -> IO AudioOutput renderSequence sequencer engine startTick endTick = do - events <- getEventsBetween sequencer startTick endTick - renderEvents $ groupEvents startTick endTick events - where - renderEvents = \case - (Tick currentTick,events):next@(Tick nextTick,_):rest -> do - let frameCount = floor $ fromIntegral (nextTick - currentTick) / 1000 * engine_sampleRate engine - sendEvents engine events - chunk <- receiveOutputs engine frameCount - remaining <- renderEvents (next:rest) - pure $ chunk <> remaining - [(_lastTick,events)] -> do - let frameCount = floor $ engine_sampleRate engine / 1000 - sendEvents engine events - receiveOutputs engine frameCount - [] -> pure $ AudioOutput [] [] + events <- getEventsBetween sequencer startTick endTick + renderEvents $ groupEvents startTick endTick events + where + renderEvents = \case + (Tick currentTick, events) : next@(Tick nextTick, _) : rest -> do + let frameCount = floor $ fromIntegral (nextTick - currentTick) / 1000 * engine_sampleRate engine + sendEvents engine events + chunk <- receiveOutputs engine frameCount + remaining <- renderEvents (next : rest) + pure $ chunk <> remaining + [(_lastTick, events)] -> do + let frameCount = floor $ engine_sampleRate engine / 1000 + sendEvents engine events + receiveOutputs engine frameCount + [] -> pure $ AudioOutput [] [] getEvents :: Sequencer -> IO [SequencerEvent] getEvents sequencer = - fmap snd <$> readIORef (sequencer_eventQueue sequencer) - + fmap snd <$> readIORef (sequencer_eventQueue sequencer) + getEventsBetween :: Sequencer -> Tick -> Tick -> IO [(Tick, SequencerEvent)] getEventsBetween sequencer startTick endTick = do - events <- readIORef (sequencer_eventQueue sequencer) - pure $ filter (\(tick, _) -> tick >= startTick && tick <= endTick) events + events <- readIORef (sequencer_eventQueue sequencer) + pure $ filter (\(tick, _) -> tick >= startTick && tick <= endTick) events groupEvents :: Tick -> Tick -> [(Tick, SequencerEvent)] -> [(Tick, [SequencerEvent])] groupEvents startTick endTick eventList = - let tickIntervals = (\tick -> (tick, [])) <$> enumFromThenTo startTick (startTick + 100) (endTick + 100) - in Map.toAscList $ Map.fromListWith (<>) $ tickIntervals <> ((\(a, b) -> (a, [b])) <$> eventList) + let tickIntervals = (\tick -> (tick, [])) <$> enumFromThenTo startTick (startTick + 100) (endTick + 100) + in Map.toAscList $ Map.fromListWith (<>) $ tickIntervals <> ((\(a, b) -> (a, [b])) <$> eventList) diff --git a/src/Ensemble/Tick.hs b/src/Ensemble/Tick.hs index 698646e..24c99e2 100644 --- a/src/Ensemble/Tick.hs +++ b/src/Ensemble/Tick.hs @@ -7,17 +7,17 @@ module Ensemble.Tick where import Data.Int import Ensemble.Schema.TH -newtype Tick = Tick { tick_value :: Int } - deriving newtype (Eq, Ord, Show, Enum, Num, Real, Integral) +newtype Tick = Tick {tick_value :: Int} + deriving newtype (Eq, Ord, Show, Enum, Num, Real, Integral) steadyTimeToTick :: Double -> Int64 -> Tick -steadyTimeToTick sampleRate steadyTime = - Tick $ fromIntegral steadyTime * 1000 `div` floor sampleRate +steadyTimeToTick sampleRate steadyTime = + Tick $ fromIntegral steadyTime * 1000 `div` floor sampleRate tickToSteadyTime :: Double -> Tick -> Int64 tickToSteadyTime sampleRate (Tick tick) = - (fromIntegral tick `div` 1000) * floor sampleRate + (fromIntegral tick `div` 1000) * floor sampleRate deriveJSONs - [ ''Tick - ] \ No newline at end of file + [ ''Tick + ] diff --git a/src/Ensemble/Util.hs b/src/Ensemble/Util.hs index 45cc5cf..7776e34 100644 --- a/src/Ensemble/Util.hs +++ b/src/Ensemble/Util.hs @@ -1,20 +1,21 @@ module Ensemble.Util where -import Prelude hiding (break) import Data.Char +import Prelude hiding (break) -break :: (a -> Bool) -> [a] -> ([a],[a]) -break _ xs@[] = (xs, xs) -break p xs@(x:xs') - | p x = ([],xs) - | otherwise = let (ys,zs) = break p xs' in (x:ys,zs) +break :: (a -> Bool) -> [a] -> ([a], [a]) +break _ xs@[] = (xs, xs) +break p xs@(x : xs') + | p x = ([], xs) + | otherwise = let (ys, zs) = break p xs' in (x : ys, zs) split :: Char -> String -> [String] split c s = case rest of - [] -> [chunk] - _:rest' -> chunk : split c rest' - where (chunk, rest) = break (==c) s + [] -> [chunk] + _ : rest' -> chunk : split c rest' + where + (chunk, rest) = break (== c) s uncapitalise :: String -> String uncapitalise [] = [] -uncapitalise (c:cs) = toLower c : cs +uncapitalise (c : cs) = toLower c : cs diff --git a/src/Ensemble/Window.hs b/src/Ensemble/Window.hs index e27627c..93dc6d0 100644 --- a/src/Ensemble/Window.hs +++ b/src/Ensemble/Window.hs @@ -1,10 +1,11 @@ {-# LANGUAGE CPP #-} -module Ensemble.Window - ( createParentWindow - , showWindow - , messagePump - ) where +module Ensemble.Window + ( createParentWindow, + showWindow, + messagePump, + ) +where #ifdef WINDOWS import Ensemble.Window.Windows diff --git a/src/Ensemble/Window/Windows.hs b/src/Ensemble/Window/Windows.hs index 5a162c2..5e2c8dc 100644 --- a/src/Ensemble/Window/Windows.hs +++ b/src/Ensemble/Window/Windows.hs @@ -9,60 +9,62 @@ import Data.Bits ((.|.)) import Foreign.Ptr import qualified Graphics.Win32 as Win32 import System.Win32.DLL (getModuleHandle) -import System.Win32.Types (LONG) import qualified System.Win32.Info.Computer as Win32 +import System.Win32.Types (LONG) showWindow :: Win32.HWND -> IO () -showWindow window = do - _ <- Win32.showWindow window Win32.sW_SHOWNORMAL - Win32.updateWindow window +showWindow window = do + _ <- Win32.showWindow window Win32.sW_SHOWNORMAL + Win32.updateWindow window createParentWindow :: Maybe (Ptr ()) -> String -> Int -> Int -> IO (Ptr ()) createParentWindow _maybeParent name clientWidth clientHeight = do - let className = Win32.mkClassName $ "Plugin GUI - " <> name - mainInstance <- getModuleHandle Nothing - _ <- Win32.registerClass - ( Win32.cS_VREDRAW + Win32.cS_HREDRAW - , mainInstance - , Nothing - , Nothing - , Nothing - , Nothing - , className - ) - let windowClosure = \window message wParam lParam -> Win32.defWindowProc (Just window) message wParam lParam - let windowStyle = Win32.wS_OVERLAPPED .|. Win32.wS_SYSMENU - sizeXFrame <- Win32.getSystemMetrics 32 -- SM_CXSIZEFRAME - let width = clientWidth + sizeXFrame - sizeYFrame <- Win32.getSystemMetrics 33 -- SM_CYSIZEFRAME - let height = clientHeight + sizeYFrame - Win32.createWindowEx - Win32.wS_EX_TOPMOST -- extended style - className -- window class name - name -- window name - windowStyle -- style - Nothing -- left position - Nothing -- top position - (Just width) -- width - (Just height) -- height - Nothing -- parent - Nothing -- menu - mainInstance -- application instance - windowClosure -- WindowClosure (?) + let className = Win32.mkClassName $ "Plugin GUI - " <> name + mainInstance <- getModuleHandle Nothing + _ <- + Win32.registerClass + ( Win32.cS_VREDRAW + Win32.cS_HREDRAW, + mainInstance, + Nothing, + Nothing, + Nothing, + Nothing, + className + ) + let windowClosure = \window message wParam lParam -> Win32.defWindowProc (Just window) message wParam lParam + let windowStyle = Win32.wS_OVERLAPPED .|. Win32.wS_SYSMENU + sizeXFrame <- Win32.getSystemMetrics 32 -- SM_CXSIZEFRAME + let width = clientWidth + sizeXFrame + sizeYFrame <- Win32.getSystemMetrics 33 -- SM_CYSIZEFRAME + let height = clientHeight + sizeYFrame + Win32.createWindowEx + Win32.wS_EX_TOPMOST -- extended style + className -- window class name + name -- window name + windowStyle -- style + Nothing -- left position + Nothing -- top position + (Just width) -- width + (Just height) -- height + Nothing -- parent + Nothing -- menu + mainInstance -- application instance + windowClosure -- WindowClosure (?) messagePump :: IO () messagePump = Win32.allocaMessage $ \msg -> - let pump = do - result :: Either SomeException LONG <- try $ Win32.c_PeekMessage msg (Win32.maybePtr Nothing) 0 0 1 - case result of - Right code -> if - | code == 0 -> threadDelay $ 1000 * 10 - | code > 0 -> do - () <$ Win32.translateMessage msg - () <$ Win32.dispatchMessage msg - | otherwise -> - print $ "Invalid peek message response: " <> show code - Left exception -> - print exception - pump - in pump + let pump = do + result :: Either SomeException LONG <- try $ Win32.c_PeekMessage msg (Win32.maybePtr Nothing) 0 0 1 + case result of + Right code -> + if + | code == 0 -> threadDelay $ 1000 * 10 + | code > 0 -> do + () <$ Win32.translateMessage msg + () <$ Win32.dispatchMessage msg + | otherwise -> + print $ "Invalid peek message response: " <> show code + Left exception -> + print exception + pump + in pump diff --git a/test/Main.hs b/test/Main.hs index 3916c2d..991a351 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -1,93 +1,106 @@ {-# LANGUAGE OverloadedStrings #-} -module Main (main) where -import Prelude hiding (show) +module Main (main) where import Clap.Interface.Events import Clap.Interface.Extension.Params import Clap.Interface.Id import Control.Concurrent import Control.Monad.Reader +import Data.Foldable (for_) import Ensemble.API import Ensemble.Config -import Ensemble.Event import Ensemble.Env +import Ensemble.Event import Ensemble.Schema.TH import Ensemble.Tick -import Test.Hspec import System.Environment import System.IO -import Data.Foldable (for_) +import Test.Hspec +import Prelude hiding (show) main :: IO () main = do - setEnv "FLUIDSYNTH_CLAP_DEBUG" "True" - hSetBuffering stdout NoBuffering - env <- createEnv defaultConfig - result <- runEnsemble env $ do - - liftIO $ putStrLn "startEngine" - Ok <- startEngine - - paths <- getPluginLocations - _ <- scanForPlugins (Argument paths) - - liftIO $ putStrLn "createPluginNode" - nodeId <- createPluginNode (Argument "./plugins/FluidSynth.clap") (Argument 0) - - liftIO $ putStrLn "getPluginParameters" - parameters <- getPluginParameters (Argument nodeId) - - liftIO $ putStrLn "getPluginParameterValue" - for_ parameters $ \parameter -> - getPluginParameterValue (Argument nodeId) (Argument $ clapId_id $ parameterInfo_id parameter) - - playNote nodeId 70 (Tick 1) 200 - playNote nodeId 72 (Tick 501) 200 - playNote nodeId 74 (Tick 1000) 200 - playNote nodeId 75 (Tick 1501) 200 - playNote nodeId 77 (Tick 2001) 200 - playNote nodeId 79 (Tick 2501) 200 - - liftIO $ putStrLn "playSequence" - Ok <- playSequence (Argument $ Tick 1) (Argument Nothing) (Argument False) - - liftIO $ threadDelay (5 * 1000 * 1000) - - liftIO $ putStrLn "clearSequence" - Ok <- clearSequence - - liftIO $ putStrLn "stopEngine" - Ok <- stopEngine - pure () - - hspec $ describe "result" $ - it "should not have an error" $ - result `shouldSatisfy` (== ()) - - where - playNote nodeId key startTick duration = do - Ok <- scheduleEvent (Argument startTick) (Argument $ SequencerEvent - { sequencerEvent_nodeId = nodeId - , sequencerEvent_eventConfig = Nothing - , sequencerEvent_event = Event_NoteOn $ NoteEvent - { noteEvent_noteId = 0 - , noteEvent_portIndex = 0 - , noteEvent_channel = 0 - , noteEvent_key = key - , noteEvent_velocity = 0.5 - } - }) - - Ok <- scheduleEvent (Argument $ startTick + duration) (Argument $ SequencerEvent - { sequencerEvent_nodeId = nodeId - , sequencerEvent_eventConfig = Nothing - , sequencerEvent_event = Event_NoteOff $ NoteEvent - { noteEvent_noteId = 0 - , noteEvent_portIndex = 0 - , noteEvent_channel = 0 - , noteEvent_key = key - , noteEvent_velocity = 0.5 - } - }) - pure () + setEnv "FLUIDSYNTH_CLAP_DEBUG" "True" + hSetBuffering stdout NoBuffering + env <- createEnv defaultConfig + result <- runEnsemble env $ do + liftIO $ putStrLn "startEngine" + Ok <- startEngine + + paths <- getPluginLocations + _ <- scanForPlugins (Argument paths) + + liftIO $ putStrLn "createPluginNode" + nodeId <- createPluginNode (Argument "./plugins/FluidSynth.clap") (Argument 0) + + liftIO $ putStrLn "getPluginParameters" + parameters <- getPluginParameters (Argument nodeId) + + liftIO $ putStrLn "getPluginParameterValue" + for_ parameters $ \parameter -> + getPluginParameterValue (Argument nodeId) (Argument $ clapId_id $ parameterInfo_id parameter) + + playNote nodeId 70 (Tick 1) 200 + playNote nodeId 72 (Tick 501) 200 + playNote nodeId 74 (Tick 1000) 200 + playNote nodeId 75 (Tick 1501) 200 + playNote nodeId 77 (Tick 2001) 200 + playNote nodeId 79 (Tick 2501) 200 + + liftIO $ putStrLn "playSequence" + Ok <- playSequence (Argument $ Tick 1) (Argument Nothing) (Argument False) + + liftIO $ threadDelay (5 * 1000 * 1000) + + liftIO $ putStrLn "clearSequence" + Ok <- clearSequence + + liftIO $ putStrLn "stopEngine" + Ok <- stopEngine + pure () + + hspec $ + describe "result" $ + it "should not have an error" $ + result `shouldSatisfy` (== ()) + where + playNote nodeId key startTick duration = do + Ok <- + scheduleEvent + (Argument startTick) + ( Argument $ + SequencerEvent + { sequencerEvent_nodeId = nodeId, + sequencerEvent_eventConfig = Nothing, + sequencerEvent_event = + Event_NoteOn $ + NoteEvent + { noteEvent_noteId = 0, + noteEvent_portIndex = 0, + noteEvent_channel = 0, + noteEvent_key = key, + noteEvent_velocity = 0.5 + } + } + ) + + Ok <- + scheduleEvent + (Argument $ startTick + duration) + ( Argument $ + SequencerEvent + { sequencerEvent_nodeId = nodeId, + sequencerEvent_eventConfig = Nothing, + sequencerEvent_event = + Event_NoteOff $ + NoteEvent + { noteEvent_noteId = 0, + noteEvent_portIndex = 0, + noteEvent_channel = 0, + noteEvent_key = key, + noteEvent_velocity = 0.5 + } + } + ) + pure ()