diff --git a/minipat-dirt/minipat-dirt.cabal b/minipat-dirt/minipat-dirt.cabal new file mode 100644 index 0000000..a5d05cc --- /dev/null +++ b/minipat-dirt/minipat-dirt.cabal @@ -0,0 +1,76 @@ +cabal-version: 1.12 + +-- This file has been generated from package.yaml by hpack version 0.36.0. +-- +-- see: https://github.com/sol/hpack + +name: minipat-dirt +version: 0.1.0 +synopsis: Supercollider integration for minipat +description: Please see the README on GitHub at +homepage: https://github.com/ejconlon/minipat#readme +bug-reports: https://github.com/ejconlon/minipat/issues +author: Eric Conlon +maintainer: ejconlon@gmail.com +copyright: (c) 2024 Eric Conlon +license: BSD3 +build-type: Simple +tested-with: + GHC == 9.6.4 + +source-repository head + type: git + location: https://github.com/ejconlon/minipat + +library + exposed-modules: + Minipat.Dirt.Dirt + Minipat.Dirt.Spy + other-modules: + Paths_minipat_dirt + hs-source-dirs: + src + default-extensions: + BangPatterns + ConstraintKinds + DataKinds + DeriveFunctor + DeriveFoldable + DeriveGeneric + DeriveTraversable + DerivingStrategies + DerivingVia + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + GeneralizedNewtypeDeriving + ImportQualifiedPost + LambdaCase + KindSignatures + MultiParamTypeClasses + MultiWayIf + PatternSynonyms + Rank2Types + ScopedTypeVariables + StandaloneDeriving + StandaloneKindSignatures + TupleSections + TypeApplications + TypeOperators + TypeFamilies + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints -fno-warn-unused-top-binds + build-depends: + base >=4.12 && <5 + , containers ==0.6.* + , dahdit ==0.5.* + , dahdit-midi ==0.5.* + , dahdit-network ==0.5.* + , minipat ==0.1.* + , mtl ==2.3.* + , nanotime ==0.1.* + , network ==3.1.* + , optics ==0.4.* + , resourcet ==1.3.* + , text ==2.0.* + default-language: GHC2021 diff --git a/minipat-dirt/package.yaml b/minipat-dirt/package.yaml new file mode 100644 index 0000000..1ae7c4b --- /dev/null +++ b/minipat-dirt/package.yaml @@ -0,0 +1,32 @@ +name: minipat-dirt +version: 0.1.0 + +github: ejconlon/minipat +license: BSD3 +author: Eric Conlon +maintainer: ejconlon@gmail.com +copyright: (c) 2024 Eric Conlon +synopsis: Supercollider integration for minipat +description: Please see the README on GitHub at +tested-with: GHC == 9.6.4 + +defaults: + local: ../defaults.yaml + +dependencies: +- base >= 4.12 && < 5 +- containers >= 0.6 && < 0.7 +- dahdit >= 0.5 && < 0.6 +- dahdit-midi >= 0.5 && < 0.6 +- dahdit-network >= 0.5 && < 0.6 +- minipat >= 0.1 && < 0.2 +- mtl >= 2.3 && < 2.4 +- nanotime >= 0.1 && < 0.2 +- network >= 3.1 && < 3.2 +- optics >= 0.4 && < 0.5 +- resourcet >= 1.3 && < 1.4 +- text >= 2.0 && < 2.1 + +library: + source-dirs: src + diff --git a/minipat-dirt/src/Minipat/Dirt/Dirt.hs b/minipat-dirt/src/Minipat/Dirt/Dirt.hs new file mode 100644 index 0000000..de9e727 --- /dev/null +++ b/minipat-dirt/src/Minipat/Dirt/Dirt.hs @@ -0,0 +1,262 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} + +module Minipat.Dirt.Dirt where + +import Control.Monad (unless) +import Control.Monad.Except (Except, MonadError (..), runExcept) +import Control.Monad.State.Strict (MonadState (..), StateT, evalStateT) +import Dahdit (ShortByteString) +import Data.Int (Int32, Int64) +import Data.Map.Strict (Map) +import Data.Map.Strict qualified as Map +import Data.Sequence (Seq (..)) +import Data.Sequence qualified as Seq +import Data.Set (Set) +import Data.Set qualified as Set +import Data.Text (Text) +import GHC.Generics (Generic) +import Dahdit.Midi.Osc (Datum (..), DatumType (..), Msg (..), PortMsg, datumType) +import Dahdit.Midi.OscAddr (RawAddrPat) +import Nanotime (NtpTime) +import Optics (AffineTraversal', Prism', gafield, gconstructor, ix, preview, prism', review, set, (%)) + +-- TODO move into source lib +deriving stock instance Generic Datum + +data ArgsErr + = ArgsErrEmpty + | ArgsErrMismatch !Datum !Datum + | ArgsErrTyMismatch !DatumType !DatumType + | ArgsErrLeftover !Int + | ArgsErrInvalidField !Text !Datum + | ArgsErrMissingFields !(Set Text) + deriving stock (Eq, Ord, Show) + +type P = StateT (Seq Datum) (Except ArgsErr) + +rethrow :: Either ArgsErr a -> P a +rethrow = either throwError pure + +parseArgs :: P a -> Seq Datum -> Either ArgsErr a +parseArgs m s = runExcept (evalStateT m s) + +getArgRaw :: (Datum -> Either ArgsErr a) -> P a +getArgRaw f = do + args <- get + case args of + Empty -> throwError ArgsErrEmpty + hd :<| tl -> do + a <- rethrow (f hd) + put tl + pure a + +getArg :: P Datum +getArg = getArgRaw Right + +getArgExact :: Datum -> P () +getArgExact wantDat = getArgRaw $ \actualDat -> + if actualDat == wantDat + then Right () + else Left (ArgsErrMismatch actualDat wantDat) + +data DatumPrism a = DatumPrism + { dpType :: !DatumType + , dpPrism :: !(Prism' Datum a) + } + +asInt32 :: DatumPrism Int32 +asInt32 = DatumPrism DatumTypeInt32 (gconstructor @"DatumInt32") + +asInt64 :: DatumPrism Int64 +asInt64 = DatumPrism DatumTypeInt64 (gconstructor @"DatumInt64") + +asFloat :: DatumPrism Float +asFloat = DatumPrism DatumTypeFloat (gconstructor @"DatumFloat") + +asDouble :: DatumPrism Double +asDouble = DatumPrism DatumTypeDouble (gconstructor @"DatumDouble") + +asString :: DatumPrism Text +asString = DatumPrism DatumTypeString (gconstructor @"DatumString") + +asBlob :: DatumPrism ShortByteString +asBlob = DatumPrism DatumTypeBlob (gconstructor @"DatumBlob") + +asTime :: DatumPrism NtpTime +asTime = DatumPrism DatumTypeTime (gconstructor @"DatumTime") + +asMidi :: DatumPrism PortMsg +asMidi = DatumPrism DatumTypeMidi (gconstructor @"DatumMidi") + +viewDatum :: DatumPrism a -> Datum -> Either DatumType a +viewDatum (DatumPrism _ pr) dat = + case preview pr dat of + Just val -> Right val + Nothing -> Left (datumType dat) + +previewDatum :: DatumPrism a -> Datum -> Maybe a +previewDatum dr = either (const Nothing) Just . viewDatum dr + +reviewDatum :: DatumPrism a -> a -> Datum +reviewDatum (DatumPrism _ pr) = review pr + +getArgTyped :: DatumPrism a -> P a +getArgTyped dr = getArgRaw $ \actualDat -> + case viewDatum dr actualDat of + Left actualTy -> Left (ArgsErrTyMismatch actualTy (dpType dr)) + Right a -> Right a + +foldArgs :: s -> (s -> P s) -> P s +foldArgs start f = go start + where + go !val = do + args <- get + case args of + Empty -> pure val + _ -> do + val' <- f val + args' <- get + unless + (Seq.length args' < Seq.length args) + (error "Not consuming args") + go val' + +forArgs :: P a -> P (Seq a) +forArgs act = foldArgs Empty (\s -> fmap (s :|>) act) + +endArgs :: P () +endArgs = do + args <- get + case args of + Empty -> pure () + _ -> throwError (ArgsErrLeftover (Seq.length args)) + +data DatumField b where + DatumField :: Prism' Datum a -> AffineTraversal' b a -> DatumField b + +previewDatumField :: DatumField b -> b -> Maybe Datum +previewDatumField (DatumField x y) b = fmap (review x) (preview y b) + +setDatumField :: DatumField b -> b -> Datum -> Maybe b +setDatumField (DatumField pri len) b v = fmap (flip (set len) b) (preview pri v) + +setFirstDatumField :: [DatumField b] -> b -> Datum -> Maybe b +setFirstDatumField ss0 b v = foldr go Nothing ss0 + where + go field = maybe id (const . Just) (setDatumField field b v) + +data Struct b = Struct + { structNull :: !b + , structRequired :: !(Set Text) + , structFields :: !(Text -> [DatumField b]) + } + +expectStruct :: Struct b -> P b +expectStruct (Struct nul req fields) = do + (b', ks') <- foldArgs (nul, Set.empty) $ \(b, ks) -> do + k <- getArgTyped asString + v <- getArg + case setFirstDatumField (fields k) b v of + Nothing -> throwError (ArgsErrInvalidField k v) + Just b' -> pure (b', Set.insert k ks) + unless (Set.isSubsetOf ks' req) (throwError (ArgsErrMissingFields (Set.difference req ks'))) + pure b' + +data AddrSerde a = AddrSerde + { addrSerdeTo :: !(a -> RawAddrPat) + , addrSerdeFrom :: !(RawAddrPat -> Maybe a) + } + +exactAddrSerde :: RawAddrPat -> AddrSerde () +exactAddrSerde pat = AddrSerde (const pat) (\pat' -> if pat == pat' then Just () else Nothing) + +data ArgsSerde b = ArgsSerde + { argsSerdeTo :: !(b -> Seq Datum) + , argsSerdeFrom :: !(Seq Datum -> Either ArgsErr b) + } + +mkArgsSerde :: (b -> Seq Datum) -> P b -> ArgsSerde b +mkArgsSerde argsTo argsParser = ArgsSerde argsTo (parseArgs argsParser) + +structArgsSerde :: Struct b -> ArgsSerde b +structArgsSerde struct = ArgsSerde to from + where + to = error "TODO" + from = parseArgs (expectStruct struct) + +data Serde a b = Serde {serdeAddr :: !(AddrSerde a), serdeArgs :: !(ArgsSerde b)} + +serdeTo :: Serde a b -> a -> b -> Msg +serdeTo (Serde (AddrSerde addrTo _) (ArgsSerde argsTo _)) a b = Msg (addrTo a) (argsTo b) + +serdeFrom :: Serde a b -> Msg -> Maybe (a, Either ArgsErr b) +serdeFrom (Serde (AddrSerde _ addrFrom) (ArgsSerde _ argsFrom)) (Msg addr args) = + fmap (,argsFrom args) (addrFrom addr) + +data Handshake = Handshake + deriving stock (Eq, Ord, Show) + +handshakeS :: Serde () Handshake +handshakeS = Serde (exactAddrSerde "/dirt/handshake") (mkArgsSerde argsTo argsParser) + where + argsTo = const Empty + argsParser = do + endArgs + pure Handshake + +data HandshakeReply = HandshakeReply + { hrServerHostname :: !Text + , hrServerPort :: !Int32 + , hrControlBusIndices :: !(Seq Int32) + } + deriving stock (Eq, Ord, Show) + +handshakeReplyS :: Serde () HandshakeReply +handshakeReplyS = Serde (exactAddrSerde "/dirt/handshake/reply") (mkArgsSerde argsTo argsParser) + where + argsTo (HandshakeReply host port idxs) = prefix <> fmap DatumInt32 idxs + where + prefix = + Seq.fromList + [ DatumString "&serverHostname" + , DatumString host + , DatumString "&serverPort" + , DatumInt32 port + , DatumString "&controlBusIndices" + ] + argsParser = do + getArgExact (DatumString "&serverHostname") + host <- getArgTyped asString + getArgExact (DatumString "&serverPort") + port <- getArgTyped asInt32 + getArgExact (DatumString "&controlBusIndices") + idxs <- forArgs (getArgTyped asInt32) + pure (HandshakeReply host port idxs) + +data Play = Play + { playId :: !Text + , playOrbit :: !Int32 + , playCps :: !Float + , playCycle :: !Float + , playDelta :: !Float + , playOther :: !(Map Text Datum) + } + deriving stock (Eq, Ord, Show, Generic) + +playStruct :: Struct Play +playStruct = Struct nul req sets + where + nul = Play "" 0 0 0 0 Map.empty + req = Set.fromList ["_id_", "orbit", "cps", "cycle", "delta"] + sets = \case + "_id_" -> [DatumField (gconstructor @"DatumString") (gafield @"playId")] + "orbit" -> [DatumField (gconstructor @"DatumInt32") (gafield @"playOrbit")] + "cps" -> [DatumField (gconstructor @"DatumFloat") (gafield @"playCps")] + "cycle" -> [DatumField (gconstructor @"DatumFloat") (gafield @"playCycle")] + "delta" -> [DatumField (gconstructor @"DatumFloat") (gafield @"playDelta")] + k -> [DatumField (prism' id Just) (gafield @"playOther" % ix k)] + +playS :: Serde () Play +playS = Serde (exactAddrSerde "/dirt/play") (structArgsSerde playStruct) diff --git a/minipat-dirt/src/Minipat/Dirt/Spy.hs b/minipat-dirt/src/Minipat/Dirt/Spy.hs new file mode 100644 index 0000000..70ab3e5 --- /dev/null +++ b/minipat-dirt/src/Minipat/Dirt/Spy.hs @@ -0,0 +1,51 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Minipat.Dirt.Spy where + +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans.Resource (runResourceT) +import Dahdit.Network (Conn (..), HostPort (..), resolveAddr, runDecoder, runEncoder, udpServerConn) +import Data.Acquire (allocateAcquire) +import Data.Sequence qualified as Seq +import Dahdit.Midi.Osc (Datum (..), Msg (..), Packet (..)) +import Network.Socket qualified as NS + +oscSpyLocal :: Int -> Int -> IO () +oscSpyLocal spyPort destPort = oscSpy (mkHp spyPort) (mkHp destPort) + where + mkHp = HostPort (Just "127.0.0.1") + +oscSpy :: HostPort -> HostPort -> IO () +oscSpy spyHost destHost = runResourceT $ do + destAddr <- liftIO (resolveAddr destHost) + (_, srvConn) <- allocateAcquire (udpServerConn Nothing spyHost) + liftIO (oscSpyLoop destAddr srvConn) + +-- Just testing whether tidal connects to this or not +xformMsg :: Packet -> Packet +xformMsg = \case + PacketMsg (Msg addr ds) + | addr == "/dirt/handshake/reply" -> + let ds' = Seq.update 3 (DatumInt32 57111) ds + in PacketMsg (Msg addr ds') + p -> p + +oscSpyLoop :: NS.SockAddr -> Conn NS.SockAddr -> IO () +oscSpyLoop destAddr (Conn dec enc) = go Nothing + where + go maySrcAddr = do + (recvAddr, res) <- runDecoder dec + case res of + Left err -> print err + Right (msg :: Packet) -> do + let msg' = xformMsg msg + print (recvAddr, msg') + if recvAddr == destAddr + then do + case maySrcAddr of + Just srcAddr -> runEncoder enc srcAddr msg' + Nothing -> pure () + go maySrcAddr + else do + runEncoder enc destAddr msg' + go (Just recvAddr) diff --git a/minipat/minipat.cabal b/minipat/minipat.cabal index 608b25e..7b31f28 100644 --- a/minipat/minipat.cabal +++ b/minipat/minipat.cabal @@ -74,8 +74,8 @@ library , heaps ==0.4.* , looksee ==0.5.* , mtl ==2.3.* + , nanotime ==0.1.* , nonempty-containers ==0.3.* - , pretty-show , prettyprinter ==1.7.* , text ==2.0.* , transformers ==0.6.* @@ -127,8 +127,8 @@ test-suite minipat-test , looksee ==0.5.* , minipat , mtl ==2.3.* + , nanotime ==0.1.* , nonempty-containers ==0.3.* - , pretty-show , prettyprinter ==1.7.* , tasty ==1.4.* , tasty-hunit ==0.10.* diff --git a/minipat/package.yaml b/minipat/package.yaml index 1c1899c..1072997 100644 --- a/minipat/package.yaml +++ b/minipat/package.yaml @@ -20,11 +20,11 @@ dependencies: - heaps >= 0.4 && < 0.5 - looksee >= 0.5 && < 0.6 - mtl >= 2.3 && < 2.4 +- nanotime >= 0.1 && < 0.2 - nonempty-containers >= 0.3 && < 0.4 - prettyprinter >= 1.7 && < 1.8 - text >= 2.0 && < 2.1 - transformers >= 0.6 && < 0.7 -- pretty-show library: source-dirs: src diff --git a/minipat/src/Minipat/Rewrite.hs b/minipat/src/Minipat/Rewrite.hs index 49ce10f..71c00ca 100644 --- a/minipat/src/Minipat/Rewrite.hs +++ b/minipat/src/Minipat/Rewrite.hs @@ -10,9 +10,9 @@ module Minipat.Rewrite , rewrite , PatRwM , rewriteM - , PatOv + , PatOvh , overhaul - , PatOvM + , PatOvhM , overhaulM ) where @@ -109,20 +109,20 @@ instance Bitraversable (TapF a) where A.PatMod m -> fmap A.PatMod (bitraverse f g m) A.PatPoly p -> fmap A.PatPoly (traverse g p) -type PatOv b = forall a. PatRw b a (A.UnPat b a) +type PatOvh b = forall a. PatRw b a (A.UnPat b a) -overhaul :: PatOv b -> A.UnPat b a -> A.UnPat b a +overhaul :: PatOvh b -> A.UnPat b a -> A.UnPat b a overhaul f = runIdentity . overhaulM (f . fmap runIdentity) -type PatOvM b m = forall a. PatRwM b a m (A.UnPat b a) +type PatOvhM b m = forall a. PatRwM b a m (A.UnPat b a) -overhaulM :: (Monad m) => PatOvM b m -> A.UnPat b a -> m (A.UnPat b a) -overhaulM f (JotP b0 pf0) = goOvM f (NESeq.singleton b0) pf0 +overhaulM :: (Monad m) => PatOvhM b m -> A.UnPat b a -> m (A.UnPat b a) +overhaulM f (JotP b0 pf0) = goOvhM f (NESeq.singleton b0) pf0 -goOvM :: (Monad m) => PatOvM b m -> NESeq b -> A.PatX b a (A.UnPat b a) -> m (A.UnPat b a) -goOvM f bs pf = do - pf' <- fmap unTapF (bitraverse (fmap A.Pat . pushOvM f bs . A.unPat) (pure . pushOvM f bs) (TapF pf)) +goOvhM :: (Monad m) => PatOvhM b m -> NESeq b -> A.PatX b a (A.UnPat b a) -> m (A.UnPat b a) +goOvhM f bs pf = do + pf' <- fmap unTapF (bitraverse (fmap A.Pat . pushOvhM f bs . A.unPat) (pure . pushOvhM f bs) (TapF pf)) runReaderT (f pf') bs -pushOvM :: (Monad m) => PatOvM b m -> NESeq b -> A.UnPat b a -> m (A.UnPat b a) -pushOvM f bs (JotP b pf) = goOvM f (bs NESeq.|> b) pf +pushOvhM :: (Monad m) => PatOvhM b m -> NESeq b -> A.UnPat b a -> m (A.UnPat b a) +pushOvhM f bs (JotP b pf) = goOvhM f (bs NESeq.|> b) pf diff --git a/minipat/src/Minipat/Time.hs b/minipat/src/Minipat/Time.hs index cf64c10..e8c4dc7 100644 --- a/minipat/src/Minipat/Time.hs +++ b/minipat/src/Minipat/Time.hs @@ -1,5 +1,7 @@ module Minipat.Time where +import Nanotime (TimeDelta, timeDeltaFromFracSecs, timeDeltaToFracSecs) + type Time = Rational timeFloor :: Time -> Integer @@ -71,3 +73,19 @@ spanSplit (Arc s0 e) = then [(si, Span (Arc s e) wh)] else (si, Span (Arc s sc) wh) : go sc in go s0 + +-- | Convert BPM to CPS +bpmToCps :: Rational -> Rational +bpmToCps = (/ 60) + +-- | Convert CPS to BPM +cpsToBpm :: Rational -> Rational +cpsToBpm = (60 *) + +-- | Given CPS convert absolute time diff from start to cycle time +deltaToCycle :: Rational -> TimeDelta -> Time +deltaToCycle cps = (cps *) . timeDeltaToFracSecs + +-- | Given CPS convert cycle time to absolute time diff from start +cycleToDelta :: Rational -> Time -> TimeDelta +cycleToDelta cps = timeDeltaFromFracSecs . (/ cps) diff --git a/stack.yaml b/stack.yaml index cc08e80..ff54547 100644 --- a/stack.yaml +++ b/stack.yaml @@ -5,12 +5,18 @@ ghc-options: packages: - minipat +- minipat-dirt extra-deps: - bowtie-0.2.0 +- dahdit-0.5.1 +- dahdit-network-0.5.1 +- dahdit-midi-0.5.1 +- data-sword-0.2.0.3 - daytripper-0.3.1 - falsify-0.2.0 - looksee-0.5.2 +- nanotime-0.1.0 allow-newer: true allow-newer-deps: diff --git a/stack.yaml.lock b/stack.yaml.lock index 74f8d19..3a9fe81 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -11,6 +11,34 @@ packages: size: 159 original: hackage: bowtie-0.2.0 +- completed: + hackage: dahdit-0.5.1@sha256:f583794914a2c775f3e59fe4ef1d39c5ab827ff27894da32f7ae0de4ea9f15b1,3704 + pantry-tree: + sha256: 9b42c87254506abaa97962be7cdd558388ae0e77bf13678017a97c6feddf773b + size: 1135 + original: + hackage: dahdit-0.5.1 +- completed: + hackage: dahdit-network-0.5.1@sha256:9a5aed2baed0f5122e613f5533416151fe894478a480cfa274a95dbe781324af,3000 + pantry-tree: + sha256: af524794fb60872f93d23d31f305ddc6835e91d1952858c8acff95702453af07 + size: 227 + original: + hackage: dahdit-network-0.5.1 +- completed: + hackage: dahdit-midi-0.5.1@sha256:106700dd9a1c00506f63825c2e8418954d35a36c7acfa5faf7c73e4ee7d84c9f,3395 + pantry-tree: + sha256: 6ff9ec8f4ec494b2816c2e5f1bb6a9c304216e12cbe18d854f717993823e2058 + size: 562 + original: + hackage: dahdit-midi-0.5.1 +- completed: + hackage: data-sword-0.2.0.3@sha256:953cca4b4533a388df0ef17e06ac5530652d1d73decfe8b656242ccdf4b40999,1568 + pantry-tree: + sha256: 60a104a3d3cbba2e099a9e7ce561322b2884f5323cf74fd876dfb59f265d803c + size: 439 + original: + hackage: data-sword-0.2.0.3 - completed: hackage: daytripper-0.3.1@sha256:225a6e9348c2b5b6e0a55d408559b55df06f1365d5741d2f9d1856d8632769ae,2985 pantry-tree: @@ -32,6 +60,13 @@ packages: size: 279 original: hackage: looksee-0.5.2 +- completed: + hackage: nanotime-0.1.0@sha256:394c44f44fcb0dab9abe267e490ceb836501147abd92fd97a2b9218b1e293700,2827 + pantry-tree: + sha256: 63b85e0fa5eb344d72e41b9e3e998c46bea374d022e4ca93793a67a068e26c80 + size: 167 + original: + hackage: nanotime-0.1.0 snapshots: - completed: sha256: 7b975b104cb3dbf0c297dfd01f936a4d2ee523241dd0b1ae960522b833fe3027