diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a7bcbeaf7..f4ad238cf 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,23 +17,27 @@ jobs:
cabal: latest
args: --allow-newer=base,template-haskell
experimental: true
+ - ghc: 9.4.1
+ cabal: 3.8.1.0
+ args: --allow-newer=base,template-haskell
+ experimental: false
- ghc: 9.0.1
cabal: 3.4.0.0
args: --allow-newer=base,template-haskell
experimental: false
- ghc: 8.10.1
- cabal: 3.2.0.0
+ cabal: 3.4.0.0
args: --allow-newer=base,template-haskell
experimental: false
- ghc: 8.8.3
- cabal: 3.0.0.0
+ cabal: 3.4.0.0
args: --allow-newer=base,template-haskell
experimental: false
- ghc: 8.6.5
- cabal: 2.4.1.0
+ cabal: 3.4.0.0
experimental: false
- ghc: 8.4.4
- cabal: 2.4.1.0
+ cabal: 3.4.0.0
experimental: false
continue-on-error: ${{ matrix.versions.experimental }}
diff --git a/.github/workflows/listener-build-linux.yml b/.github/workflows/listener-build-linux.yml
index 7241d6f8b..cc6749dc0 100644
--- a/.github/workflows/listener-build-linux.yml
+++ b/.github/workflows/listener-build-linux.yml
@@ -112,16 +112,17 @@ jobs:
- name: move executable
run: |
cp dist-newstyle/build/x86_64-linux/ghc-${{ matrix.ghc }}/tidal-listener-0.1.0.0/x/tidal-listener/build/tidal-listener/tidal-listener tidal-listener/binary/tidal-listener
- cp dist-newstyle/build/x86_64-linux/ghc-${{ matrix.ghc }}/tidal-1.7.10/x/tidal/build/tidal/tidal tidal-listener/binary/tidal
+ cp dist-newstyle/build/x86_64-linux/ghc-${{ matrix.ghc }}/tidal-*/x/tidal/build/tidal/tidal tidal-listener/binary/tidal
- name: zip files
run: |
cd tidal-listener/
- tar cvfj binary.tar binary/*
+ mv binary tidal
+ tar cvfj linux.tar tidal/*
- uses: actions/upload-artifact@v2
with:
- path: tidal-listener/binary.tar
+ path: tidal-listener/linux.tar
release:
runs-on: ubuntu-latest
@@ -133,4 +134,4 @@ jobs:
- uses: softprops/action-gh-release@v1
with:
- files: artifact/binary.tar
\ No newline at end of file
+ files: artifact/linux.tar
diff --git a/.github/workflows/listener-build-macosx.yml b/.github/workflows/listener-build-macosx.yml
index f84c73981..5a963ae56 100644
--- a/.github/workflows/listener-build-macosx.yml
+++ b/.github/workflows/listener-build-macosx.yml
@@ -101,7 +101,7 @@ jobs:
- name: move executables
run: |
cp -r dist-newstyle/build/x86_64-osx/ghc-${{ matrix.ghc }}/tidal-listener-0.1.0.0/x/tidal-listener/build/tidal-listener/tidal-listener tidal-listener/binary/tidal-listener
- cp -r dist-newstyle/build/x86_64-osx/ghc-${{ matrix.ghc }}/tidal-1.7.10/x/tidal/build/tidal/tidal tidal-listener/binary/tidal
+ cp -r dist-newstyle/build/x86_64-osx/ghc-${{ matrix.ghc }}/tidal-*/x/tidal/build/tidal/tidal tidal-listener/binary/tidal
- name: zip files
run: |
@@ -122,4 +122,4 @@ jobs:
- uses: softprops/action-gh-release@v1
with:
- files: artifact/macosx.tar
\ No newline at end of file
+ files: artifact/macosx.tar
diff --git a/.github/workflows/listener-build-windows.yml b/.github/workflows/listener-build-windows.yml
index a5c4b95dd..06cc06174 100644
--- a/.github/workflows/listener-build-windows.yml
+++ b/.github/workflows/listener-build-windows.yml
@@ -82,7 +82,7 @@ jobs:
- name: move executables
run: |
Copy-Item -Path 'dist-newstyle\build\x86_64-windows\ghc-${{ matrix.ghc }}\tidal-listener-0.1.0.0\x\tidal-listener\build\tidal-listener\tidal-listener.exe' -Recurse -Destination 'tidal-listener\binary\tidal-listener.exe'
- Copy-Item -Path 'dist-newstyle\build\x86_64-windows\ghc-${{ matrix.ghc }}\tidal-1.7.10\x\tidal\build\tidal\tidal.exe' -Recurse -Destination 'tidal-listener\binary\tidal.exe'
+ Copy-Item -Path 'dist-newstyle\build\x86_64-windows\ghc-${{ matrix.ghc }}\tidal-*\x\tidal\build\tidal\tidal.exe' -Recurse -Destination 'tidal-listener\binary\tidal.exe'
- name: zip files
run: Compress-Archive -LiteralPath 'tidal-listener\binary\' -DestinationPath 'tidal-listener\windows.zip'
@@ -101,4 +101,4 @@ jobs:
- uses: softprops/action-gh-release@v1
with:
- files: artifact/windows.zip
\ No newline at end of file
+ files: artifact/windows.zip
diff --git a/BootTidal.hs b/BootTidal.hs
index f2d3026eb..49194c2af 100644
--- a/BootTidal.hs
+++ b/BootTidal.hs
@@ -6,8 +6,7 @@ import Sound.Tidal.Context
import System.IO (hSetEncoding, stdout, utf8)
hSetEncoding stdout utf8
--- total latency = oLatency + cFrameTimespan
-tidal <- startTidal (superdirtTarget {oLatency = 0.1, oAddress = "127.0.0.1", oPort = 57120}) (defaultConfig {cVerbose = True, cFrameTimespan = 1/20})
+tidal <- startTidal (superdirtTarget {oLatency = 0.05, oAddress = "127.0.0.1", oPort = 57120}) (defaultConfig {cVerbose = True, cFrameTimespan = 1/20})
:{
let only = (hush >>)
diff --git a/cabal.project b/cabal.project
index 0a20fbfb5..34e38b735 100644
--- a/cabal.project
+++ b/cabal.project
@@ -1 +1 @@
-packages: ./ tidal-parse tidal-listener
+packages: ./ tidal-parse tidal-listener tidal-link
diff --git a/old/sync/Canute.hs b/old/sync/Canute.hs
deleted file mode 100644
index 18cbccfd9..000000000
--- a/old/sync/Canute.hs
+++ /dev/null
@@ -1,28 +0,0 @@
-import Sound.Tidal.Tempo (Tempo, logicalTime, clocked, clockedTick, bps)
-
-import Sound.OSC.FD
-import Sound.OSC.Datum
---import Sound.OpenSoundControl
---import Sound.OSC.FD
-import System.IO
-import Control.Concurrent
-
-mykip = "192.168.178.135";
-mykport = 57120
-
-main :: IO ()
-main = do myk <- openUDP mykip mykport
- clockedTick 2 $ onTick myk
-
-wave n = drop i s ++ take i s
- where s = "¸.·´¯`·.´¯`·.¸¸.·´¯`·.¸<º)))><"
- i = n `mod` (length s)
-
-onTick :: UDP -> Tempo -> Int -> IO ()
-onTick myk current ticks =
- do putStr $ "tickmyk " ++ (show ticks) ++ " " ++ (wave ticks) ++ "\r"
- hFlush stdout
- let m = Message "/sync" [int32 ticks, float ((bps current) * 60)]
- forkIO $ do threadDelay $ floor $ 0.075 * 1000000
- sendOSC myk m
- return ()
diff --git a/old/sync/CanuteMIDI.hs b/old/sync/CanuteMIDI.hs
deleted file mode 100644
index 29d8d2590..000000000
--- a/old/sync/CanuteMIDI.hs
+++ /dev/null
@@ -1,62 +0,0 @@
-import qualified Sound.ALSA.Sequencer.Address as Addr
-import qualified Sound.ALSA.Sequencer.Client as Client
-import qualified Sound.ALSA.Sequencer.Port as Port
-import qualified Sound.ALSA.Sequencer.Event as Event
-import qualified Sound.ALSA.Sequencer as SndSeq
-import qualified Sound.ALSA.Exception as AlsaExc
-import qualified Sound.ALSA.Sequencer.Connect as Connect
-import Sound.Tidal.Tempo (Tempo, logicalTime, clocked, clockedTick, bps)
-import System.Environment (getArgs, )
-import Data.Maybe
-import GHC.Word
-import GHC.Int
-
-import Sound.OSC.FD
-import Sound.OSC.Datum
---import Sound.OpenSoundControl
---import Sound.OSC.FD
-import System.IO
-import Control.Concurrent
-
-channel = Event.Channel 0
-
-mykip = "192.168.178.135";
-mykport = 57120
-
-main :: IO ()
-main = do --myk <- openUDP mykip mykport
- h <- SndSeq.openDefault SndSeq.Block
- Client.setName (h :: SndSeq.T SndSeq.OutputMode) "Tidal"
- c <- Client.getId h
- p <- Port.createSimple h "out"
- (Port.caps [Port.capRead, Port.capSubsRead]) Port.typeMidiGeneric
- as <- getArgs
- let dev = fromMaybe "28:0" $ listToMaybe as
- conn <- Connect.createTo h p =<< Addr.parse h dev
- clockedTick 4 $ onTick h conn
-
-wave n = drop i s ++ take i s
- where s = "¸.·´¯`·.´¯`·.¸¸.·´¯`·.¸<" ++ eye ++ ")))><"
- i = n `mod` (length s)
- eye | n `mod` 4 == 0 = "O"
- | otherwise = "º"
-
---onTick :: UDP -> Tempo -> Int -> IO ()
-onTick h conn current ticks =
- do putStr $ "tickmyk " ++ (show ticks) ++ " " ++ (wave ticks) ++ "\r"
- hFlush stdout
- --let m = Message "/sync" [int32 ticks, float ((bps current) * 60)]
- forkIO $ do threadDelay $ floor $ 0.179 * 1000000
- Event.outputDirect h $ noteOn conn (fromIntegral $ ticks `mod` 128) 127
- return ()
-
- --sendOSC myk m
- return ()
-
-noteOn :: Connect.T -> Word8 -> Word8 -> Event.T
-noteOn conn n v =
- Event.forConnection conn
- $ Event.NoteEv Event.NoteOn
- $ Event.simpleNote channel
- (Event.Pitch (n))
- (Event.Velocity v)
diff --git a/old/sync/Leafcutter.hs b/old/sync/Leafcutter.hs
deleted file mode 100644
index 418df68a0..000000000
--- a/old/sync/Leafcutter.hs
+++ /dev/null
@@ -1,38 +0,0 @@
-
-import Sound.Tidal.Tempo (State, Tempo, clocked, cps, ticks)
-import Sound.Tidal.Config
-import Control.Concurrent.MVar
-import Control.Monad (when)
-
-import Sound.OSC.FD
-import Sound.OSC.Datum
---import Sound.OpenSoundControl
---import Sound.OSC.FD
-import System.IO
-
-tpb = 4
-
-mykip = "10.0.1.10";
-mykport = 4000
-main :: IO ()
-main = do myk <- openUDP mykip mykport
- tempoMV <- newEmptyMVar
- clocked defaultConfig tempoMV $ onTick myk tempoMV
- putStrLn "hmm."
- return ()
-
-wave n = drop i s ++ take i s
- where s = "¸.·´¯`·.´¯`·.¸¸.·´¯`·.¸<º)))><"
- i = n `mod` (length s)
-
-onTick :: UDP -> MVar Tempo -> State -> IO ()
-onTick myk mtempo current =
- do let t = ticks current
- when (t `mod` tpb == 0) $
- do tempo <- readMVar mtempo
- let b = t `div` tpb
- bpm = (cps tempo) * 60
- putStr $ show bpm ++ " : " ++ (show b) ++ " " ++ (wave b) ++ "\r"
- hFlush stdout
- let m = Message "/sync" [int32 b, float bpm]
- sendMessage myk m
diff --git a/old/sync/Sync.hs b/old/sync/Sync.hs
deleted file mode 100644
index fd37dd6f5..000000000
--- a/old/sync/Sync.hs
+++ /dev/null
@@ -1,36 +0,0 @@
-import Sound.Tidal.Tempo (Tempo, logicalTime, clocked, clockedTick, bps)
-
-import Sound.OSC.FD
-import Sound.OSC.Datum
---import Sound.OpenSoundControl
---import Sound.OSC.FD
-import System.IO
-
-daveip = "192.168.0.3";
-daveport = 4000
-adeip = "10.0.0.3";
-adeport = 1777;
-
-tpb = 8
-
-main :: IO ()
-main = do dave <- openUDP daveip daveport
- clocked $ onTick dave
-
-wave n = drop i s ++ take i s
- where s = "¸.·´¯`·.´¯`·.¸¸.·´¯`·.¸<º)))><"
- i = n `mod` (length s)
-
-onTick :: UDP -> Tempo -> Int -> IO ()
-onTick dave current ticks
- | ticks `mod` 8 == 0 =
- do putStr $ "tickdave " ++ (show ticks) ++ " " ++ (wave ticks) ++ "\r"
- hFlush stdout
- let m = Message "/sync" [int32 tpb, float ((bps current) * 60)]
- sendOSC dave m
- | otherwise = return ()
-
--- onTickAde :: UDP -> BpsChange -> Int -> IO ()
--- onTickAde ade current ticks =
--- do let n = Message "/PureEvents/Beat" [Int ticks]
--- sendOSC ade n
diff --git a/old/sync/serial.hs b/old/sync/serial.hs
deleted file mode 100644
index f1699969b..000000000
--- a/old/sync/serial.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-import Sound.Tidal.Tempo (Tempo, logicalTime, clocked, clockedTick, bps)
-import System.Hardware.Serialport
-import qualified Data.ByteString.Char8 as B
-import Data.Char
-import Data.Word
-
-import Sound.OSC.FD
-import Sound.OSC.Datum
-import System.IO
-import Control.Concurrent
-
-tpb = 1
-
-wave n = drop i s ++ take i s
- where s = "¸.·´¯`·.´¯`·.¸¸.·´¯`·.¸<" ++ eye ++ ")))><"
- i = n `mod` (length s)
- eye | n `mod` 4 == 0 = "O"
- | otherwise = "º"
-
-onTick ard current ticks =
- do let message = B.pack [chr $ if (ticks `mod` 4 == 0) then 100 else 50]
- forkIO $ do threadDelay $ floor $ 0.09 * 1000000
- send ard message
- return ()
- threadDelay $ floor $ 0.04 * 1000000
- putStr $ "Pulse " ++ (show ticks) ++ " " ++ (wave ticks) ++ "\r"
- hFlush stdout
- return ()
-
-main = do ard <- openSerial "/dev/serial/by-id/usb-Arduino__www.arduino.cc__Arduino_Uno_64938323231351417131-if00" defaultSerialSettings {commSpeed = CS9600}
- clockedTick 4 $ onTick ard
diff --git a/src/Sound/Tidal/Carabiner.hs b/src/Sound/Tidal/Carabiner.hs
deleted file mode 100644
index 549a24f00..000000000
--- a/src/Sound/Tidal/Carabiner.hs
+++ /dev/null
@@ -1,83 +0,0 @@
-{-# OPTIONS_GHC -fno-warn-dodgy-imports -fno-warn-name-shadowing #-}
-module Sound.Tidal.Carabiner where
-
-{-
- Carabiner.hs - For syncing with the Link protocol over Carabiner.
- Copyright (C) 2020, Alex McLean and contributors
-
- This library is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This library is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this library. If not, see .
--}
-
-import Network.Socket hiding (send, sendTo, recv, recvFrom)
-import Network.Socket.ByteString (send, recv)
-import qualified Data.ByteString.Char8 as B8
-import Control.Concurrent (forkIO, takeMVar, putMVar)
-import qualified Sound.Tidal.Stream as S
-import Sound.Tidal.Tempo
-import System.Clock
-import Text.Read (readMaybe)
-import Control.Monad (when, forever)
-import Data.Maybe (isJust, fromJust)
-import qualified Sound.OSC.FD as O
-
-carabiner :: S.Stream -> Int -> Double -> IO Socket
-carabiner tidal bpc latency = do sock <- client tidal bpc latency "127.0.0.1" 17000
- sendMsg sock "status\n"
- return sock
-
-client :: S.Stream -> Int -> Double -> String -> Int -> IO Socket
-client tidal bpc latency host port = withSocketsDo $
- do addrInfo <- getAddrInfo Nothing (Just host) (Just $ show port)
- let serverAddr = head addrInfo
- sock <- socket (addrFamily serverAddr) Stream defaultProtocol
- connect sock (addrAddress serverAddr)
- _ <- forkIO $ listener tidal bpc latency sock
- -- sendMsg sock "status\n"
- -- threadDelay 10000000
- return sock
-
-listener :: S.Stream -> Int -> Double -> Socket -> IO ()
-listener tidal bpc latency sock =
- forever $ do rMsg <- recv sock 1024
- let msg = B8.unpack rMsg
- (name:_:ws) = words msg
- pairs = pairs' ws
- pairs' (a:b:xs) = (a,b):pairs' xs
- pairs' _ = []
- act tidal bpc latency name pairs
-
-act :: S.Stream -> Int -> Double -> String -> [(String, String)] -> IO ()
-act tidal bpc latency "status" pairs
- = do let start = (lookup ":start" pairs >>= readMaybe) :: Maybe Integer
- bpm = (lookup ":bpm" pairs >>= readMaybe) :: Maybe Double
- beat = (lookup ":beat" pairs >>= readMaybe) :: Maybe Double
- when (and [isJust start, isJust bpm, isJust beat]) $ do
- nowM <- getTime Monotonic
- nowO <- O.time
- let m = fromIntegral (sec nowM) + (fromIntegral (nsec nowM)/1000000000)
- d = nowO - m
- start' = fromIntegral (fromJust start) / 1000000
- startO = start' + d
- -- cyc = toRational $ (fromJust beat) / (fromIntegral bpc)
- tempo <- takeMVar (S.sTempoMV tidal)
- let tempo' = tempo {atTime = startO + latency,
- atCycle = 0,
- cps = (fromJust bpm / 60) / fromIntegral bpc
- }
- putMVar (S.sTempoMV tidal) tempo'
-act _ _ _ name _ = putStr $ "Unhandled thingie " ++ name
-
-sendMsg :: Socket -> String -> IO ()
-sendMsg sock msg = do _ <- send sock $ B8.pack msg
- return ()
diff --git a/src/Sound/Tidal/Chords.hs b/src/Sound/Tidal/Chords.hs
index 3650c7989..5c94d3dd8 100644
--- a/src/Sound/Tidal/Chords.hs
+++ b/src/Sound/Tidal/Chords.hs
@@ -274,3 +274,44 @@ chordL p = (\name -> fromMaybe [] $ lookup name chordTable) <$> p
chordList :: String
chordList = unwords $ map fst (chordTable :: [(String, [Int])])
+data Modifier = Range Int | Drop Int | Invert | Open deriving Eq
+
+instance Show Modifier where
+ show (Range i) = "Range " ++ show i
+ show (Drop i) = "Drop " ++ show i
+ show Invert = "Invert"
+ show Open = "Open"
+
+applyModifier :: (Enum a, Num a) => Modifier -> [a] -> [a]
+applyModifier (Range i) ds = take i $ concatMap (\x -> map (+ x) ds) [0,12..]
+applyModifier Invert [] = []
+applyModifier Invert (d:ds) = ds ++ [d+12]
+applyModifier Open ds = case length ds > 2 of
+ True -> [ (ds !! 0 - 12), (ds !! 2 - 12), (ds !! 1) ] ++ reverse (take (length ds - 3) (reverse ds))
+ False -> ds
+applyModifier (Drop i) ds = case length ds < i of
+ True -> ds
+ False -> (ds!!s - 12):(xs ++ drop 1 ys)
+ where (xs,ys) = splitAt s ds
+ s = length ds - i
+
+applyModifierPat :: (Num a, Enum a) => Pattern [a] -> Pattern [Modifier] -> Pattern [a]
+applyModifierPat pat modsP = do
+ ch <- pat
+ ms <- modsP
+ return $ foldl (flip applyModifier) ch ms
+
+applyModifierPatSeq :: (Num a, Enum a) => (a -> b) -> Pattern [a] -> [Pattern [Modifier]] -> Pattern [b]
+applyModifierPatSeq f pat [] = fmap (map f) pat
+applyModifierPatSeq f pat (mP:msP) = applyModifierPatSeq f (applyModifierPat pat mP) msP
+
+chordToPatSeq :: (Num a, Enum a) => (a -> b) -> Pattern a -> Pattern String -> [Pattern [Modifier]] -> Pattern b
+chordToPatSeq f noteP nameP modsP = uncollect $ do
+ n <- noteP
+ name <- nameP
+ let ch = map (+ n) (fromMaybe [0] $ lookup name chordTable)
+ applyModifierPatSeq f (return ch) modsP
+
+-- | turns a given pattern of some Num type, a pattern of chord names and a list of patterns of modifiers into a chord pattern
+chord :: (Num a, Enum a) => Pattern a -> Pattern String -> [Pattern [Modifier]] -> Pattern a
+chord = chordToPatSeq id
diff --git a/src/Sound/Tidal/Config.hs b/src/Sound/Tidal/Config.hs
index 9f468d421..c9d2e9f00 100644
--- a/src/Sound/Tidal/Config.hs
+++ b/src/Sound/Tidal/Config.hs
@@ -1,5 +1,9 @@
module Sound.Tidal.Config where
+import qualified Sound.Tidal.Link as Link
+import Data.Int(Int64)
+import Foreign.C.Types (CDouble)
+
{-
Config.hs - For default Tidal configuration values.
Copyright (C) 2020, Alex McLean and contributors
@@ -23,11 +27,15 @@ data Config = Config {cCtrlListen :: Bool,
cCtrlPort :: Int,
cCtrlBroadcast :: Bool,
cFrameTimespan :: Double,
+ cEnableLink :: Bool,
+ cProcessAhead :: Double,
cTempoAddr :: String,
cTempoPort :: Int,
cTempoClientPort :: Int,
- cSkipTicks :: Int,
- cVerbose :: Bool
+ cSkipTicks :: Int64,
+ cVerbose :: Bool,
+ cQuantum :: CDouble,
+ cCyclesPerBeat :: CDouble
}
defaultConfig :: Config
@@ -36,9 +44,13 @@ defaultConfig = Config {cCtrlListen = True,
cCtrlPort = 6010,
cCtrlBroadcast = False,
cFrameTimespan = 1/20,
+ cEnableLink = True,
+ cProcessAhead = 3/10,
cTempoAddr = "127.0.0.1",
cTempoPort = 9160,
cTempoClientPort = 0, -- choose at random
cSkipTicks = 10,
- cVerbose = True
+ cVerbose = True,
+ cQuantum = 4,
+ cCyclesPerBeat = 4
}
diff --git a/src/Sound/Tidal/Context.hs b/src/Sound/Tidal/Context.hs
index c38df24ee..3d78630f5 100644
--- a/src/Sound/Tidal/Context.hs
+++ b/src/Sound/Tidal/Context.hs
@@ -22,7 +22,6 @@ import Prelude hiding ((<*), (*>))
import Data.Ratio as C
-import Sound.Tidal.Carabiner as C
import Sound.Tidal.Config as C
import Sound.Tidal.Control as C
import Sound.Tidal.Core as C
@@ -36,4 +35,3 @@ import Sound.Tidal.Stream as C
import Sound.Tidal.Transition as C
import Sound.Tidal.UI as C
import Sound.Tidal.Version as C
-import Sound.Tidal.EspGrid as C
diff --git a/src/Sound/Tidal/Control.hs b/src/Sound/Tidal/Control.hs
index d553166a8..8c265fbca 100644
--- a/src/Sound/Tidal/Control.hs
+++ b/src/Sound/Tidal/Control.hs
@@ -350,21 +350,21 @@ smash' n xs p = slowcat $ map (`slow` p') xs
This adds a bit of echo:
@
- d1 $ echo 4 0.5 0.2 $ sound "bd sn"
+ d1 $ echo 4 0.2 0.5 $ sound "bd sn"
@
The above results in 4 echos, each one 50% quieter than the last, with 1/5th of a cycle between them.
It is possible to reverse the echo:
@
- d1 $ echo 4 0.5 (-0.2) $ sound "bd sn"
+ d1 $ echo 4 (-0.2) 0.5 $ sound "bd sn"
@
-}
echo :: Pattern Integer -> Pattern Rational -> Pattern Double -> ControlPattern -> ControlPattern
echo = tParam3 _echo
_echo :: Integer -> Rational -> Double -> ControlPattern -> ControlPattern
-_echo count time feedback p = stack (p:map (\x -> ((x%1)*time) `rotR` (p |* P.gain (pure $ (* feedback) (fromIntegral x)))) [1..(count-1)])
+_echo count time feedback p = _echoWith count time (|* P.gain (pure $ feedback)) p
{- |
Allows to apply a function for each step and overlays the result delayed by the given time.
diff --git a/src/Sound/Tidal/Core.hs b/src/Sound/Tidal/Core.hs
index b53dc0400..04592b9d0 100644
--- a/src/Sound/Tidal/Core.hs
+++ b/src/Sound/Tidal/Core.hs
@@ -150,6 +150,9 @@ a |+| b = (+) <$> a <*> b
a |+ b = (+) <$> a <* b
( +|) :: Num a => Pattern a -> Pattern a -> Pattern a
a +| b = (+) <$> a *> b
+(||+) :: Num a => Pattern a -> Pattern a -> Pattern a
+a ||+ b = (+) <$> a <<* b
+
(|++|) :: Applicative a => a String -> a String -> a String
a |++| b = (++) <$> a <*> b
@@ -157,6 +160,8 @@ a |++| b = (++) <$> a <*> b
a |++ b = (++) <$> a <* b
( ++|) :: Pattern String -> Pattern String -> Pattern String
a ++| b = (++) <$> a *> b
+(||++) :: Pattern String -> Pattern String -> Pattern String
+a ||++ b = (++) <$> a <<* b
(|/|) :: (Applicative a, Fractional b) => a b -> a b -> a b
a |/| b = (/) <$> a <*> b
@@ -164,6 +169,8 @@ a |/| b = (/) <$> a <*> b
a |/ b = (/) <$> a <* b
( /|) :: Fractional a => Pattern a -> Pattern a -> Pattern a
a /| b = (/) <$> a *> b
+(||/) :: Fractional a => Pattern a -> Pattern a -> Pattern a
+a ||/ b = (/) <$> a <<* b
(|*|) :: (Applicative a, Num b) => a b -> a b -> a b
a |*| b = (*) <$> a <*> b
@@ -171,6 +178,8 @@ a |*| b = (*) <$> a <*> b
a |* b = (*) <$> a <* b
( *|) :: Num a => Pattern a -> Pattern a -> Pattern a
a *| b = (*) <$> a *> b
+(||*) :: Num a => Pattern a -> Pattern a -> Pattern a
+a ||* b = (*) <$> a <<* b
(|-|) :: (Applicative a, Num b) => a b -> a b -> a b
a |-| b = (-) <$> a <*> b
@@ -178,6 +187,8 @@ a |-| b = (-) <$> a <*> b
a |- b = (-) <$> a <* b
( -|) :: Num a => Pattern a -> Pattern a -> Pattern a
a -| b = (-) <$> a *> b
+(||-) :: Num a => Pattern a -> Pattern a -> Pattern a
+a ||- b = (-) <$> a <<* b
(|%|) :: (Applicative a, Moddable b) => a b -> a b -> a b
a |%| b = gmod <$> a <*> b
@@ -185,6 +196,8 @@ a |%| b = gmod <$> a <*> b
a |% b = gmod <$> a <* b
( %|) :: Moddable a => Pattern a -> Pattern a -> Pattern a
a %| b = gmod <$> a *> b
+(||%) :: Moddable a => Pattern a -> Pattern a -> Pattern a
+a ||% b = gmod <$> a <<* b
(|**|) :: (Applicative a, Floating b) => a b -> a b -> a b
a |**| b = (**) <$> a <*> b
@@ -192,6 +205,8 @@ a |**| b = (**) <$> a <*> b
a |** b = (**) <$> a <* b
( **|) :: Floating a => Pattern a -> Pattern a -> Pattern a
a **| b = (**) <$> a *> b
+(||**) :: Floating a => Pattern a -> Pattern a -> Pattern a
+a ||** b = (**) <$> a <<* b
(|>|) :: (Applicative a, Unionable b) => a b -> a b -> a b
a |>| b = flip union <$> a <*> b
@@ -199,6 +214,8 @@ a |>| b = flip union <$> a <*> b
a |> b = flip union <$> a <* b
( >|) :: Unionable a => Pattern a -> Pattern a -> Pattern a
a >| b = flip union <$> a *> b
+(||>) :: Unionable a => Pattern a -> Pattern a -> Pattern a
+a ||> b = flip union <$> a <<* b
(|<|) :: (Applicative a, Unionable b) => a b -> a b -> a b
a |<| b = union <$> a <*> b
@@ -206,6 +223,8 @@ a |<| b = union <$> a <*> b
a |< b = union <$> a <* b
( <|) :: Unionable a => Pattern a -> Pattern a -> Pattern a
a <| b = union <$> a *> b
+(||<) :: Unionable a => Pattern a -> Pattern a -> Pattern a
+a ||< b = union <$> a <<* b
-- Backward compatibility - structure from left, values from right.
(#) :: Unionable b => Pattern b -> Pattern b -> Pattern b
diff --git a/src/Sound/Tidal/EspGrid.hs b/src/Sound/Tidal/EspGrid.hs
deleted file mode 100644
index 05b96985d..000000000
--- a/src/Sound/Tidal/EspGrid.hs
+++ /dev/null
@@ -1,72 +0,0 @@
-{-# LANGUAGE ScopedTypeVariables #-}
-
-module Sound.Tidal.EspGrid (tidalEspGridLink,cpsEsp,espgrid) where
-
-{-
- EspGrid.hs - Provides ability to sync via the ESP Grid
- Copyright (C) 2020, David Ogborn and contributors
-
- This library is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This library is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this library. If not, see .
--}
-
-import Control.Concurrent.MVar
-import Control.Concurrent (forkIO,threadDelay)
-import Control.Monad (forever)
-import Control.Exception
-import Sound.OSC.FD
-import Sound.Tidal.Tempo
-import Sound.Tidal.Stream (Stream, sTempoMV)
-
-parseEspTempo :: [Datum] -> Maybe (Tempo -> Tempo)
-parseEspTempo d = do
- on :: Integer <- datum_integral (d!!0)
- bpm <- datum_floating (d!!1)
- t1 :: Integer <- datum_integral (d!!2)
- t2 <- datum_integral (d!!3)
- n :: Integer <- datum_integral (d!!4)
- let nanos = (t1*1000000000) + t2
- return $ \t -> t {
- atTime = ut_to_ntpr $ realToFrac nanos / 1000000000,
- atCycle = fromIntegral n,
- cps = bpm/60,
- paused = on == 0
- }
-
-changeTempo :: MVar Tempo -> Packet -> IO ()
-changeTempo t (Packet_Message msg) =
- case parseEspTempo (messageDatum msg) of
- Just f -> modifyMVarMasked_ t $ \t0 -> return (f t0)
- Nothing -> putStrLn "Warning: Unable to parse message from EspGrid as Tempo"
-changeTempo _ _ = putStrLn "Serious error: Can only process Packet_Message"
-
-tidalEspGridLink :: MVar Tempo -> IO ()
-tidalEspGridLink _ = putStrLn "Function no longer supported, please use 'espgrid tidal' to connect to ESPgrid instead."
-
-espgrid :: Stream -> IO ()
-espgrid st = do
- let t = sTempoMV st
- socket <- openUDP "127.0.0.1" 5510
- _ <- forkIO $ forever $ do
- (do
- sendMessage socket $ Message "/esp/tempo/q" []
- response <- waitAddress socket "/esp/tempo/r"
- Sound.Tidal.EspGrid.changeTempo t response
- threadDelay 200000)
- `catch` (\e -> putStrLn $ "exception caught in tidalEspGridLink: " ++ show (e :: SomeException))
- return ()
-
-cpsEsp :: Real t => t -> IO ()
-cpsEsp t = do
- socket <- openUDP "127.0.0.1" 5510
- sendMessage socket $ Message "/esp/beat/tempo" [float (t*60)]
diff --git a/src/Sound/Tidal/ParseBP.hs b/src/Sound/Tidal/ParseBP.hs
index 937f2dee5..479cb3214 100644
--- a/src/Sound/Tidal/ParseBP.hs
+++ b/src/Sound/Tidal/ParseBP.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE OverloadedStrings, FlexibleInstances, CPP, DeriveFunctor #-}
+{-# LANGUAGE OverloadedStrings, FlexibleInstances, CPP, DeriveFunctor, GADTs, StandaloneDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# OPTIONS_GHC -Wall -fno-warn-orphans -fno-warn-unused-do-bind #-}
@@ -42,7 +42,7 @@ import qualified Text.Parsec.Prim
import Sound.Tidal.Pattern
import Sound.Tidal.UI
import Sound.Tidal.Core
-import Sound.Tidal.Chords (chordTable)
+import Sound.Tidal.Chords
import Sound.Tidal.Utils (fromRight)
data TidalParseError = TidalParseError {parsecError :: ParseError,
@@ -62,22 +62,59 @@ type MyParser = Text.Parsec.Prim.Parsec String Int
-- | AST representation of patterns
-data TPat a = TPat_Atom (Maybe ((Int, Int), (Int, Int))) a
- | TPat_Fast (TPat Time) (TPat a)
- | TPat_Slow (TPat Time) (TPat a)
- | TPat_DegradeBy Int Double (TPat a)
- | TPat_CycleChoose Int [TPat a]
- | TPat_Euclid (TPat Int) (TPat Int) (TPat Int) (TPat a)
- | TPat_Stack [TPat a]
- | TPat_Polyrhythm (Maybe (TPat Rational)) [TPat a]
- | TPat_Seq [TPat a]
- | TPat_Silence
- | TPat_Foot
- | TPat_Elongate Rational (TPat a)
- | TPat_Repeat Int (TPat a)
- | TPat_EnumFromTo (TPat a) (TPat a)
- | TPat_Var String
- deriving (Show, Functor)
+data TPat a where
+ TPat_Atom :: (Maybe ((Int, Int), (Int, Int))) -> a -> (TPat a)
+ TPat_Fast :: (TPat Time) -> (TPat a) -> (TPat a)
+ TPat_Slow :: (TPat Time) -> (TPat a) -> (TPat a)
+ TPat_DegradeBy :: Int -> Double -> (TPat a) -> (TPat a)
+ TPat_CycleChoose :: Int -> [TPat a] -> (TPat a)
+ TPat_Euclid :: (TPat Int) -> (TPat Int) -> (TPat Int) -> (TPat a) -> (TPat a)
+ TPat_Stack :: [TPat a] -> (TPat a)
+ TPat_Polyrhythm :: (Maybe (TPat Rational)) -> [TPat a] -> (TPat a)
+ TPat_Seq :: [TPat a] -> (TPat a)
+ TPat_Silence :: (TPat a)
+ TPat_Foot :: (TPat a)
+ TPat_Elongate :: Rational -> (TPat a) -> (TPat a)
+ TPat_Repeat :: Int -> (TPat a) -> (TPat a)
+ TPat_EnumFromTo :: (TPat a) -> (TPat a) -> (TPat a)
+ TPat_Var :: String -> (TPat a)
+ TPat_Chord :: (Num b, Enum b, Parseable b, Enumerable b) => (b -> a) -> (TPat b) -> (TPat String) -> [TPat [Modifier]] -> (TPat a)
+
+instance Show a => Show (TPat a) where
+ show (TPat_Atom c v) = "TPat_Atom (" ++ show c ++ ") (" ++ show v ++ ")"
+ show (TPat_Fast t v) = "TPat_Fast (" ++ show t ++ ") (" ++ show v ++ ")"
+ show (TPat_Slow t v) = "TPat_Slow (" ++ show t ++ ") (" ++ show v ++ ")"
+ show (TPat_DegradeBy x r v) = "TPat_DegradeBy (" ++ show x ++ ") (" ++ show r ++ ") (" ++ show v ++ ")"
+ show (TPat_CycleChoose x vs) = "TPat_CycleChoose (" ++ show x ++ ") (" ++ show vs ++ ")"
+ show (TPat_Euclid a b c v) = "TPat_Euclid (" ++ show a ++ ") (" ++ show b ++ ") (" ++ show c ++ ") " ++ show v ++ ")"
+ show (TPat_Stack vs) = "TPat_Stack " ++ show vs
+ show (TPat_Polyrhythm mSteprate vs) = "TPat_Polyrhythm (" ++ show mSteprate ++ ") " ++ show vs
+ show (TPat_Seq vs) = "TPat_Seq " ++ show vs
+ show TPat_Silence = "TPat_Silence"
+ show TPat_Foot = "TPat_Foot"
+ show (TPat_Elongate r v) = "TPat_Elongate (" ++ show r ++ ") (" ++ show v ++ ")"
+ show (TPat_Repeat r v) = "TPat_Repeat (" ++ show r ++ ") (" ++ show v ++ ")"
+ show (TPat_EnumFromTo a b) = "TPat_EnumFromTo (" ++ show a ++ ") (" ++ show b ++ ")"
+ show (TPat_Var s) = "TPat_Var " ++ show s
+ show (TPat_Chord g iP nP msP) = "TPat_Chord (" ++ (show $ fmap g iP) ++ ") (" ++ show nP ++ ") (" ++ show msP ++ ")"
+
+instance Functor TPat where
+ fmap f (TPat_Atom c v) = TPat_Atom c (f v)
+ fmap f (TPat_Fast t v) = TPat_Fast t (fmap f v)
+ fmap f (TPat_Slow t v) = TPat_Slow t (fmap f v)
+ fmap f (TPat_DegradeBy x r v) = TPat_DegradeBy x r (fmap f v)
+ fmap f (TPat_CycleChoose x vs) = TPat_CycleChoose x (map (fmap f) vs)
+ fmap f (TPat_Euclid a b c v) = TPat_Euclid a b c (fmap f v)
+ fmap f (TPat_Stack vs) = TPat_Stack (map (fmap f) vs)
+ fmap f (TPat_Polyrhythm mSteprate vs) = TPat_Polyrhythm mSteprate (map (fmap f) vs)
+ fmap f (TPat_Seq vs) = TPat_Seq (map (fmap f) vs)
+ fmap _ TPat_Silence = TPat_Silence
+ fmap _ TPat_Foot = TPat_Foot
+ fmap f (TPat_Elongate r v) = TPat_Elongate r (fmap f v)
+ fmap f (TPat_Repeat r v) = TPat_Repeat r (fmap f v)
+ fmap f (TPat_EnumFromTo a b) = TPat_EnumFromTo (fmap f a) (fmap f b)
+ fmap _ (TPat_Var s) = TPat_Var s
+ fmap f (TPat_Chord g iP nP msP) = TPat_Chord (f . g) iP nP msP
tShowList :: (Show a) => [TPat a] -> String
tShowList vs = "[" ++ intercalate "," (map tShow vs) ++ "]"
@@ -107,6 +144,7 @@ tShow (TPat_Seq vs) = snd $ steps_seq vs
tShow TPat_Silence = "silence"
tShow (TPat_EnumFromTo a b) = "unwrap $ fromTo <$> (" ++ tShow a ++ ") <*> (" ++ tShow b ++ ")"
tShow (TPat_Var s) = "getControl " ++ s
+tShow (TPat_Chord f n name mods) = "chord (" ++ (tShow $ fmap f n) ++ ") (" ++ tShow name ++ ")" ++ tShowList mods
tShow a = "can't happen? " ++ show a
@@ -132,6 +170,7 @@ toPat = \case
| otherwise = pure $ fst $ head pats
TPat_Seq xs -> snd $ resolve_seq xs
TPat_Var s -> getControl s
+ TPat_Chord f iP nP mP -> chordToPatSeq f (toPat iP) (toPat nP) (map toPat mP)
_ -> silence
resolve_tpat :: (Enumerable a, Parseable a) => TPat a -> (Rational, Pattern a)
@@ -431,22 +470,28 @@ pChar :: MyParser (TPat Char)
pChar = wrapPos $ TPat_Atom Nothing <$> pCharNum
pDouble :: MyParser (TPat Double)
-pDouble = wrapPos $ do s <- sign
- f <- choice [fromRational <$> pRatio, parseNote] > "float"
- let v = applySign s f
- do TPat_Stack . map (TPat_Atom Nothing . (+ v)) <$> parseChord
- <|> return (TPat_Atom Nothing v)
- <|>
- do TPat_Stack . map (TPat_Atom Nothing) <$> parseChord
+pDouble = try $ do d <- pDoubleWithoutChord
+ pChord d <|> return d
+ <|> pChord (TPat_Atom Nothing 0)
+ <|> pDoubleWithoutChord
+
+pDoubleWithoutChord :: MyParser (TPat Double)
+pDoubleWithoutChord = pPart $ wrapPos $ do s <- sign
+ f <- choice [fromRational <$> pRatio, parseNote] > "float"
+ return $ TPat_Atom Nothing (applySign s f)
pNote :: MyParser (TPat Note)
-pNote = wrapPos $ fmap (fmap Note) $ do s <- sign
- f <- choice [intOrFloat, parseNote] > "float"
- let v = applySign s f
- do TPat_Stack . map (TPat_Atom Nothing . (+ v)) <$> parseChord
- <|> return (TPat_Atom Nothing v)
- <|> do TPat_Stack . map (TPat_Atom Nothing) <$> parseChord
- <|> do TPat_Atom Nothing . fromRational <$> pRatio
+pNote = try $ do n <- pNoteWithoutChord
+ pChord n <|> return n
+ <|> pChord (TPat_Atom Nothing 0)
+ <|> pNoteWithoutChord
+ <|> do TPat_Atom Nothing . fromRational <$> pRatio
+
+pNoteWithoutChord :: MyParser (TPat Note)
+pNoteWithoutChord = pPart $ wrapPos $ do s <- sign
+ f <- choice [intOrFloat, parseNote] > "float"
+ return $ TPat_Atom Nothing (Note $ applySign s f)
+
pBool :: MyParser (TPat Bool)
pBool = wrapPos $ do oneOf "t1"
@@ -462,12 +507,14 @@ parseIntNote = do s <- sign
then return $ applySign s $ round d
else fail "not an integer"
-pIntegral :: Integral a => MyParser (TPat a)
-pIntegral = wrapPos $ do i <- parseIntNote
- do TPat_Stack . map (TPat_Atom Nothing . (+i)) <$> parseChord
- <|> return (TPat_Atom Nothing i)
- <|>
- do TPat_Stack . map (TPat_Atom Nothing) <$> parseChord
+pIntegral :: (Integral a, Parseable a, Enumerable a) => MyParser (TPat a)
+pIntegral = try $ do i <- pIntegralWithoutChord
+ pChord i <|> return i
+ <|> pChord (TPat_Atom Nothing 0)
+ <|> pIntegralWithoutChord
+
+pIntegralWithoutChord :: (Integral a, Parseable a, Enumerable a) => MyParser (TPat a)
+pIntegralWithoutChord = pPart $ wrapPos $ fmap (TPat_Atom Nothing) parseIntNote
parseChord :: (Enum a, Num a) => MyParser [a]
parseChord = do char '\''
@@ -607,3 +654,47 @@ pRatioSingleChar c v = try $ do
isInt :: RealFrac a => a -> Bool
isInt x = x == fromInteger (round x)
+
+---
+
+instance Parseable [Modifier] where
+ tPatParser = pModifiers
+ doEuclid = euclidOff
+
+instance Enumerable [Modifier] where
+ fromTo a b = fastFromList [a,b]
+ fromThenTo a b c = fastFromList [a,b,c]
+
+parseModInv :: MyParser Modifier
+parseModInv = char 'i' >> return Invert
+
+parseModInvNum :: MyParser [Modifier]
+parseModInvNum = do
+ char 'i'
+ n <- pInteger
+ return $ replicate (round n) Invert
+
+parseModDrop :: MyParser [Modifier]
+parseModDrop = do
+ char 'd'
+ n <- pInteger
+ return $ [Drop $ round n]
+
+parseModOpen :: MyParser Modifier
+parseModOpen = char 'o' >> return Open
+
+parseModRange :: MyParser Modifier
+parseModRange = parseIntNote >>= \i -> return $ Range $ fromIntegral i
+
+parseModifiers :: MyParser [Modifier]
+parseModifiers = (many1 parseModOpen) <|> parseModDrop <|> (fmap pure parseModRange) <|> try parseModInvNum <|> (many1 parseModInv) > "modifier"
+
+pModifiers :: MyParser (TPat [Modifier])
+pModifiers = wrapPos $ TPat_Atom Nothing <$> parseModifiers
+
+pChord :: (Enum a, Num a, Parseable a, Enumerable a) => TPat a -> MyParser (TPat a)
+pChord i = do
+ char '\''
+ n <- pPart pVocable > "chordname"
+ ms <- option [] $ many1 $ (char '\'' >> pPart pModifiers)
+ return $ TPat_Chord id i n ms
diff --git a/src/Sound/Tidal/Pattern.hs b/src/Sound/Tidal/Pattern.hs
index 894b249de..57a50c907 100644
--- a/src/Sound/Tidal/Pattern.hs
+++ b/src/Sound/Tidal/Pattern.hs
@@ -37,7 +37,7 @@ import Control.DeepSeq (NFData)
import Control.Monad ((>=>))
import qualified Data.Map.Strict as Map
import Data.Maybe (isJust, fromJust, catMaybes, mapMaybe)
-import Data.List (delete, findIndex, sort)
+import Data.List (delete, findIndex, (\\))
import Data.Word (Word8)
import Data.Data (Data) -- toConstr
import Data.Typeable (Typeable)
@@ -79,7 +79,11 @@ instance Applicative Pattern where
(*>) :: Pattern (a -> b) -> Pattern a -> Pattern b
(*>) = applyPatToPatRight
-infixl 4 <*, *>
+-- | Like <*>, but the 'wholes' come from the left
+(<<*) :: Pattern (a -> b) -> Pattern a -> Pattern b
+(<<*) = applyPatToPatSqueeze
+
+infixl 4 <*, *>, <<*
applyPatToPat :: (Maybe Arc -> Maybe Arc -> Maybe (Maybe Arc)) -> Pattern (a -> b) -> Pattern a -> Pattern b
applyPatToPat combineWholes pf px = Pattern q
where q st = catMaybes $ concatMap match $ query pf st
@@ -126,6 +130,9 @@ applyPatToPatRight pf px = Pattern q
part' <- subArc (part ef) (part ex)
return (Event (combineContexts [context ef, context ex]) whole' part' (value ef $ value ex))
+applyPatToPatSqueeze :: Pattern (a -> b) -> Pattern a -> Pattern b
+applyPatToPatSqueeze pf px = squeezeJoin $ (\f -> f <$> px) <$> pf
+
-- * Monad and friends
-- Note there are four ways of joining - the default 'unwrap' used by @>>=@, as well
@@ -341,7 +348,7 @@ empty :: Pattern a
empty = Pattern {query = const []}
queryArc :: Pattern a -> Arc -> [Event a]
-queryArc p a = query p $ State a Map.empty
+queryArc p a = query p $ State a Map.empty
-- | Splits queries that span cycles. For example `query p (0.5, 1.5)` would be
-- turned into two queries, `(0.5,1)` and `(1,1.5)`, and the results
@@ -422,7 +429,7 @@ compressArcTo (Arc s e) = compressArc (Arc (cyclePos s) (e - sam s))
_fastGap :: Time -> Pattern a -> Pattern a
_fastGap 0 _ = empty
-_fastGap r p = splitQueries $
+_fastGap r p = splitQueries $
withResultArc (\(Arc s e) -> Arc (sam s + ((s - sam s)/r'))
(sam s + ((e - sam s)/r'))
) $ p {query = f}
@@ -573,11 +580,6 @@ isDigital = not . isAnalog
onsetIn :: Arc -> Event a -> Bool
onsetIn a e = isIn a (wholeStart e)
--- | Compares two lists of events, attempting to combine fragmented events in the process
--- for a 'truer' compare
-compareDefrag :: (Ord a) => [Event a] -> [Event a] -> Bool
-compareDefrag as bs = sort (defragParts as) == sort (defragParts bs)
-
-- | Returns a list of events, with any adjacent parts of the same whole combined
defragParts :: Eq a => [Event a] -> [Event a]
defragParts [] = []
@@ -833,3 +835,52 @@ getList _ = Nothing
valueToPattern :: Value -> Pattern Value
valueToPattern (VPattern pat) = pat
valueToPattern v = pure v
+
+--- functions relating to chords/patterns of lists
+
+
+sameDur :: Event a -> Event a -> Bool
+sameDur e1 e2 = (whole e1 == whole e2) && (part e1 == part e2)
+
+groupEventsBy :: Eq a => (Event a -> Event a -> Bool) -> [Event a] -> [[Event a]]
+groupEventsBy _ [] = []
+groupEventsBy f (e:es) = eqs:(groupEventsBy f (es \\ eqs))
+ where eqs = e:[x | x <- es, f e x]
+
+-- assumes that all events in the list have same whole/part
+collectEvent :: [Event a] -> Maybe (Event [a])
+collectEvent [] = Nothing
+collectEvent l@(e:_) = Just $ e {context = con, value = vs}
+ where con = unionC $ map context l
+ vs = map value l
+ unionC [] = Context []
+ unionC ((Context is):cs) = Context (is ++ iss)
+ where Context iss = unionC cs
+
+collectEventsBy :: Eq a => (Event a -> Event a -> Bool) -> [Event a] -> [Event [a]]
+collectEventsBy f es = remNo $ map collectEvent (groupEventsBy f es)
+ where
+ remNo [] = []
+ remNo (Nothing:cs) = remNo cs
+ remNo ((Just c):cs) = c : (remNo cs)
+
+-- | collects all events satisfying the same constraint into a list
+collectBy :: Eq a => (Event a -> Event a -> Bool) -> Pattern a -> Pattern [a]
+collectBy f = withEvents (collectEventsBy f)
+
+-- | collects all events occuring at the exact same time into a list
+collect :: Eq a => Pattern a -> Pattern [a]
+collect = collectBy sameDur
+
+uncollectEvent :: Event [a] -> [Event a]
+uncollectEvent e = [e {value = (value e)!!i, context = resolveContext i (context e)} | i <-[0..length (value e) - 1]]
+ where resolveContext i (Context xs) = case length xs <= i of
+ True -> Context []
+ False -> Context [xs!!i]
+
+uncollectEvents :: [Event [a]] -> [Event a]
+uncollectEvents = concatMap uncollectEvent
+
+-- | merges all values in a list into one pattern by stacking the values
+uncollect :: Pattern [a] -> Pattern a
+uncollect = withEvents uncollectEvents
diff --git a/src/Sound/Tidal/Safe/Context.hs b/src/Sound/Tidal/Safe/Context.hs
index 178eb252a..efc2421fe 100644
--- a/src/Sound/Tidal/Safe/Context.hs
+++ b/src/Sound/Tidal/Safe/Context.hs
@@ -51,7 +51,6 @@ module Sound.Tidal.Safe.Context
where
import Data.Ratio as C
-import Sound.Tidal.Carabiner as C
import Sound.Tidal.Config as C
import Sound.Tidal.Control as C
import Sound.Tidal.Core as C
@@ -65,7 +64,6 @@ import Sound.Tidal.Stream
-- import Sound.Tidal.Transition as C
import Sound.Tidal.UI as C
import Sound.Tidal.Version as C
-import Sound.Tidal.EspGrid as C
import qualified Sound.Tidal.Context as C
import Sound.Tidal.Context
diff --git a/src/Sound/Tidal/Stream.hs b/src/Sound/Tidal/Stream.hs
index 95ac40279..db9577e2d 100644
--- a/src/Sound/Tidal/Stream.hs
+++ b/src/Sound/Tidal/Stream.hs
@@ -2,7 +2,7 @@
{-# OPTIONS_GHC -fno-warn-missing-fields #-}
{-# language DeriveGeneric, StandaloneDeriving #-}
-module Sound.Tidal.Stream where
+module Sound.Tidal.Stream (module Sound.Tidal.Stream) where
{-
Stream.hs - Tidal's thingie for turning patterns into OSC streams
@@ -26,9 +26,12 @@ import Control.Applicative ((<|>))
import Control.Concurrent.MVar
import Control.Concurrent
import Control.Monad (forM_, when)
+import Data.Coerce (coerce)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromJust, fromMaybe, catMaybes, isJust)
import qualified Control.Exception as E
+import Foreign
+import Foreign.C.Types
import System.IO (hPutStrLn, stderr)
import qualified Sound.OSC.FD as O
@@ -37,6 +40,7 @@ import qualified Network.Socket as N
import Sound.Tidal.Config
import Sound.Tidal.Core (stack, silence, (#))
import Sound.Tidal.ID
+import qualified Sound.Tidal.Link as Link
import Sound.Tidal.Params (pS)
import Sound.Tidal.Pattern
import qualified Sound.Tidal.Tempo as T
@@ -49,19 +53,20 @@ import Data.Word (Word8)
import Sound.Tidal.Version
+import Sound.Tidal.StreamTypes as Sound.Tidal.Stream
+
data Stream = Stream {sConfig :: Config,
sBusses :: MVar [Int],
sStateMV :: MVar ValueMap,
-- sOutput :: MVar ControlPattern,
+ sLink :: Link.AbletonLink,
sListen :: Maybe O.UDP,
sPMapMV :: MVar PlayMap,
- sTempoMV :: MVar T.Tempo,
+ sActionsMV :: MVar [T.TempoAction],
sGlobalFMV :: MVar (ControlPattern -> ControlPattern),
sCxs :: [Cx]
}
-type PatId = String
-
data Cx = Cx {cxTarget :: Target,
cxUDP :: O.UDP,
cxOSCs :: [OSC],
@@ -99,15 +104,18 @@ data OSC = OSC {path :: String,
| OSCContext {path :: String}
deriving Show
-data PlayState = PlayState {pattern :: ControlPattern,
- mute :: Bool,
- solo :: Bool,
- history :: [ControlPattern]
- }
- deriving Show
-
-type PlayMap = Map.Map PatId PlayState
-
+data ProcessedEvent =
+ ProcessedEvent {
+ peHasOnset :: Bool,
+ peEvent :: Event ValueMap,
+ peCps :: Link.BPM,
+ peDelta :: Link.Micros,
+ peCycle :: Time,
+ peOnWholeOrPart :: Link.Micros,
+ peOnWholeOrPartOsc :: O.Time,
+ peOnPart :: Link.Micros,
+ peOnPartOsc :: O.Time
+ }
sDefault :: String -> Maybe Value
sDefault x = Just $ VS x
@@ -186,6 +194,12 @@ dirtShape = OSC "/play" $ ArgList [("cps", fDefault 0),
-- ("id", iDefault 0)
]
+defaultCps :: O.Time
+defaultCps = 0.5625
+
+-- Start an instance of Tidal
+-- Spawns a thread within Tempo that acts as the clock
+-- Spawns a thread that listens to and acts on OSC control messages
startStream :: Config -> [(Target, [OSC])] -> IO Stream
startStream config oscmap
= do sMapMV <- newMVar Map.empty
@@ -193,6 +207,7 @@ startStream config oscmap
bussesMV <- newMVar []
globalFMV <- newMVar id
tempoMV <- newEmptyMVar
+ actionsMV <- newEmptyMVar
tidal_status_string >>= verbose config
verbose config $ "Listening for external controls on " ++ cCtrlAddr config ++ ":" ++ show (cCtrlPort config)
@@ -208,17 +223,27 @@ startStream config oscmap
) (oAddress target) (oPort target)
return $ Cx {cxUDP = u, cxAddr = remote_addr, cxBusAddr = remote_bus_addr, cxTarget = target, cxOSCs = os}
) oscmap
+ let bpm = (coerce defaultCps) * 60 * (cCyclesPerBeat config)
+ abletonLink <- Link.create bpm
let stream = Stream {sConfig = config,
sBusses = bussesMV,
sStateMV = sMapMV,
+ sLink = abletonLink,
sListen = listen,
sPMapMV = pMapMV,
- sTempoMV = tempoMV,
+ sActionsMV = actionsMV,
sGlobalFMV = globalFMV,
sCxs = cxs
}
sendHandshakes stream
- _ <- T.clocked config tempoMV $ onTick stream
+ let ac = T.ActionHandler {
+ T.onTick = onTick stream,
+ T.onSingleTick = onSingleTick stream,
+ T.updatePattern = updatePattern stream
+ }
+ -- Spawn a thread that acts as the clock
+ _ <- T.clocked config sMapMV pMapMV actionsMV ac abletonLink
+ -- Spawn a thread to handle OSC control messages
_ <- forkIO $ ctrlResponder 0 config stream
return stream
@@ -250,6 +275,7 @@ resolve host port = do let hints = N.defaultHints { N.addrSocketType = N.Stream
addr:_ <- N.getAddrInfo (Just hints) (Just host) (Just port)
return addr
+-- Start an instance of Tidal with superdirt OSC
startTidal :: Target -> Config -> IO Stream
startTidal target config = startStream config [(target, [superdirtShape])]
@@ -314,105 +340,151 @@ playStack pMap = stack $ map pattern active
else not (mute pState)
) $ Map.elems pMap
-toOSC :: Double -> [Int] -> Event ValueMap -> T.Tempo -> OSC -> [(Double, Bool, O.Message)]
-toOSC latency busses e tempo osc@(OSC _ _)
+toOSC :: [Int] -> ProcessedEvent -> OSC -> [(Double, Bool, O.Message)]
+toOSC busses pe osc@(OSC _ _)
= catMaybes (playmsg:busmsgs)
- where (playmap, busmap) = Map.partitionWithKey (\k _ -> null k || head k /= '^') $ value e
- -- swap in bus ids where needed
- playmap' = Map.union (Map.mapKeys tail $ Map.map (\(VI i) -> VS ('c':(show $ toBus i))) busmap) playmap
- addExtra = Map.union playmap' extra
- playmsg | eventHasOnset e = do vs <- toData osc (e {value = addExtra})
- mungedPath <- substitutePath (path osc) playmap'
- return (ts,
- False, -- bus message ?
- O.Message mungedPath vs
- )
- | otherwise = Nothing
- toBus n | null busses = n
- | otherwise = busses !!! n
- busmsgs = map
- (\(('^':k), (VI b)) -> do v <- Map.lookup k playmap
- return $ (tsPart,
- True, -- bus message ?
- O.Message "/c_set" [O.int32 b, toDatum v]
- )
- )
- (Map.toList busmap)
- onPart = sched tempo $ start $ part e
- on = sched tempo $ start $ wholeOrPart e
- off = sched tempo $ stop $ wholeOrPart e
- delta = off - on
- -- If there is already cps in the event, the union will preserve that.
- extra = Map.fromList [("cps", (VF (T.cps tempo))),
- ("delta", VF delta),
- ("cycle", VF (fromRational $ start $ wholeOrPart e))
- ]
- nudge = fromJust $ getF $ fromMaybe (VF 0) $ Map.lookup "nudge" $ playmap
- ts = on + nudge + latency
- tsPart = onPart + nudge + latency
-
-toOSC latency _ e tempo (OSCContext oscpath)
- = map cToM $ contextPosition $ context e
+ -- playmap is a ValueMap where the keys don't start with ^ and are not ""
+ -- busmap is a ValueMap containing the rest of the keys from the event value
+ -- The partition is performed in order to have special handling of bus ids.
+ where
+ (playmap, busmap) = Map.partitionWithKey (\k _ -> null k || head k /= '^') $ val pe
+ -- Map in bus ids where needed.
+ --
+ -- Bus ids are integers
+ -- If busses is empty, the ids to send are directly contained in the the values of the busmap.
+ -- Otherwise, the ids to send are contained in busses at the indices of the values of the busmap.
+ -- Both cases require that the values of the busmap are only ever integers,
+ -- that is, they are Values with constructor VI
+ -- (but perhaps we should explicitly crash with an error message if it contains something else?).
+ -- Map.mapKeys tail is used to remove ^ from the keys.
+ -- In case (value e) has the key "", we will get a crash here.
+ playmap' = Map.union (Map.mapKeys tail $ Map.map (\(VI i) -> VS ('c':(show $ toBus i))) busmap) playmap
+ toChannelId (VI i) = VS ('c':(show $ toBus i))
+ toChannelId _ = error "All channels IDs should be VI"
+ val = value . peEvent
+ -- Only events that start within the current nowArc are included
+ playmsg | peHasOnset pe = do
+ -- If there is already cps in the event, the union will preserve that.
+ let extra = Map.fromList [("cps", (VF (coerce $! peCps pe))),
+ ("delta", VF (T.addMicrosToOsc (peDelta pe) 0)),
+ ("cycle", VF (fromRational (peCycle pe)))
+ ]
+ addExtra = Map.union playmap' extra
+ ts = (peOnWholeOrPartOsc pe) + nudge -- + latency
+ vs <- toData osc ((peEvent pe) {value = addExtra})
+ mungedPath <- substitutePath (path osc) playmap'
+ return (ts,
+ False, -- bus message ?
+ O.Message mungedPath vs
+ )
+ | otherwise = Nothing
+ toBus n | null busses = n
+ | otherwise = busses !!! n
+ busmsgs = map
+ (\(('^':k), (VI b)) -> do v <- Map.lookup k playmap
+ return $ (tsPart,
+ True, -- bus message ?
+ O.Message "/c_set" [O.int32 b, toDatum v]
+ )
+ )
+ (Map.toList busmap)
+ where
+ tsPart = (peOnPartOsc pe) + nudge -- + latency
+ nudge = fromJust $ getF $ fromMaybe (VF 0) $ Map.lookup "nudge" $ playmap
+toOSC _ pe (OSCContext oscpath)
+ = map cToM $ contextPosition $ context $ peEvent pe
where cToM :: ((Int,Int),(Int,Int)) -> (Double, Bool, O.Message)
cToM ((x, y), (x',y')) = (ts,
False, -- bus message ?
- O.Message oscpath $ (O.string ident):(O.float delta):(O.float cyc):(map O.int32 [x,y,x',y'])
+ O.Message oscpath $ (O.string ident):(O.float (peDelta pe)):(O.float cyc):(map O.int32 [x,y,x',y'])
)
- on = sched tempo $ start $ wholeOrPart e
- off = sched tempo $ stop $ wholeOrPart e
- delta = off - on
cyc :: Double
- cyc = fromRational $ start $ wholeOrPart e
- nudge = fromMaybe 0 $ Map.lookup "nudge" (value e) >>= getF
- ident = fromMaybe "unknown" $ Map.lookup "_id_" (value e) >>= getS
- ts = on + nudge + latency
-
-doCps :: MVar T.Tempo -> (Double, Maybe Value) -> IO ()
-doCps tempoMV (d, Just (VF cps)) =
- do _ <- forkIO $ do threadDelay $ floor $ d * 1000000
- -- hack to stop things from stopping !
- -- TODO is this still needed?
- _ <- T.setCps tempoMV (max 0.00001 cps)
- return ()
- return ()
-doCps _ _ = return ()
-
-onTick :: Stream -> T.State -> IO ()
-onTick stream st
- = do doTick False stream st
-
-processCps :: T.Tempo -> [Event ValueMap] -> ([(T.Tempo, Event ValueMap)], T.Tempo)
-processCps t [] = ([], t)
--- If an event has a tempo change, that affects the following events..
-processCps t (e:evs) = (((t', e):es'), t'')
- where cps' | eventHasOnset e = do x <- Map.lookup "cps" $ value e
- getF x
- | otherwise = Nothing
- t' = (maybe t (\newCps -> T.changeTempo' t newCps (eventPartStart e)) cps')
- (es', t'') = processCps t' evs
+ cyc = fromRational $ peCycle pe
+ nudge = fromMaybe 0 $ Map.lookup "nudge" (value $ peEvent pe) >>= getF
+ ident = fromMaybe "unknown" $ Map.lookup "_id_" (value $ peEvent pe) >>= getS
+ ts = (peOnWholeOrPartOsc pe) + nudge -- + latency
+
+-- Used for Tempo callback
+updatePattern :: Stream -> ID -> ControlPattern -> IO ()
+updatePattern stream k pat = do
+ let x = queryArc pat (Arc 0 0)
+ pMap <- seq x $ takeMVar (sPMapMV stream)
+ let playState = updatePS $ Map.lookup (fromID k) pMap
+ putMVar (sPMapMV stream) $ Map.insert (fromID k) playState pMap
+ where updatePS (Just playState) = do playState {pattern = pat', history = pat:(history playState)}
+ updatePS Nothing = PlayState pat' False False [pat']
+ pat' = pat # pS "_id_" (pure $ fromID k)
+processCps :: T.LinkOperations -> [Event ValueMap] -> IO [ProcessedEvent]
+processCps ops = mapM processEvent
+ where
+ processEvent :: Event ValueMap -> IO ProcessedEvent
+ processEvent e = do
+ let wope = wholeOrPart e
+ partStartCycle = start $ part e
+ partStartBeat = (T.cyclesToBeat ops) (realToFrac partStartCycle)
+ onCycle = start wope
+ onBeat = (T.cyclesToBeat ops) (realToFrac onCycle)
+ offCycle = stop wope
+ offBeat = (T.cyclesToBeat ops) (realToFrac offCycle)
+ on <- (T.timeAtBeat ops) onBeat
+ onPart <- (T.timeAtBeat ops) partStartBeat
+ when (eventHasOnset e) (do
+ let cps' = Map.lookup "cps" (value e) >>= getF
+ maybe (return ()) (\newCps -> (T.setTempo ops) ((T.cyclesToBeat ops) (newCps * 60)) on) $ coerce cps'
+ )
+ off <- (T.timeAtBeat ops) offBeat
+ bpm <- (T.getTempo ops)
+ let cps = ((T.beatToCycles ops) bpm) / 60
+ let delta = off - on
+ return $! ProcessedEvent {
+ peHasOnset = eventHasOnset e,
+ peEvent = e,
+ peCps = cps,
+ peDelta = delta,
+ peCycle = onCycle,
+ peOnWholeOrPart = on,
+ peOnWholeOrPartOsc = (T.linkToOscTime ops) on,
+ peOnPart = onPart,
+ peOnPartOsc = (T.linkToOscTime ops) onPart
+ }
+
+
+-- streamFirst but with random cycle instead of always first cicle
streamOnce :: Stream -> ControlPattern -> IO ()
streamOnce st p = do i <- getStdRandom $ randomR (0, 8192)
streamFirst st $ rotL (toRational (i :: Int)) p
+-- here let's do modifyMVar_ on actions
streamFirst :: Stream -> ControlPattern -> IO ()
-streamFirst stream pat = do now <- O.time
- tempo <- readMVar (sTempoMV stream)
- pMapMV <- newMVar $ Map.singleton "fake"
- (PlayState {pattern = pat,
- mute = False,
- solo = False,
- history = []
- }
- )
- let cps = T.cps tempo
- state = T.State {T.ticks = 0,
- T.start = now,
- T.nowTimespan = (now, now + (1/cps)),
- T.starting = True, -- really?
- T.nowArc = (Arc 0 1)
- }
- doTick True (stream {sPMapMV = pMapMV}) state
+streamFirst stream pat = modifyMVar_ (sActionsMV stream) (\actions -> return $ (T.SingleTick pat) : actions)
+
+-- Used for Tempo callback
+onTick :: Stream -> TickState -> T.LinkOperations -> ValueMap -> IO ValueMap
+onTick stream st ops s
+ = doTick stream st ops s
+
+-- Used for Tempo callback
+-- Tempo changes will be applied.
+-- However, since the full arc is processed at once and since Link does not support
+-- scheduling, tempo change may affect scheduling of events that happen earlier
+-- in the normal stream (the one handled by onTick).
+onSingleTick :: Stream -> Link.Micros -> T.LinkOperations -> ValueMap -> ControlPattern -> IO ValueMap
+onSingleTick stream now ops s pat = do
+ pMapMV <- newMVar $ Map.singleton "fake"
+ (PlayState {pattern = pat,
+ mute = False,
+ solo = False,
+ history = []
+ }
+ )
+ bpm <- (T.getTempo ops)
+ let cps = realToFrac $ ((T.beatToCycles ops) bpm) / 60
+
+ -- The nowArc is a full cycle
+ let state = TickState {tickArc = (Arc 0 1), tickNudge = 0}
+ doTick (stream {sPMapMV = pMapMV}) state ops s
+
-- | Query the current pattern (contained in argument @stream :: Stream@)
-- for the events in the current arc (contained in argument @st :: T.State@),
@@ -427,62 +499,43 @@ streamFirst stream pat = do now <- O.time
-- this function prints a warning and resets the current pattern
-- to the previous one (or to silence if there isn't one) and continues,
-- because the likely reason is that something is wrong with the current pattern.
-doTick :: Bool -> Stream -> T.State -> IO ()
-doTick fake stream st =
+doTick :: Stream -> TickState -> T.LinkOperations -> ValueMap -> IO ValueMap
+doTick stream st ops sMap =
E.handle (\ (e :: E.SomeException) -> do
hPutStrLn stderr $ "Failed to Stream.doTick: " ++ show e
hPutStrLn stderr $ "Return to previous pattern."
setPreviousPatternOrSilence stream
- ) $
- modifyState $ \(tempo, sMap) -> do
- pMap <- readMVar (sPMapMV stream)
- busses <- readMVar (sBusses stream)
- sGlobalF <- readMVar (sGlobalFMV stream)
- -- putStrLn $ show st
- let config = sConfig stream
- cxs = sCxs stream
- cycleNow = T.timeToCycles tempo $ T.start st
- patstack = sGlobalF $ playStack pMap
- -- If a 'fake' tick, it'll be aligned with cycle zero
- pat | fake = withResultTime (+ cycleNow) patstack
- | otherwise = patstack
- frameEnd = snd $ T.nowTimespan st
- -- add cps to state
- sMap' = Map.insert "_cps" (VF (T.cps tempo)) sMap
- --filterOns = filter eventHasOnset
- extraLatency | fake = 0
- | otherwise = cFrameTimespan config + T.nudged tempo
- -- First the state is used to query the pattern
- es = sortOn (start . part) $ query pat (State {arc = T.nowArc st,
+ return sMap) (do
+ pMap <- readMVar (sPMapMV stream)
+ busses <- readMVar (sBusses stream)
+ sGlobalF <- readMVar (sGlobalFMV stream)
+ bpm <- (T.getTempo ops)
+ let
+ config = sConfig stream
+ cxs = sCxs stream
+ patstack = sGlobalF $ playStack pMap
+ cps = ((T.beatToCycles ops) bpm) / 60
+ sMap' = Map.insert "_cps" (VF $ coerce cps) sMap
+ extraLatency = tickNudge st
+ -- First the state is used to query the pattern
+ es = sortOn (start . part) $ query patstack (State {arc = tickArc st,
controls = sMap'
- }
+ }
)
-- Then it's passed through the events
- (sMap'', es') = resolveState sMap' es
-
- -- TODO onset is calculated in toOSC as well..
- on e tempo'' = (sched tempo'' $ start $ wholeOrPart e)
- (tes, tempo') = processCps tempo $ es'
- forM_ cxs $ \cx@(Cx target _ oscs _ _) -> do
- let latency = oLatency target + extraLatency
- ms = concatMap (\(t, e) ->
- if (fake || (on e t) < frameEnd)
- then concatMap (toOSC latency busses e t) oscs
- else []
- ) tes
- forM_ ms $ \ m -> send (sListen stream) cx m `E.catch` \ (e :: E.SomeException) -> do
- hPutStrLn stderr $ "Failed to send. Is the '" ++ oName target ++ "' target running? " ++ show e
-
- when (tempo /= tempo') $ T.sendTempo tempo'
-
- (tempo', sMap'') `seq` return (tempo', sMap'')
- where modifyState :: ((T.Tempo, ValueMap) -> IO (T.Tempo, ValueMap)) -> IO ()
- modifyState io = E.mask $ \restore -> do
- s <- takeMVar (sStateMV stream)
- t <- takeMVar (sTempoMV stream)
- (t', s') <- restore (io (t, s)) `E.onException` (do {putMVar (sStateMV stream) s; putMVar (sTempoMV stream) t; return ()})
- putMVar (sStateMV stream) s'
- putMVar (sTempoMV stream) t'
+ (sMap'', es') = resolveState sMap' es
+ tes <- processCps ops es'
+ -- For each OSC target
+ forM_ cxs $ \cx@(Cx target _ oscs _ _) -> do
+ -- Latency is configurable per target.
+ -- Latency is only used when sending events live.
+ let latency = oLatency target
+ ms = concatMap (\e -> concatMap (toOSC busses e) oscs) tes
+ -- send the events to the OSC target
+ forM_ ms $ \ m -> (do
+ send (sListen stream) cx latency extraLatency m) `E.catch` \ (e :: E.SomeException) -> do
+ hPutStrLn stderr $ "Failed to send. Is the '" ++ oName target ++ "' target running? " ++ show e
+ sMap'' `seq` return sMap'')
setPreviousPatternOrSilence :: Stream -> IO ()
setPreviousPatternOrSilence stream =
@@ -491,36 +544,35 @@ setPreviousPatternOrSilence stream =
_:p:ps -> pMap { pattern = p, history = p:ps }
_ -> pMap { pattern = silence, history = [silence] }
)
-
-send :: Maybe O.UDP -> Cx -> (Double, Bool, O.Message) -> IO ()
-send listen cx (time, isBusMsg, m)
- | oSchedule target == Pre BundleStamp = sendBndl isBusMsg listen cx $ O.Bundle time [m]
+
+-- send has three modes:
+-- Send events early using timestamp in the OSC bundle - used by Superdirt
+-- Send events early by adding timestamp to the OSC message - used by Dirt
+-- Send events live by delaying the thread
+send :: Maybe O.UDP -> Cx -> Double -> Double -> (Double, Bool, O.Message) -> IO ()
+send listen cx latency extraLatency (time, isBusMsg, m)
+ | oSchedule target == Pre BundleStamp = sendBndl isBusMsg listen cx $ O.Bundle timeWithLatency [m]
| oSchedule target == Pre MessageStamp = sendO isBusMsg listen cx $ addtime m
- | otherwise = do _ <- forkIO $ do now <- O.time
- threadDelay $ floor $ (time - now) * 1000000
+ | otherwise = do _ <- forkOS $ do now <- O.time
+ threadDelay $ floor $ (timeWithLatency - now) * 1000000
sendO isBusMsg listen cx m
return ()
where addtime (O.Message mpath params) = O.Message mpath ((O.int32 sec):((O.int32 usec):params))
- ut = O.ntpr_to_ut time
+ ut = O.ntpr_to_ut timeWithLatency
sec :: Int
sec = floor ut
usec :: Int
usec = floor $ 1000000 * (ut - (fromIntegral sec))
target = cxTarget cx
-
-sched :: T.Tempo -> Rational -> Double
-sched tempo c = ((fromRational $ c - (T.atCycle tempo)) / T.cps tempo)
- + (T.atTime tempo)
+ timeWithLatency = time - latency + extraLatency
-- Interaction
streamNudgeAll :: Stream -> Double -> IO ()
-streamNudgeAll s nudge = do tempo <- takeMVar $ sTempoMV s
- putMVar (sTempoMV s) $ tempo {T.nudged = nudge}
+streamNudgeAll s nudge = T.setNudge (sActionsMV s) nudge
streamResetCycles :: Stream -> IO ()
-streamResetCycles s = do _ <- T.resetCycles (sTempoMV s)
- return ()
+streamResetCycles s =T.resetCycles (sActionsMV s)
hasSolo :: Map.Map k PlayState -> Bool
hasSolo = (>= 1) . length . filter solo . Map.elems
@@ -539,25 +591,7 @@ streamList s = do pMap <- readMVar (sPMapMV s)
streamReplace :: Stream -> ID -> ControlPattern -> IO ()
streamReplace s k !pat
- = E.catch (do let x = queryArc pat (Arc 0 0)
- tempo <- readMVar $ sTempoMV s
- input <- takeMVar $ sStateMV s
- -- put pattern id and change time in control input
- now <- O.time
- let cyc = T.timeToCycles tempo now
- putMVar (sStateMV s) $
- Map.insert ("_t_all") (VR cyc) $ Map.insert ("_t_" ++ fromID k) (VR cyc) input
- -- update the pattern itself
- pMap <- seq x $ takeMVar $ sPMapMV s
- let playState = updatePS $ Map.lookup (fromID k) pMap
- putMVar (sPMapMV s) $ Map.insert (fromID k) playState pMap
- return ()
- )
- (\(e :: E.SomeException) -> hPutStrLn stderr $ "Error in pattern: " ++ show e
- )
- where updatePS (Just playState) = do playState {pattern = pat', history = pat:(history playState)}
- updatePS Nothing = PlayState pat' False False [pat']
- pat' = pat # pS "_id_" (pure $ fromID k)
+ = modifyMVar_ (sActionsMV s) (\actions -> return $ (T.StreamReplace k pat) : actions)
streamMute :: Stream -> ID -> IO ()
streamMute s k = withPatIds s [k] (\x -> x {mute = True})
@@ -638,6 +672,7 @@ openListener c
catchAny :: IO a -> (E.SomeException -> IO a) -> IO a
catchAny = E.catch
+-- Listen to and act on OSC control messages
ctrlResponder :: Int -> Config -> Stream -> IO ()
ctrlResponder waits c (stream@(Stream {sListen = Just sock}))
= do ms <- recvMessagesTimeout 2 sock
@@ -699,20 +734,24 @@ ctrlResponder waits c (stream@(Stream {sListen = Just sock}))
withID _ _ = return ()
ctrlResponder _ _ _ = return ()
-
verbose :: Config -> String -> IO ()
verbose c s = when (cVerbose c) $ putStrLn s
recvMessagesTimeout :: (O.Transport t) => Double -> t -> IO [O.Message]
recvMessagesTimeout n sock = fmap (maybe [] O.packetMessages) $ O.recvPacketTimeout n sock
-
-streamGetcps :: Stream -> IO O.Time
-streamGetcps s = do tempo <- readMVar $ sTempoMV s
- return $ T.cps tempo
+streamGetcps :: Stream -> IO Double
+streamGetcps s = do
+ let config = sConfig s
+ now <- Link.clock (sLink s)
+ ss <- Link.createAndCaptureAppSessionState (sLink s)
+ bpm <- Link.getTempo ss
+ return $! coerce $ bpm / (cCyclesPerBeat config) / 60
streamGetnow :: Stream -> IO Double
-streamGetnow s = do tempo <- readMVar $ sTempoMV s
- now <- O.time
- return $ fromRational $ T.timeToCycles tempo now
-
+streamGetnow s = do
+ let config = sConfig s
+ ss <- Link.createAndCaptureAppSessionState (sLink s)
+ now <- Link.clock (sLink s)
+ beat <- Link.beatAtTime ss now (cQuantum config)
+ return $! coerce $ beat / (cCyclesPerBeat config)
diff --git a/src/Sound/Tidal/StreamTypes.hs b/src/Sound/Tidal/StreamTypes.hs
new file mode 100644
index 000000000..6088b9d32
--- /dev/null
+++ b/src/Sound/Tidal/StreamTypes.hs
@@ -0,0 +1,22 @@
+module Sound.Tidal.StreamTypes where
+
+import qualified Data.Map.Strict as Map
+import Sound.Tidal.Pattern
+import Sound.Tidal.Show ()
+import qualified Sound.Tidal.Link as Link
+
+data PlayState = PlayState {pattern :: ControlPattern,
+ mute :: Bool,
+ solo :: Bool,
+ history :: [ControlPattern]
+ }
+ deriving Show
+
+type PatId = String
+type PlayMap = Map.Map PatId PlayState
+
+data TickState = TickState {
+ tickArc :: Arc,
+ tickNudge :: Double
+ }
+ deriving Show
diff --git a/src/Sound/Tidal/Tempo.hs b/src/Sound/Tidal/Tempo.hs
index ca3c82976..cb32a83d6 100644
--- a/src/Sound/Tidal/Tempo.hs
+++ b/src/Sound/Tidal/Tempo.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE ConstraintKinds, GeneralizedNewtypeDeriving, FlexibleContexts, ScopedTypeVariables, BangPatterns #-}
{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns -fno-warn-orphans #-}
@@ -6,13 +7,21 @@ module Sound.Tidal.Tempo where
import Control.Concurrent.MVar
import qualified Sound.Tidal.Pattern as P
import qualified Sound.OSC.FD as O
-import qualified Network.Socket as N
import Control.Concurrent (forkIO, ThreadId, threadDelay)
-import Control.Monad (forever, when, foldM)
-import Data.List (nub)
+import Control.Monad (when)
+import qualified Data.Map.Strict as Map
import qualified Control.Exception as E
+import Sound.Tidal.ID
import Sound.Tidal.Config
import Sound.Tidal.Utils (writeError)
+import qualified Sound.Tidal.Link as Link
+import Foreign.C.Types (CDouble(..))
+import Data.Coerce (coerce)
+import System.IO (hPutStrLn, stderr)
+import Data.Int(Int64)
+
+import Sound.Tidal.StreamTypes
+import Sound.Tidal.Core (silence)
{-
Tempo.hs - Tidal's scheduler
@@ -35,210 +44,259 @@ import Sound.Tidal.Utils (writeError)
instance Show O.UDP where
show _ = "-unshowable-"
-data Tempo = Tempo {atTime :: O.Time,
- atCycle :: Rational,
- cps :: O.Time,
- paused :: Bool,
- nudged :: Double,
- localUDP :: O.UDP,
- remoteAddr :: N.SockAddr,
- synched :: Bool
- }
- deriving Show
+type TransitionMapper = P.Time -> [P.ControlPattern] -> P.ControlPattern
-instance Eq Tempo where
- (==) t t' = and [(atTime t) == (atTime t'),
- (atCycle t) == (atCycle t'),
- (cps t) == (cps t'),
- (paused t) == (paused t'),
- (nudged t) == (nudged t')
- ]
-
-data State = State {ticks :: Int,
- start :: O.Time,
- nowTimespan :: (O.Time, O.Time),
- nowArc :: P.Arc,
- starting :: Bool
+data TempoAction =
+ ResetCycles
+ | SingleTick P.ControlPattern
+ | SetNudge Double
+ | StreamReplace ID P.ControlPattern
+ | Transition Bool TransitionMapper ID P.ControlPattern
+
+data State = State {ticks :: Int64,
+ start :: Link.Micros,
+ nowEnd :: Link.Micros,
+ nowArc :: P.Arc,
+ nudged :: Double
}
deriving Show
-changeTempo :: MVar Tempo -> (O.Time -> Tempo -> Tempo) -> IO Tempo
-changeTempo tempoMV f = do t <- O.time
- tempo <- takeMVar tempoMV
- let tempo' = f t tempo
- sendTempo tempo'
- putMVar tempoMV tempo'
- return tempo'
-
-changeTempo' :: Tempo -> O.Time -> Rational -> Tempo
-changeTempo' tempo newCps cyc = tempo {atTime = cyclesToTime tempo cyc,
- cps = newCps,
- atCycle = cyc
- }
-
-resetCycles :: MVar Tempo -> IO Tempo
-resetCycles tempoMV = changeTempo tempoMV (\t tempo -> tempo {atTime = t, atCycle = 0})
-
-setCps :: MVar Tempo -> O.Time -> IO Tempo
-setCps tempoMV newCps = changeTempo tempoMV (\t tempo -> tempo {atTime = t,
- atCycle = timeToCycles tempo t,
- cps = newCps
- })
-
-defaultCps :: O.Time
-defaultCps = 0.5625
-
-defaultTempo :: O.Time -> O.UDP -> N.SockAddr -> Tempo
-defaultTempo t local remote = Tempo {atTime = t,
- atCycle = 0,
- cps = defaultCps,
- paused = False,
- nudged = 0,
- localUDP = local,
- remoteAddr = remote,
- synched = False
- }
-
--- | Returns the given time in terms of
--- cycles relative to metrical grid of a given Tempo
-timeToCycles :: Tempo -> O.Time -> Rational
-timeToCycles tempo t = atCycle tempo + toRational cycleDelta
- where delta = t - atTime tempo
- cycleDelta = realToFrac (cps tempo) * delta
-
-cyclesToTime :: Tempo -> Rational -> O.Time
-cyclesToTime tempo cyc = atTime tempo + fromRational timeDelta
- where cycleDelta = cyc - atCycle tempo
- timeDelta = cycleDelta / toRational (cps tempo)
+data ActionHandler =
+ ActionHandler {
+ onTick :: TickState -> LinkOperations -> P.ValueMap -> IO P.ValueMap,
+ onSingleTick :: Link.Micros -> LinkOperations -> P.ValueMap -> P.ControlPattern -> IO P.ValueMap,
+ updatePattern :: ID -> P.ControlPattern -> IO ()
+ }
-{-
-getCurrentCycle :: MVar Tempo -> IO Rational
-getCurrentCycle t = (readMVar t) >>= (cyclesNow) >>= (return . toRational)
--}
+data LinkOperations =
+ LinkOperations {
+ timeAtBeat :: Link.Beat -> IO Link.Micros,
+ timeToCycles :: Link.Micros -> IO P.Time,
+ getTempo :: IO Link.BPM,
+ setTempo :: Link.BPM -> Link.Micros -> IO (),
+ linkToOscTime :: Link.Micros -> O.Time,
+ beatToCycles :: CDouble -> CDouble,
+ cyclesToBeat :: CDouble -> CDouble
+ }
-clocked :: Config -> MVar Tempo -> (State -> IO ()) -> IO [ThreadId]
-clocked config tempoMV callback
- = do s <- O.time
- -- TODO - do something with thread id
- _ <- serverListen config
- listenTid <- clientListen config tempoMV s
- let st = State {ticks = 0,
- start = s,
- nowTimespan = (s, s + frameTimespan),
+resetCycles :: MVar [TempoAction] -> IO ()
+resetCycles actionsMV = modifyMVar_ actionsMV (\actions -> return $ ResetCycles : actions)
+
+setNudge :: MVar [TempoAction] -> Double -> IO ()
+setNudge actionsMV nudge = modifyMVar_ actionsMV (\actions -> return $ SetNudge nudge : actions)
+
+timeToCycles' :: Config -> Link.SessionState -> Link.Micros -> IO P.Time
+timeToCycles' config ss time = do
+ beat <- Link.beatAtTime ss time (cQuantum config)
+ return $! (toRational beat) / (toRational (cCyclesPerBeat config))
+
+cyclesToTime :: Config -> Link.SessionState -> P.Time -> IO Link.Micros
+cyclesToTime config ss cyc = do
+ let beat = (fromRational cyc) * (cCyclesPerBeat config)
+ Link.timeAtBeat ss beat (cQuantum config)
+
+addMicrosToOsc :: Link.Micros -> O.Time -> O.Time
+addMicrosToOsc m t = ((fromIntegral m) / 1000000) + t
+
+-- clocked assumes tempoMV is empty
+clocked :: Config -> MVar P.ValueMap -> MVar PlayMap -> MVar [TempoAction] -> ActionHandler -> Link.AbletonLink -> IO [ThreadId]
+clocked config stateMV mapMV actionsMV ac abletonLink
+ = do -- TODO - do something with thread id
+ clockTid <- forkIO $ loopInit
+ return $! [clockTid]
+ where frameTimespan :: Link.Micros
+ frameTimespan = round $ (cFrameTimespan config) * 1000000
+ quantum :: CDouble
+ quantum = cQuantum config
+ cyclesPerBeat :: CDouble
+ cyclesPerBeat = cCyclesPerBeat config
+ loopInit :: IO a
+ loopInit =
+ do
+ when (cEnableLink config) $ Link.enable abletonLink
+ sessionState <- Link.createAndCaptureAppSessionState abletonLink
+ now <- Link.clock abletonLink
+ let startAt = now + processAhead
+ Link.requestBeatAtTime sessionState 0 startAt quantum
+ Link.commitAppSessionState abletonLink sessionState
+ putMVar actionsMV []
+ let st = State {ticks = 0,
+ start = now,
+ nowEnd = logicalTime now 1,
nowArc = P.Arc 0 0,
- starting = True
+ nudged = 0
}
- clockTid <- forkIO $ loop st
- return [listenTid, clockTid]
- where frameTimespan :: Double
- frameTimespan = cFrameTimespan config
- loop st =
- do -- putStrLn $ show $ nowArc ts
- tempo <- readMVar tempoMV
- t <- O.time
- let logicalT ticks' = start st + fromIntegral ticks' * frameTimespan
- logicalNow = logicalT $ ticks st + 1
- -- Wait maximum of two frames
- delta = min (frameTimespan * 2) (logicalNow - t)
- e = timeToCycles tempo logicalNow
- s = if starting st && synched tempo
- then timeToCycles tempo (logicalT $ ticks st)
- else P.stop $ nowArc st
- when (t < logicalNow) $ threadDelay (floor $ delta * 1000000)
- t' <- O.time
- let actualTick = floor $ (t' - start st) / frameTimespan
- -- reset ticks if ahead/behind by skipTicks or more
- ahead = abs (actualTick - ticks st) > cSkipTicks config
- newTick | ahead = actualTick
- | otherwise = ticks st + 1
- st' = st {ticks = newTick,
- nowArc = P.Arc s e,
- nowTimespan = (logicalNow, logicalNow + frameTimespan),
- starting = not (synched tempo)
- }
- when ahead $ writeError $ "skip: " ++ show (actualTick - ticks st)
- callback st'
- {-putStrLn ("actual tick: " ++ show actualTick
- ++ " old tick: " ++ show (ticks st)
- ++ " new tick: " ++ show newTick
- )-}
- loop st'
-
-clientListen :: Config -> MVar Tempo -> O.Time -> IO ThreadId
-clientListen config tempoMV s =
- do -- Listen on random port
- let tempoClientPort = cTempoClientPort config
- hostname = cTempoAddr config
- port = cTempoPort config
- (remote_addr:_) <- N.getAddrInfo Nothing (Just hostname) Nothing
- local <- O.udpServer "0.0.0.0" tempoClientPort
- let (N.SockAddrInet _ a) = N.addrAddress remote_addr
- remote = N.SockAddrInet (fromIntegral port) a
- t = defaultTempo s local remote
- putMVar tempoMV t
- -- Send to clock port from same port that's listened to
- O.sendTo local (O.p_message "/hello" []) remote
- -- Make tempo mvar
- -- Listen to tempo changes
- forkIO $ listenTempo local tempoMV
-
-sendTempo :: Tempo -> IO ()
-sendTempo tempo = O.sendTo (localUDP tempo) (O.p_bundle (atTime tempo) [m]) (remoteAddr tempo)
- where m = O.Message "/transmit/cps/cycle" [O.Float $ fromRational $ atCycle tempo,
- O.Float $ realToFrac $ cps tempo,
- O.Int32 $ if paused tempo then 1 else 0
- ]
-
-listenTempo :: O.UDP -> MVar Tempo -> IO ()
-listenTempo udp tempoMV = forever $ do pkt <- O.recvPacket udp
- act Nothing pkt
- return ()
- where act _ (O.Packet_Bundle (O.Bundle ts ms)) = mapM_ (act (Just ts) . O.Packet_Message) ms
- act (Just ts) (O.Packet_Message (O.Message "/cps/cycle" [O.Float atCycle',
- O.Float cps',
- O.Int32 paused'
- ]
- )
- ) =
- do tempo <- takeMVar tempoMV
- putMVar tempoMV $ tempo {atTime = ts,
- atCycle = realToFrac atCycle',
- cps = realToFrac cps',
- paused = paused' == 1,
- synched = True
- }
- act _ pkt = writeError $ "Unknown packet (client): " ++ show pkt
-
-serverListen :: Config -> IO (Maybe ThreadId)
-serverListen config = catchAny run (\_ -> return Nothing) -- probably just already running)
- where run = do let port = cTempoPort config
- -- iNADDR_ANY deprecated - what's the right way to do this?
- udp <- O.udpServer "0.0.0.0" port
- cpsMessage <- defaultCpsMessage
- tid <- forkIO $ loop udp ([], cpsMessage)
- return $ Just tid
- loop udp (cs, msg) = do (pkt,c) <- O.recvFrom udp
- (cs', msg') <- act udp c Nothing (cs,msg) pkt
- loop udp (cs', msg')
- act :: O.UDP -> N.SockAddr -> Maybe O.Time -> ([N.SockAddr], O.Packet) -> O.Packet -> IO ([N.SockAddr], O.Packet)
- act udp c _ (cs,msg) (O.Packet_Bundle (O.Bundle ts ms)) = foldM (act udp c (Just ts)) (cs,msg) $ map O.Packet_Message ms
- act udp c _ (cs,msg) (O.Packet_Message (O.Message "/hello" []))
- = do O.sendTo udp msg c
- return (nub (c:cs),msg)
- act udp _ (Just ts) (cs,_) (O.Packet_Message (O.Message "/transmit/cps/cycle" params)) =
- do let path' = "/cps/cycle"
- msg' = O.p_bundle ts [O.Message path' params]
- mapM_ (O.sendTo udp msg') cs
- return (cs, msg')
- act _ x _ (cs,msg) pkt = do writeError $ "Unknown packet (serv): " ++ show pkt ++ " / " ++ show x
- return (cs,msg)
- catchAny :: IO a -> (E.SomeException -> IO a) -> IO a
- catchAny = E.catch
- defaultCpsMessage = do ts <- O.time
- return $ O.p_bundle ts [O.Message "/cps/cycle" [O.Float 0,
- O.Float $ realToFrac defaultCps,
- O.Int32 0
- ]
- ]
-
+ checkArc $! st
+ -- Time is processed at a fixed rate according to configuration
+ -- logicalTime gives the time when a tick starts based on when
+ -- processing first started.
+ logicalTime :: Link.Micros -> Int64 -> Link.Micros
+ logicalTime startTime ticks' = startTime + ticks' * frameTimespan
+ -- tick moves the logical time forward or recalculates the ticks in case
+ -- the logical time is out of sync with Link time.
+ -- tick delays the thread when logical time is ahead of Link time.
+ tick :: State -> IO a
+ tick st = do
+ now <- Link.clock abletonLink
+ let preferredNewTick = ticks st + 1
+ logicalNow = logicalTime (start st) preferredNewTick
+ aheadOfNow = now + processAhead
+ actualTick = (aheadOfNow - start st) `div` frameTimespan
+ drifted = abs (actualTick - preferredNewTick) > cSkipTicks config
+ newTick | drifted = actualTick
+ | otherwise = preferredNewTick
+ st' = st {ticks = newTick}
+ delta = min frameTimespan (logicalNow - aheadOfNow)
+ if drifted
+ then writeError $ "skip: " ++ (show (actualTick - ticks st))
+ else when (delta > 0) $ threadDelay $ fromIntegral delta
+ checkArc st'
+ -- The reference time Link uses,
+ -- is the time the audio for a certain beat hits the speaker.
+ -- Processing of the nowArc should happen early enough for
+ -- all events in the nowArc to hit the speaker, but not too early.
+ -- Processing thus needs to happen a short while before the start
+ -- of nowArc. How far ahead is controlled by cProcessAhead.
+ processAhead :: Link.Micros
+ processAhead = round $ (cProcessAhead config) * 1000000
+ checkArc :: State -> IO a
+ checkArc st = do
+ actions <- swapMVar actionsMV []
+ st' <- processActions st actions
+ let logicalEnd = logicalTime (start st') $ ticks st' + 1
+ nextArcStartCycle = P.stop $ nowArc st'
+ ss <- Link.createAndCaptureAppSessionState abletonLink
+ arcStartTime <- cyclesToTime config ss nextArcStartCycle
+ Link.destroySessionState ss
+ if (arcStartTime < logicalEnd)
+ then processArc st'
+ else tick st'
+ processArc :: State -> IO a
+ processArc st =
+ do
+ streamState <- takeMVar stateMV
+ let logicalEnd = logicalTime (start st) $ ticks st + 1
+ startCycle = P.stop $ nowArc st
+ sessionState <- Link.createAndCaptureAppSessionState abletonLink
+ endCycle <- timeToCycles' config sessionState logicalEnd
+ let st' = st {nowArc = P.Arc startCycle endCycle,
+ nowEnd = logicalEnd
+ }
+ nowOsc <- O.time
+ nowLink <- Link.clock abletonLink
+ let ops = LinkOperations {
+ timeAtBeat = \beat -> Link.timeAtBeat sessionState beat quantum ,
+ timeToCycles = timeToCycles' config sessionState,
+ getTempo = Link.getTempo sessionState,
+ setTempo = Link.setTempo sessionState,
+ linkToOscTime = \lt -> addMicrosToOsc (lt - nowLink) nowOsc,
+ beatToCycles = btc,
+ cyclesToBeat = ctb
+ }
+ let state = TickState {
+ tickArc = nowArc st',
+ tickNudge = nudged st'
+ }
+ streamState' <- (onTick ac) state ops streamState
+ Link.commitAndDestroyAppSessionState abletonLink sessionState
+ putMVar stateMV streamState'
+ tick st'
+ btc :: CDouble -> CDouble
+ btc beat = beat / cyclesPerBeat
+ ctb :: CDouble -> CDouble
+ ctb cyc = cyc * cyclesPerBeat
+ processActions :: State -> [TempoAction] -> IO State
+ processActions st [] = return $! st
+ processActions st actions = do
+ streamState <- takeMVar stateMV
+ (st', streamState') <- handleActions st actions streamState
+ putMVar stateMV streamState'
+ return $! st'
+ handleActions :: State -> [TempoAction] -> P.ValueMap -> IO (State, P.ValueMap)
+ handleActions st [] streamState = return (st, streamState)
+ handleActions st (ResetCycles : otherActions) streamState =
+ do
+ (st', streamState') <- handleActions st otherActions streamState
+ sessionState <- Link.createAndCaptureAppSessionState abletonLink
+ let logicalEnd = logicalTime (start st') $ ticks st' + 1
+ st'' = st' {
+ nowArc = P.Arc 0 0,
+ nowEnd = logicalEnd + frameTimespan
+ }
+ now <- Link.clock abletonLink
+ Link.requestBeatAtTime sessionState 0 now quantum
+ Link.commitAndDestroyAppSessionState abletonLink sessionState
+ return (st'', streamState')
+ handleActions st (SingleTick pat : otherActions) streamState =
+ do
+ (st', streamState') <- handleActions st otherActions streamState
+ -- onSingleTick assumes it runs at beat 0.
+ -- The best way to achieve that is to use forceBeatAtTime.
+ -- But using forceBeatAtTime means we can not commit its session state.
+ -- Another session state, which we will commit,
+ -- is introduced to keep track of tempo changes.
+ sessionState <- Link.createAndCaptureAppSessionState abletonLink
+ zeroedSessionState <- Link.createAndCaptureAppSessionState abletonLink
+ nowOsc <- O.time
+ nowLink <- Link.clock abletonLink
+ Link.forceBeatAtTime zeroedSessionState 0 (nowLink + processAhead) quantum
+ let ops = LinkOperations {
+ timeAtBeat = \beat -> Link.timeAtBeat zeroedSessionState beat quantum,
+ timeToCycles = timeToCycles' config zeroedSessionState,
+ getTempo = Link.getTempo zeroedSessionState,
+ setTempo = \bpm micros ->
+ Link.setTempo zeroedSessionState bpm micros >>
+ Link.setTempo sessionState bpm micros,
+ linkToOscTime = \lt -> addMicrosToOsc (lt - nowLink) nowOsc,
+ beatToCycles = btc,
+ cyclesToBeat = ctb
+ }
+ streamState'' <- (onSingleTick ac) nowLink ops streamState' pat
+ Link.commitAndDestroyAppSessionState abletonLink sessionState
+ Link.destroySessionState zeroedSessionState
+ return (st', streamState'')
+ handleActions st (SetNudge nudge : otherActions) streamState =
+ do
+ (st', streamState') <- handleActions st otherActions streamState
+ let st'' = st' {nudged = nudge}
+ return (st'', streamState')
+ handleActions st (StreamReplace k pat : otherActions) streamState =
+ do
+ (st', streamState') <- handleActions st otherActions streamState
+ E.catch (
+ do
+ now <- Link.clock abletonLink
+ sessionState <- Link.createAndCaptureAppSessionState abletonLink
+ cyc <- timeToCycles' config sessionState now
+ Link.destroySessionState sessionState
+ -- put pattern id and change time in control input
+ let streamState'' = Map.insert ("_t_all") (P.VR $! cyc) $ Map.insert ("_t_" ++ fromID k) (P.VR $! cyc) streamState'
+ (updatePattern ac) k pat
+ return (st', streamState'')
+ )
+ (\(e :: E.SomeException) -> do
+ hPutStrLn stderr $ "Error in pattern: " ++ show e
+ return (st', streamState')
+ )
+ handleActions st (Transition historyFlag f patId pat : otherActions) streamState =
+ do
+ (st', streamState') <- handleActions st otherActions streamState
+ let
+ appendPat flag = if flag then (pat:) else id
+ updatePS (Just playState) = playState {history = (appendPat historyFlag) (history playState)}
+ updatePS Nothing = PlayState {pattern = silence,
+ mute = False,
+ solo = False,
+ history = (appendPat historyFlag) (silence:[])
+ }
+ transition' pat' = do now <- Link.clock abletonLink
+ ss <- Link.createAndCaptureAppSessionState abletonLink
+ c <- timeToCycles' config ss now
+ return $! f c pat'
+ pMap <- readMVar mapMV
+ let playState = updatePS $ Map.lookup (fromID patId) pMap
+ pat' <- transition' $ appendPat (not historyFlag) (history playState)
+ let pMap' = Map.insert (fromID patId) (playState {pattern = pat'}) pMap
+ _ <- swapMVar mapMV pMap'
+ return (st', streamState')
diff --git a/src/Sound/Tidal/Transition.hs b/src/Sound/Tidal/Transition.hs
index 38f937052..b4fda7014 100644
--- a/src/Sound/Tidal/Transition.hs
+++ b/src/Sound/Tidal/Transition.hs
@@ -4,7 +4,7 @@ module Sound.Tidal.Transition where
import Prelude hiding ((<*), (*>))
-import Control.Concurrent.MVar (readMVar, swapMVar)
+import Control.Concurrent.MVar (readMVar, swapMVar, modifyMVar_)
import qualified Sound.OSC.FD as O
import qualified Data.Map.Strict as Map
@@ -16,7 +16,7 @@ import Sound.Tidal.ID
import Sound.Tidal.Params (gain, pan)
import Sound.Tidal.Pattern
import Sound.Tidal.Stream
-import Sound.Tidal.Tempo (timeToCycles)
+import Sound.Tidal.Tempo as T
import Sound.Tidal.UI (fadeOutFrom, fadeInFrom)
import Sound.Tidal.Utils (enumerate)
@@ -42,24 +42,7 @@ import Sound.Tidal.Utils (enumerate)
-- the "historyFlag" determines if the new pattern should be placed on the history stack or not
transition :: Stream -> Bool -> (Time -> [ControlPattern] -> ControlPattern) -> ID -> ControlPattern -> IO ()
transition stream historyFlag f patId !pat =
- do pMap <- readMVar (sPMapMV stream)
- let playState = updatePS $ Map.lookup (fromID patId) pMap
- pat' <- transition' $ appendPat (not historyFlag) (history playState)
- let pMap' = Map.insert (fromID patId) (playState {pattern = pat'}) pMap
- _ <- swapMVar (sPMapMV stream) pMap'
- return ()
- where
- appendPat flag = if flag then (pat:) else id
- updatePS (Just playState) = playState {history = (appendPat historyFlag) (history playState)}
- updatePS Nothing = PlayState {pattern = silence,
- mute = False,
- solo = False,
- history = (appendPat historyFlag) (silence:[])
- }
- transition' pat' = do tempo <- readMVar $ sTempoMV stream
- now <- O.time
- let c = timeToCycles tempo now
- return $ f c pat'
+ modifyMVar_ (sActionsMV stream) (\actions -> return $! (T.Transition historyFlag f patId pat) : actions)
mortalOverlay :: Time -> Time -> [Pattern a] -> Pattern a
mortalOverlay _ _ [] = silence
diff --git a/src/Sound/Tidal/UI.hs b/src/Sound/Tidal/UI.hs
index 369bbf122..c598977c2 100644
--- a/src/Sound/Tidal/UI.hs
+++ b/src/Sound/Tidal/UI.hs
@@ -110,7 +110,7 @@ rand :: Fractional a => Pattern a
rand = Pattern (\(State a@(Arc s e) _) -> [Event (Context []) Nothing a (realToFrac $ (timeToRand ((e + s)/2) :: Double))])
-- | Boolean rand - a continuous stream of true/false values, with a 50/50 chance.
-brand :: Pattern Bool
+brand :: Pattern Bool
brand = _brandBy 0.5
-- | Boolean rand with probability as input, e.g. brandBy 0.25 is 25% chance of being true.
@@ -718,6 +718,8 @@ In the above, three sounds are picked from the pattern on the right according
to the structure given by the `e 3 8`. It ends up picking two `bd` sounds, a
`cp` and missing the `sn` entirely.
+A negative first argument provides the inverse of the euclidean pattern.
+
These types of sequences use "Bjorklund's algorithm", which wasn't made for
music but for an application in nuclear physics, which is exciting. More
exciting still is that it is very similar in structure to the one of the first
@@ -755,7 +757,8 @@ euclid :: Pattern Int -> Pattern Int -> Pattern a -> Pattern a
euclid = tParam2 _euclid
_euclid :: Int -> Int -> Pattern a -> Pattern a
-_euclid n k a = fastcat $ fmap (bool silence a) $ bjorklund (n,k)
+_euclid n k a | n >= 0 = fastcat $ fmap (bool silence a) $ bjorklund (n,k)
+ | otherwise = fastcat $ fmap (bool a silence) $ bjorklund (-n,k)
{- | `euclidfull n k pa pb` stacks @e n k pa@ with @einv n k pb@ -}
euclidFull :: Pattern Int -> Pattern Int -> Pattern a -> Pattern a -> Pattern a
@@ -809,7 +812,7 @@ euclidInv :: Pattern Int -> Pattern Int -> Pattern a -> Pattern a
euclidInv = tParam2 _euclidInv
_euclidInv :: Int -> Int -> Pattern a -> Pattern a
-_euclidInv n k a = fastcat $ fmap (bool a silence) $ bjorklund (n,k)
+_euclidInv n k a = _euclid (-n) k a
index :: Real b => b -> Pattern b -> Pattern c -> Pattern c
index sz indexpat pat =
@@ -960,7 +963,7 @@ discretise = segment
-- | @randcat ps@: does a @slowcat@ on the list of patterns @ps@ but
-- randomises the order in which they are played.
randcat :: [Pattern a] -> Pattern a
-randcat ps = spread' rotL (_segment 1 $ (%1) . fromIntegral <$> (_irand (length ps) :: Pattern Int)) (slowcat ps)
+randcat ps = spread' rotL (_segment 1 $ (% 1) . fromIntegral <$> (_irand (length ps) :: Pattern Int)) (slowcat ps)
wrandcat :: [(Pattern a, Double)] -> Pattern a
wrandcat ps = unwrap $ wchooseBy (segment 1 rand) ps
@@ -1228,33 +1231,32 @@ fit' cyc n from to p = squeezeJoin $ _fit n mapMasks to
p' = density cyc p
from' = density cyc from
-{-| @chunk n f p@ treats the given pattern @p@ as having @n@ chunks, and applies the function @f@ to one of those sections per cycle, running from left to right.
-
-@
-d1 $ chunk 4 (density 4) $ sound "cp sn arpy [mt lt]"
-@
+{-|
+ Treats the given pattern @p@ as having @n@ chunks, and applies the function @f@ to one of those sections per cycle.
+ Running:
+ - from left to right if chunk number is positive
+ - from right to left if chunk number is negative
+
+ @
+ d1 $ chunk 4 (fast 4) $ sound "cp sn arpy [mt lt]"
+ @
-}
-_chunk :: Int -> (Pattern b -> Pattern b) -> Pattern b -> Pattern b
-_chunk n f p = cat [withinArc (Arc (i % fromIntegral n) ((i+1) % fromIntegral n)) f p | i <- [0 .. fromIntegral n - 1]]
-
-
chunk :: Pattern Int -> (Pattern b -> Pattern b) -> Pattern b -> Pattern b
-chunk npat f p = innerJoin $ (\n -> _chunk n f p) <$> npat
-
--- deprecated (renamed to chunk)
-runWith :: Int -> (Pattern b -> Pattern b) -> Pattern b -> Pattern b
-runWith = _chunk
+chunk npat f p = innerJoin $ (\n -> _chunk n f p) <$> npat
-{-| @chunk'@ works much the same as `chunk`, but runs from right to left.
--}
--- this was throwing a parse error when I ran it in tidal whenever I changed the function name..
-_chunk' :: Integral a => a -> (Pattern b -> Pattern b) -> Pattern b -> Pattern b
-_chunk' n f p = do i <- _slow (toRational n) $ rev $ run (fromIntegral n)
- withinArc (Arc (i % fromIntegral n) ((i+)1 % fromIntegral n)) f p
+_chunk :: Integral a => a -> (Pattern b -> Pattern b) -> Pattern b -> Pattern b
+_chunk n f p | n >= 0 = cat [withinArc (Arc (i % fromIntegral n) ((i+1) % fromIntegral n)) f p | i <- [0 .. fromIntegral n - 1]]
+ | otherwise = do i <- _slow (toRational (-n)) $ rev $ run (fromIntegral (-n))
+ withinArc (Arc (i % fromIntegral (-n)) ((i+1) % fromIntegral (-n))) f p
+-- | DEPRECATED, use 'chunk' with negative numbers instead
chunk' :: Integral a1 => Pattern a1 -> (Pattern a2 -> Pattern a2) -> Pattern a2 -> Pattern a2
chunk' npat f p = innerJoin $ (\n -> _chunk' n f p) <$> npat
+-- | DEPRECATED, use '_chunk' with negative numbers instead
+_chunk' :: Integral a => a -> (Pattern b -> Pattern b) -> Pattern b -> Pattern b
+_chunk' n f p = _chunk (-n) f p
+
_inside :: Time -> (Pattern a1 -> Pattern a) -> Pattern a1 -> Pattern a
_inside n f p = _fast n $ f (_slow n p)
@@ -1468,7 +1470,7 @@ rolledBy "<1 -0.5 0.25 -0.125>" $ note "c'maj9" # s "superpiano"
rolledWith :: Ratio Integer -> Pattern a -> Pattern a
rolledWith t = withEvents aux
where aux es = concatMap (steppityIn) (groupBy (\a b -> whole a == whole b) $ ((isRev t) es))
- isRev b = (\x -> if x > 0 then id else reverse ) b
+ isRev b = (\x -> if x > 0 then id else reverse ) b
steppityIn xs = mapMaybe (\(n, ev) -> (timeguard n xs ev t)) $ enumerate xs
timeguard _ _ ev 0 = return ev
timeguard n xs ev _ = (shiftIt n (length xs) ev)
diff --git a/src/Sound/Tidal/Version.hs b/src/Sound/Tidal/Version.hs
index e26ffdafa..82bb265db 100644
--- a/src/Sound/Tidal/Version.hs
+++ b/src/Sound/Tidal/Version.hs
@@ -21,7 +21,7 @@ import Paths_tidal
-}
tidal_version :: String
-tidal_version = "1.8.0"
+tidal_version = "1.9.0"
tidal_status :: IO ()
tidal_status = tidal_status_string >>= putStrLn
diff --git a/stack.yaml b/stack.yaml
index 8867f79c2..c933842f4 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -3,6 +3,7 @@ resolver: lts-16.31
packages:
- '.'
- 'tidal-parse'
+ - 'tidal-link'
extra-deps:
- hosc-0.18.1
diff --git a/test/Sound/Tidal/ControlTest.hs b/test/Sound/Tidal/ControlTest.hs
index 7c0f879ca..4a5b4aa67 100644
--- a/test/Sound/Tidal/ControlTest.hs
+++ b/test/Sound/Tidal/ControlTest.hs
@@ -18,15 +18,23 @@ run =
describe "echo" $ do
it "should echo the event by the specified time and multiply the gain factor" $ do
- compareP (Arc 0 1)
- (echo 2 0.25 0.5 $ s "bd" # gain "1")
- (stack [rotR 0 "bd" # gain 1, rotR 0.25 "bd" # gain 0.5])
+ comparePD (Arc 0 1)
+ (echo 3 0.2 0.5 $ s "bd" # gain "1")
+ (stack [
+ rotR 0 $ s "bd" # gain 1,
+ rotR 0.2 $ s "bd" # gain 0.5,
+ rotR 0.4 $ s "bd" # gain 0.25
+ ])
describe "echoWith" $ do
it "should echo the event by the specified time and apply the specified function" $ do
- compareP (Arc 0 1)
- (echoWith 2 0.25 (|* speed 2) $ s "bd" # speed "1")
- (stack [rotR 0 "bd" # speed 1, rotR 0.25 "bd" # speed 2])
+ comparePD (Arc 0 1)
+ (echoWith 3 0.25 (|* speed 2) $ s "bd" # speed "1")
+ (stack [
+ rotR 0 $ s "bd" # speed 1,
+ rotR 0.25 $ s "bd" # speed 2,
+ rotR 0.5 $ s "bd" # speed 4
+ ])
describe "stutWith" $ do
it "can mimic stut" $ do
diff --git a/test/Sound/Tidal/ParseTest.hs b/test/Sound/Tidal/ParseTest.hs
index 55a26c5a7..5448f37c9 100644
--- a/test/Sound/Tidal/ParseTest.hs
+++ b/test/Sound/Tidal/ParseTest.hs
@@ -160,6 +160,42 @@ run =
compareP (Arc 0 2)
("c'major c'minor" :: Pattern Note)
("'major 'minor")
+ it "can invert chords" $ do
+ compareP (Arc 0 2)
+ ("c'major'i" :: Pattern Note)
+ ("[4,7,12]")
+ it "can invert chords using a number" $ do
+ compareP (Arc 0 2)
+ ("c'major'i2" :: Pattern Note)
+ ("[7,12,16]")
+ it "spread chords over a range" $ do
+ compareP (Arc 0 2)
+ ("c'major'5 e'min7'5" :: Pattern Note)
+ ("[0,4,7,12,16] [4,7,11,14,16]")
+ it "can open chords" $ do
+ compareP (Arc 0 2)
+ ("c'major'o" :: Pattern Note)
+ ("[-12,-5,4]")
+ it "can drop notes in a chord" $ do
+ compareP (Arc 0 2)
+ ("c'major'd1" :: Pattern Note)
+ ("[-5,0,4]")
+ it "can apply multiple modifiers" $ do
+ compareP (Arc 0 2)
+ ("c'major'i'5" :: Pattern Note)
+ ("[4,7,12,16,19]")
+ it "can pattern modifiers" $ do
+ compareP (Arc 0 2)
+ ("c'major'" :: Pattern Note)
+ ("<[4,7,12] [0,4,7,12,16]>")
+ it "can pattern chord names" $ do
+ compareP (Arc 0 2)
+ ("c''i" :: Pattern Note)
+ ("<[4,7,12] [3,7,12]>")
+ it "can pattern chord notes" $ do
+ compareP (Arc 0 2)
+ ("''i" :: Pattern Note)
+ ("<[4,7,12] [7,11,16]>")
it "handle trailing and leading whitespaces" $ do
compareP (Arc 0 1)
(" bd " :: Pattern String)
diff --git a/test/Sound/Tidal/PatternTest.hs b/test/Sound/Tidal/PatternTest.hs
index 951cee61d..898a135b3 100644
--- a/test/Sound/Tidal/PatternTest.hs
+++ b/test/Sound/Tidal/PatternTest.hs
@@ -428,17 +428,6 @@ run =
let res = defragParts [(Event (Context []) (Just $ Arc 1 2) (Arc 3 4) 5), (Event (Context []) (Just $ Arc 7 8) (Arc 4 3) (5 :: Int))]
property $ [Event (Context []) (Just $ Arc 1 2) (Arc 3 4) 5, Event (Context []) (Just $ Arc 7 8) (Arc 4 3) (5 :: Int)] === res
- describe "compareDefrag" $ do
- it "compare list with Events with empty list of Events" $ do
- let res = compareDefrag [Event (Context []) (Just $ Arc 1 2) (Arc 3 4) (5 :: Int), Event (Context []) (Just $ Arc 1 2) (Arc 4 3) (5 :: Int)] []
- property $ False === res
- it "compare lists containing same Events but of different length" $ do
- let res = compareDefrag [Event (Context []) (Just $ Arc 1 2) (Arc 3 4) (5 :: Int), Event (Context []) (Just $ Arc 1 2) (Arc 4 3) 5] [Event (Context []) (Just $ Arc 1 2) (Arc 3 4) (5 :: Int)]
- property $ True === res
- it "compare lists of same length with same Events" $ do
- let res = compareDefrag [Event (Context []) (Just $ Arc 1 2) (Arc 3 4) (5 :: Int)] [Event (Context []) (Just $ Arc 1 2) (Arc 3 4) (5 :: Int)]
- property $ True === res
-
describe "sect" $ do
it "take two Arcs and return - Arc (max of two starts) (min of two ends)" $ do
let res = sect (Arc 2.2 3) (Arc 2 2.9)
diff --git a/test/Sound/Tidal/UITest.hs b/test/Sound/Tidal/UITest.hs
index 4a22e7717..71a3f0980 100644
--- a/test/Sound/Tidal/UITest.hs
+++ b/test/Sound/Tidal/UITest.hs
@@ -306,6 +306,15 @@ run =
(euclid 11 24 "x", "x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~"),
(euclid 13 24 "x", "x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~")
] :: [(Pattern String, String)])
+ it "can be called with a negative first value to give the inverse" $ do
+ compareP (Arc 0 1)
+ (euclid (-3) 8 ("bd" :: Pattern String))
+ (euclidInv 3 8 ("bd" :: Pattern String))
+ it "can be called with a negative first value to give the inverse (patternable)" $ do
+ compareP (Arc 0 1)
+ (euclid (-3) 8 ("bd" :: Pattern String))
+ ("bd(-3,8)" :: Pattern String)
+
describe "wedge" $ do
it "should not freeze tidal amount is 1" $ do
@@ -360,6 +369,10 @@ run =
compareP (Arc 0 4)
(chunk 2 (fast 2) $ "a b" :: Pattern String)
(slow 2 $ "a b b _ a _ a b" :: Pattern String)
+ it "should chunk backward with a negative number" $ do
+ compareP (Arc 0 4)
+ (chunk (-2) (rev) $ ("a b c d" :: Pattern String))
+ (slow 2 $ "a b b a d c c d" :: Pattern String)
describe "binary" $ do
it "converts a number to a pattern of boolean" $ do
diff --git a/test/TestUtils.hs b/test/TestUtils.hs
index 7bbd1940f..eb9928af5 100644
--- a/test/TestUtils.hs
+++ b/test/TestUtils.hs
@@ -36,13 +36,17 @@ instance TolerantEq (Event ValueMap) where
-- | Compare the events of two patterns using the given arc
compareP :: (Ord a, Show a) => Arc -> Pattern a -> Pattern a -> Property
-compareP a p p' = (sort $ query (stripContext p) $ State a Map.empty) `shouldBe` (sort $ query (stripContext p') $ State a Map.empty)
+compareP a p p' =
+ (sort $ queryArc (stripContext p) a)
+ `shouldBe`
+ (sort $ queryArc (stripContext p') a)
-- | Like @compareP@, but tries to 'defragment' the events
-comparePD :: (Ord a) => Arc -> Pattern a -> Pattern a -> Bool
-comparePD a p p' = compareDefrag es es'
- where es = query (stripContext p) (State a Map.empty)
- es' = query (stripContext p') (State a Map.empty)
+comparePD :: (Ord a, Show a) => Arc -> Pattern a -> Pattern a -> Property
+comparePD a p p' =
+ (sort $ defragParts $ queryArc (stripContext p) a)
+ `shouldBe`
+ (sort $ defragParts $ queryArc (stripContext p') a)
-- | Like @compareP@, but for control patterns, with some tolerance for floating point error
compareTol :: Arc -> ControlPattern -> ControlPattern -> Bool
diff --git a/tidal-link/LICENSE b/tidal-link/LICENSE
new file mode 100644
index 000000000..f288702d2
--- /dev/null
+++ b/tidal-link/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/tidal-link/README.md b/tidal-link/README.md
new file mode 100644
index 000000000..fcebd905d
--- /dev/null
+++ b/tidal-link/README.md
@@ -0,0 +1,6 @@
+# tidal-link
+
+Ableton Link integration for Tidal
+
+Requires fixes to GHC on Windows (see https://gitlab.haskell.org/ghc/ghc/-/issues/20918)
+which are available from GHC 9.2.4.
diff --git a/tidal-link/link/.appveyor.yml b/tidal-link/link/.appveyor.yml
new file mode 100644
index 000000000..27214e28d
--- /dev/null
+++ b/tidal-link/link/.appveyor.yml
@@ -0,0 +1,146 @@
+clone_depth: 50
+
+branches:
+ only:
+ - master
+
+environment:
+ matrix:
+ - APPVEYOR_BUILD_WORKER_IMAGE: macos-mojave
+ CONFIGURATION: Release
+ XCODE_VERSION: 9.4.1
+ - APPVEYOR_BUILD_WORKER_IMAGE: macos-catalina
+ CONFIGURATION: Release
+ XCODE_VERSION: 11.7
+ - APPVEYOR_BUILD_WORKER_IMAGE: macos-catalina
+ CONFIGURATION: Debug
+ XCODE_VERSION: 12.3
+ - APPVEYOR_BUILD_WORKER_IMAGE: macos-monterey
+ CONFIGURATION: Release
+ XCODE_VERSION: 12.5.1
+ - APPVEYOR_BUILD_WORKER_IMAGE: macos-monterey
+ CONFIGURATION: Release
+ XCODE_VERSION: 13.2.1
+ - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
+ AUDIO_DRIVER: Jack
+ CONFIGURATION: Debug
+ GENERATOR: Ninja
+ CXX: clang++-11
+ - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
+ AUDIO_DRIVER: Alsa
+ CONFIGURATION: Release
+ GENERATOR: Ninja
+ CXX: clang++-10
+ - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
+ AUDIO_DRIVER: Jack
+ CONFIGURATION: Debug
+ GENERATOR: Ninja
+ CXX: clang++-9
+ - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
+ AUDIO_DRIVER: Alsa
+ CONFIGURATION: Release
+ GENERATOR: Ninja
+ CXX: g++-9
+ - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
+ AUDIO_DRIVER: Jack
+ CONFIGURATION: Debug
+ GENERATOR: Ninja
+ CXX: g++-8
+ - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
+ AUDIO_DRIVER: Alsa
+ CONFIGURATION: Release
+ GENERATOR: Ninja
+ CXX: g++-7
+ - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
+ AUDIO_DRIVER: Asio
+ THREAD_DESCRIPTION: OFF
+ CONFIGURATION: Release
+ GENERATOR: Visual Studio 14 2015
+ - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
+ AUDIO_DRIVER: Asio
+ THREAD_DESCRIPTION: OFF
+ CONFIGURATION: Debug
+ GENERATOR: Visual Studio 14 2015 Win64
+ - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
+ AUDIO_DRIVER: Asio
+ THREAD_DESCRIPTION: OFF
+ CONFIGURATION: Release
+ GENERATOR: Visual Studio 14 2015 Win64
+ - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
+ AUDIO_DRIVER: Wasapi
+ THREAD_DESCRIPTION: OFF
+ CONFIGURATION: Release
+ GENERATOR: Visual Studio 14 2015 Win64
+ - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
+ AUDIO_DRIVER: Asio
+ THREAD_DESCRIPTION: OFF
+ CONFIGURATION: Release
+ GENERATOR: Visual Studio 15 2017 Win64
+ - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
+ AUDIO_DRIVER: Asio
+ THREAD_DESCRIPTION: ON
+ CONFIGURATION: Release
+ GENERATOR: Visual Studio 16 2019
+ - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
+ AUDIO_DRIVER: Wasapi
+ THREAD_DESCRIPTION: ON
+ CONFIGURATION: Debug
+ GENERATOR: Visual Studio 17 2022
+ - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
+ ESP_IDF: true
+ IDF_RELEASE: v4.3.1
+ - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
+ FORMATTING: true
+
+install:
+ - git submodule update --init --recursive
+
+for:
+ - matrix:
+ only:
+ - APPVEYOR_BUILD_WORKER_IMAGE: macos-mojave
+ - APPVEYOR_BUILD_WORKER_IMAGE: macos-catalina
+ - APPVEYOR_BUILD_WORKER_IMAGE: macos-monterey
+ build_script:
+ - sudo xcode-select -s /Applications/Xcode-$XCODE_VERSION.app
+ - python3 ci/configure.py --generator Xcode
+ - python3 ci/build.py --configuration $CONFIGURATION
+ test_script:
+ - python3 ci/run-tests.py --target LinkCoreTest
+ - python3 ci/run-tests.py --target LinkDiscoveryTest
+ - matrix:
+ only:
+ # Ubuntu2004 but not ESP_IDF or FORMATTING
+ - GENERATOR: Ninja
+ install:
+ - git submodule update --init --recursive
+ - sudo apt-get update
+ - sudo apt-get install -y libjack-dev portaudio19-dev valgrind
+ build_script:
+ - python3 ci/configure.py --audio-driver $AUDIO_DRIVER --generator "$GENERATOR" --configuration $CONFIGURATION
+ - python3 ci/build.py
+ test_script:
+ - python3 ci/run-tests.py --target LinkCoreTest --valgrind
+ - python3 ci/run-tests.py --target LinkDiscoveryTest --valgrind
+ - matrix:
+ only:
+ - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
+ - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
+ - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
+ - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
+ build_script:
+ - py -3 ci/configure.py --audio-driver %AUDIO_DRIVER% --thread-description %THREAD_DESCRIPTION% --generator "%GENERATOR%" --flags="-DCMAKE_SYSTEM_VERSION=10.0.18362.0"
+ - py -3 ci/build.py --configuration %CONFIGURATION%
+ test_script:
+ - py -3 ci/run-tests.py --target LinkCoreTest
+ - py -3 ci/run-tests.py --target LinkDiscoveryTest
+ - matrix:
+ only:
+ - ESP_IDF: true
+ build_script:
+ - docker run --rm -v $APPVEYOR_BUILD_FOLDER:/link -w /link/examples/esp32 -e LC_ALL=C.UTF-8 espressif/idf:$IDF_RELEASE idf.py build
+ - matrix:
+ only:
+ - FORMATTING: true
+ build_script:
+ - docker run -v $APPVEYOR_BUILD_FOLDER:/link dalg24/clang-format:18.04.0 python /link/ci/check-formatting.py -c /usr/bin/clang-format-6.0
diff --git a/tidal-link/link/.clang-format b/tidal-link/link/.clang-format
new file mode 100644
index 000000000..ac03c4e31
--- /dev/null
+++ b/tidal-link/link/.clang-format
@@ -0,0 +1,50 @@
+Language: Cpp
+
+AccessModifierOffset: -2
+AlignAfterOpenBracket: DontAlign
+AlignEscapedNewlinesLeft: false
+AlignOperands: true
+AlignTrailingComments: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowShortBlocksOnASingleLine: false
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: true
+AlwaysBreakTemplateDeclarations: true
+BinPackArguments: true
+BinPackParameters: false
+BreakBeforeBinaryOperators: NonAssignment
+BreakBeforeBraces: Allman
+BreakBeforeTernaryOperators: true
+BreakConstructorInitializersBeforeComma: true
+ColumnLimit: 90
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+ConstructorInitializerIndentWidth: 2
+ContinuationIndentWidth: 2
+Cpp11BracedListStyle: true
+DerivePointerAlignment: false
+IndentCaseLabels: false
+IndentFunctionDeclarationAfterType: false
+IndentWidth: 2
+IndentWrappedFunctionNames: false
+KeepEmptyLinesAtTheStartOfBlocks: true
+MaxEmptyLinesToKeep: 2
+NamespaceIndentation: None
+PenaltyBreakBeforeFirstCallParameter: 0
+PenaltyReturnTypeOnItsOwnLine: 1000
+PointerAlignment: Left
+SpaceAfterCStyleCast: false
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeParens: ControlStatements
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles: false
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+Standard: Cpp11
+UseTab: Never
diff --git a/tidal-link/link/.gitignore b/tidal-link/link/.gitignore
new file mode 100644
index 000000000..bcdff6af1
--- /dev/null
+++ b/tidal-link/link/.gitignore
@@ -0,0 +1,33 @@
+# IDE generated files and build outputs
+/build/
+/ide/
+/output/
+/logs/
+.idea/*
+
+# System temporary files
+.DS_Store
+*~
+*.swp
+
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
diff --git a/tidal-link/link/AbletonLinkConfig.cmake b/tidal-link/link/AbletonLinkConfig.cmake
new file mode 100644
index 000000000..43b66e7d3
--- /dev/null
+++ b/tidal-link/link/AbletonLinkConfig.cmake
@@ -0,0 +1,50 @@
+if(CMAKE_VERSION VERSION_LESS 3.0)
+ message(FATAL_ERROR "CMake 3.0 or greater is required")
+endif()
+
+add_library(Ableton::Link IMPORTED INTERFACE)
+set_property(TARGET Ableton::Link APPEND PROPERTY
+ INTERFACE_INCLUDE_DIRECTORIES
+ ${CMAKE_CURRENT_LIST_DIR}/include
+)
+
+# Force C++11 support for consuming targets
+set_property(TARGET Ableton::Link APPEND PROPERTY
+ INTERFACE_COMPILE_FEATURES
+ cxx_generalized_initializers
+)
+
+if(UNIX)
+ set_property(TARGET Ableton::Link APPEND PROPERTY
+ INTERFACE_COMPILE_DEFINITIONS
+ LINK_PLATFORM_UNIX=1
+ )
+endif()
+
+if(APPLE)
+ set_property(TARGET Ableton::Link APPEND PROPERTY
+ INTERFACE_COMPILE_DEFINITIONS
+ LINK_PLATFORM_MACOSX=1
+ )
+elseif(WIN32)
+ set_property(TARGET Ableton::Link APPEND PROPERTY
+ INTERFACE_COMPILE_DEFINITIONS
+ LINK_PLATFORM_WINDOWS=1
+ )
+elseif(CMAKE_SYSTEM_NAME MATCHES "Linux|kFreeBSD|GNU")
+ set_property(TARGET Ableton::Link APPEND PROPERTY
+ INTERFACE_COMPILE_DEFINITIONS
+ LINK_PLATFORM_LINUX=1
+ )
+endif()
+
+include(${CMAKE_CURRENT_LIST_DIR}/cmake_include/AsioStandaloneConfig.cmake)
+set_property(TARGET Ableton::Link APPEND PROPERTY
+ INTERFACE_LINK_LIBRARIES
+ AsioStandalone::AsioStandalone
+)
+
+set_property(TARGET Ableton::Link APPEND PROPERTY
+ INTERFACE_SOURCES
+ ${CMAKE_CURRENT_LIST_DIR}/include/ableton/Link.hpp
+)
diff --git a/tidal-link/link/CMakeLists.txt b/tidal-link/link/CMakeLists.txt
new file mode 100644
index 000000000..5924722da
--- /dev/null
+++ b/tidal-link/link/CMakeLists.txt
@@ -0,0 +1,65 @@
+cmake_minimum_required(VERSION 3.0)
+project(Link)
+
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
+
+# ___ _ _
+# / _ \ _ __ | |_(_) ___ _ __ ___
+# | | | | '_ \| __| |/ _ \| '_ \/ __|
+# | |_| | |_) | |_| | (_) | | | \__ \
+# \___/| .__/ \__|_|\___/|_| |_|___/
+# |_|
+
+# Note: Please use the LINK_* prefix for all project-specific options
+
+if(UNIX)
+ option(LINK_ENABLE_ASAN "Build with Address Sanitizier (ASan)" OFF)
+ option(LINK_BUILD_JACK "Build example applications with JACK support" OFF)
+endif()
+
+if(WIN32)
+ option(LINK_BUILD_ASIO "Build example applications with ASIO driver" ON)
+ option(LINK_BUILD_VLD "Build with VLD support (VLD must be installed separately)" OFF)
+endif()
+
+# ____ _ _
+# | _ \ __ _| |_| |__ ___
+# | |_) / _` | __| '_ \/ __|
+# | __/ (_| | |_| | | \__ \
+# |_| \__,_|\__|_| |_|___/
+#
+
+# Other CMake files must be included only after declaring build options
+include(cmake_include/ConfigureCompileFlags.cmake)
+include(cmake_include/CatchConfig.cmake)
+include(AbletonLinkConfig.cmake)
+include(extensions/abl_link/abl_link.cmake)
+
+add_subdirectory(include)
+add_subdirectory(src)
+add_subdirectory(examples)
+add_subdirectory(extensions/abl_link)
+
+# ____
+# / ___| _ _ _ __ ___ _ __ ___ __ _ _ __ _ _
+# \___ \| | | | '_ ` _ \| '_ ` _ \ / _` | '__| | | |
+# ___) | |_| | | | | | | | | | | | (_| | | | |_| |
+# |____/ \__,_|_| |_| |_|_| |_| |_|\__,_|_| \__, |
+# |___/
+
+message(STATUS "Build options")
+
+get_cmake_property(all_variables VARIABLES)
+string(REGEX MATCHALL "(^|;)LINK_[A-Z_]+" link_variables "${all_variables}")
+foreach(variable ${link_variables})
+ message(" ${variable}: ${${variable}}")
+endforeach()
+
+message(STATUS "Build configuration")
+
+if(CMAKE_BUILD_TYPE)
+ message(" Build type: ${CMAKE_BUILD_TYPE}")
+else()
+ message(" Build type: Set by IDE")
+endif()
+
diff --git a/tidal-link/link/CONTRIBUTING.md b/tidal-link/link/CONTRIBUTING.md
new file mode 100644
index 000000000..c4a0fd5e1
--- /dev/null
+++ b/tidal-link/link/CONTRIBUTING.md
@@ -0,0 +1,63 @@
+Bug Reports
+===========
+
+If you've found a bug in Link itself, then please file a new issue here at GitHub. If you
+have found a bug in a Link-enabled app, it might be wiser to reach out to the developer of
+the app before filing an issue here.
+
+Any and all information that you can provide regarding the bug will help in our being able
+to find it. Specifically, that could include:
+
+ - Stacktraces, in the event of a crash
+ - Versions of the software used, and the underlying operating system
+ - Steps to reproduce
+ - Screenshots, in the case of a bug which results in a visual error
+
+
+Pull Requests
+=============
+
+We are happy to accept pull requests from the GitHub community, assuming that they meet
+the following criteria:
+
+ - You have signed and returned Ableton's [CLA][cla]
+ - The [tests pass](#testing)
+ - The PR passes all CI service checks
+ - The code is [well-formatted](#code-formatting)
+ - The git commit messages comply to [the commonly accepted standards][git-commit-msgs]
+
+Testing
+-------
+
+Link ships with unit tests that are run on [Travis CI][travis] and [AppVeyor][appveyor] for
+all PRs. There are two test suites: `LinkCoreTest`, which tests the core Link
+functionality, and `LinkDiscoverTest`, which tests the network discovery feature of Link.
+A third virtual target, `LinkAllTest` is provided by the CMake project as a convenience
+to run all tests at once.
+
+The unit tests are run on every platform which Link is officially supported on, and also
+are run through [Valgrind][valgrind] on Linux to check for memory corruption and leaks. If
+valgrind detects any memory errors when running the tests, it will fail the build.
+
+If you are submitting a PR which fixes a bug or introduces new functionality, please add a
+test which helps to verify the correctness of the code in the PR.
+
+Code Formatting
+---------------
+
+Link uses [clang-format][clang-format] to enforce our preferred code style. At the moment,
+we use **clang-format version 6.0**. Note that other versions may format code differently.
+
+Any PRs submitted to Link are also checked with clang-format by the Travis CI service. If
+you get a build failure, then you can format your code by running the following command:
+
+```
+clang-format -style=file -i (filename)
+```
+
+[cla]: http://ableton.github.io/cla/
+[git-commit-msgs]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
+[clang-format]: http://llvm.org/builds
+[valgrind]: http://valgrind.org
+[travis]: http://travis-ci.com
+[appveyor]: http://appveyor.com
diff --git a/tidal-link/link/GNU-GPL-v2.0.md b/tidal-link/link/GNU-GPL-v2.0.md
new file mode 100644
index 000000000..0daa04150
--- /dev/null
+++ b/tidal-link/link/GNU-GPL-v2.0.md
@@ -0,0 +1,336 @@
+GNU General Public License
+==========================
+
+_Version 2, June 1991_
+_Copyright © 1989, 1991 Free Software Foundation, Inc.,_
+_51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA_
+
+Everyone is permitted to copy and distribute verbatim copies
+of this license document, but changing it is not allowed.
+
+### Preamble
+
+The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+We protect your rights with two steps: **(1)** copyright the software, and
+**(2)** offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and
+modification follow.
+
+### TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+**0.** This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The “Program”, below,
+refers to any such program or work, and a “work based on the Program”
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term “modification”.) Each licensee is addressed as “you”.
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+**1.** You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+**2.** You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+* **a)** You must cause the modified files to carry prominent notices
+stating that you changed the files and the date of any change.
+* **b)** You must cause any work that you distribute or publish, that in
+whole or in part contains or is derived from the Program or any
+part thereof, to be licensed as a whole at no charge to all third
+parties under the terms of this License.
+* **c)** If the modified program normally reads commands interactively
+when run, you must cause it, when started running for such
+interactive use in the most ordinary way, to print or display an
+announcement including an appropriate copyright notice and a
+notice that there is no warranty (or else, saying that you provide
+a warranty) and that users may redistribute the program under
+these conditions, and telling the user how to view a copy of this
+License. (Exception: if the Program itself is interactive but
+does not normally print such an announcement, your work based on
+the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+**3.** You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+* **a)** Accompany it with the complete corresponding machine-readable
+source code, which must be distributed under the terms of Sections
+1 and 2 above on a medium customarily used for software interchange; or,
+* **b)** Accompany it with a written offer, valid for at least three
+years, to give any third party, for a charge no more than your
+cost of physically performing source distribution, a complete
+machine-readable copy of the corresponding source code, to be
+distributed under the terms of Sections 1 and 2 above on a medium
+customarily used for software interchange; or,
+* **c)** Accompany it with the information you received as to the offer
+to distribute corresponding source code. (This alternative is
+allowed only for noncommercial distribution and only if you
+received the program in object code or executable form with such
+an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+**4.** You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+**5.** You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+**6.** Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+**7.** If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+**8.** If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+**9.** The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and “any
+later version”, you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+**10.** If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+### NO WARRANTY
+
+**11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+**12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+END OF TERMS AND CONDITIONS
+
+### How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the “copyright” line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w` and `show c` should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w` and `show c`; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a “copyright disclaimer” for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/tidal-link/link/LICENSE.md b/tidal-link/link/LICENSE.md
new file mode 100644
index 000000000..75fc05464
--- /dev/null
+++ b/tidal-link/link/LICENSE.md
@@ -0,0 +1,19 @@
+# License
+
+Copyright 2016, Ableton AG, Berlin. All rights reserved.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+If you would like to incorporate Link into a proprietary software application,
+please contact .
diff --git a/tidal-link/link/README.md b/tidal-link/link/README.md
new file mode 100644
index 000000000..ee2c6b1d9
--- /dev/null
+++ b/tidal-link/link/README.md
@@ -0,0 +1,156 @@
+# Ableton Link
+
+This is the codebase for Ableton Link, a technology that synchronizes musical beat, tempo,
+and phase across multiple applications running on one or more devices. Applications on
+devices connected to a local network discover each other automatically and form a musical
+session in which each participant can perform independently: anyone can start or stop
+while still staying in time. Anyone can change the tempo, the others will follow. Anyone
+can join or leave without disrupting the session.
+
+# License
+
+Ableton Link is dual [licensed][license] under GPLv2+ and a proprietary license. If you
+would like to incorporate Link into a proprietary software application, please contact
+.
+
+# Building and Running Link Examples
+
+Link relies on `asio-standalone` as a submodule. After checking out the
+main repositories, those submodules have to be loaded using
+
+```
+git submodule update --init --recursive
+```
+
+Link uses [CMake][cmake] to generate build files for the [Catch][catch]-based
+unit-tests and the example applications.
+
+```
+$ mkdir build
+$ cd build
+$ cmake ..
+$ cmake --build .
+```
+
+The output binaries for the example applications and the unit-tests will be placed in a
+`bin` subdirectory of the CMake binary directory.
+
+# Integrating Link in your Application
+
+## Test Plan
+
+To make sure users have the best possible experience using Link it is important all apps
+supporting Link behave consistently. This includes for example playing in sync with other
+apps as well as not hijacking a jams tempo when joining. To make sure your app behaves as
+intended make sure it complies to the [Test Plan](TEST-PLAN.md).
+
+## Building Link
+
+Link is a header-only library, so it should be straightforward to integrate into your
+application.
+
+### CMake-based Projects
+
+If you are using CMake, then you can simply add the following to your CMakeLists.txt file:
+
+```cmake
+include($PATH_TO_LINK/AbletonLinkConfig.cmake)
+target_link_libraries($YOUR_TARGET Ableton::Link)
+
+```
+
+You can optionally have your build target depend on `${link_HEADERS}`, which will make
+the Link headers visible in your IDE. This variable exported to the `PARENT_SCOPE` by
+Link's CMakeLists.txt.
+
+### Other Build Systems
+
+To include the Link library in your non CMake project, you must do the following:
+
+ - Add the `link/include` and `modules/asio-standalone/asio/include` directories to your
+ list of include paths
+ - Define `LINK_PLATFORM_MACOSX=1`, `LINK_PLATFORM_LINUX=1`, or `LINK_PLATFORM_WINDOWS=1`,
+ depending on which platform you are building on.
+
+If you get any compiler errors/warnings, have a look at
+[compile-flags.cmake](cmake_include/ConfigureCompileFlags.cmake), which might provide some
+insight as to the compiler flags needed to build Link.
+
+### Build Requirements
+
+| Platform | Minimum Required | Optional (only required for examples) |
+|----------|----------------------|---------------------------------------|
+| Windows | MSVC 2015 | Steinberg ASIO SDK 2.3 |
+| Mac | Xcode 9.4.1 | |
+| Linux | Clang 3.6 or GCC 5.2 | libportaudio19-dev |
+
+
+Other compilers with good C++11 support should work, but are not verified.
+
+iOS developers should not use this repo. See http://ableton.github.io/linkkit for
+information on the LinkKit SDK for iOS.
+
+# Documentation
+
+An overview of Link concepts can be found at http://ableton.github.io/link. Those that
+are new to Link should start there. The [Link.hpp](include/ableton/Link.hpp) header
+contains the full Link public interface. See the LinkHut projects in this repo for an
+example usage of the `Link` type.
+
+## Time and Clocks
+
+Link works by calculating a relationship between the system clocks of devices in a session.
+Since the mechanism for obtaining a system time value and the unit of these values differ
+across platforms, Link defines a `Clock` abstraction with platform-specific
+implementations. Please see:
+- `Link::clock()` method in [Link.hpp](include/ableton/Link.hpp)
+- OSX and iOS clock implementation in
+[platforms/darwin/Clock.hpp](include/ableton/platforms/darwin/Clock.hpp)
+- Windows clock implementation in
+[platforms/windows/Clock.hpp](include/ableton/platforms/windows/Clock.hpp)
+- C++ standard library `std::chrono::high_resolution_clock`-based implementation in
+[platforms/stl/Clock.hpp](include/ableton/platforms/stl/Clock.hpp)
+
+Using the system time correctly in the context of an audio callback gets a little
+complicated. Audio devices generally have a sample clock that is independent of the system
+Clock. Link maintains a mapping between system time and beat time and therefore can't use
+the sample time provided by the audio system directly.
+
+On OSX and iOS, the CoreAudio render callback is passed an `AudioTimeStamp` structure with
+a `mHostTime` member that represents the system time at which the audio buffer will be
+passed to the audio hardware. This is precisely the information needed to derive the beat
+time values corresponding to samples in the buffer using Link. Unfortunately, not all
+platforms provide this data to the audio callback.
+
+When a system timestamp is not provided with the audio buffer, the best a client can do in
+the audio callback is to get the current system time and filter it based on the provided
+sample time. Filtering is necessary because the audio callback will not be invoked at a
+perfectly regular interval and therefore the queried system time will exhibit jitter
+relative to the sample clock. The Link library provides a
+[HostTimeFilter](include/ableton/link/HostTimeFilter.hpp) utility class that performs a
+linear regression between system time and sample time in order to improve the accuracy of
+system time values used in an audio callback. See the audio callback implementations for
+the various [platforms](examples/linkaudio) used in the examples to see how this is used
+in practice. Note that for Windows-based systems, we recommend using the [ASIO][asio]
+audio driver.
+
+## Latency Compensation
+
+As discussed in the previous section, the system time that a client is provided in an
+audio callback either represents the time at which the buffer will be submitted to the
+audio hardware (for OSX/iOS) or the time at which the callback was invoked (when the
+code in the callback queries the system time). Note that neither of these is what we
+actually want to synchronize between devices in order to play in time.
+
+In order for multiple devices to play in time, we need to synchronize the moment at which
+their signals hit the speaker or output cable. If this compensation is not performed,
+the output signals from devices with different output latencies will exhibit a persistent
+offset from each other. For this reason, the audio system's output latency should be added
+to system time values before passing them to Link methods. Examples of this latency
+compensation can be found in the [platform](examples/linkaudio) implementations of the
+example apps.
+
+[asio]: https://www.steinberg.net/en/company/developers.html
+[catch]: https://github.com/philsquared/Catch
+[cmake]: https://www.cmake.org
+[license]: LICENSE.md
diff --git a/tidal-link/link/TEST-PLAN.md b/tidal-link/link/TEST-PLAN.md
new file mode 100644
index 000000000..5a6de3782
--- /dev/null
+++ b/tidal-link/link/TEST-PLAN.md
@@ -0,0 +1,99 @@
+# Test Plan
+
+Below are a set of user interactions that are expected to work consistently across all
+Link-enabled apps. In order to provide the best user experience, it's important that apps
+behave consistently with respect to these test cases.
+
+## Tempo Changes
+
+### TEMPO-1: Tempo changes should be transmitted between connected apps.
+
+- Open LinkHut, press **Play** and **enable** Link.
+- Open App and **enable** Link.
+- Without starting to play, change tempo in App **⇒** LinkHut clicks should speed up or slow down to match the tempo specified in
+the App.
+- Start playing in the App **⇒** App and LinkHut should be in sync
+- Change tempo in App and in LinkHut **⇒** App and LinkHut should remain in sync
+
+### TEMPO-2: Opening an app with Link enabled should not change the tempo of an existing Link session.
+
+- Open App and **enable** Link.
+- Set App tempo to 100bpm.
+- Terminate App.
+- Open LinkHut, press **Play** and **enable** Link.
+- Set LinkHut tempo to 130bpm.
+- Open App and **enable** Link. **⇒** Link should be connected (“1 Link”) and the App and
+LinkHut’s tempo should both be 130bpm.
+
+### TEMPO-3: When connected, loading a new document should not change the Link session tempo.
+
+- Open LinkHut, press **Play** and **enable** Link.
+- Set LinkHut tempo to 130bpm.
+- Open App and **enable** Link **⇒** LinkHut’s tempo should not change.
+- Load new Song/Set/Session with a tempo other than 130bpm **⇒** App and LinkHut tempo should both be 130bpm.
+
+### TEMPO-4: Tempo range handling.
+
+- Open LinkHut, press **Play**, enable Link.
+- Open App, start Audio, and **enable** Link.
+- Change tempo in LinkHut to **20bpm** **⇒** App and LinkHut should stay in sync.
+- Change Tempo in LinkHut to **999bpm** **⇒** App and LinkHut should stay in sync.
+- If App does not support the full range of tempos supported by Link, it should stay in sync by switching to a multiple of the Link session tempo.
+
+### TEMPO-5: Enabling Link does not change app's tempo if there is no Link session to join.
+- Open App, start playing.
+- Change App tempo to something other than the default.
+- **Enable** Link **⇒** App's tempo should not change.
+- Change App tempo to a new value (not the default).
+- **Disable** Link **⇒** App's tempo should not change.
+
+## Beat Time
+
+These cases verify the continuity of beat time across Link operations.
+
+### BEATTIME-1: Enabling Link does not change app's beat time if there is no Link session to join.
+- Open App, start playing.
+- **Enable** Link **⇒** No beat time jump or audible discontinuity should occur.
+- **Disable** Link **⇒** No beat time jump or audible discontinuity should occur.
+
+### BEATTIME-2: App's beat time does not change if another participant joins its session.
+- Open App and **enable** Link.
+- Start playing.
+- Open LinkHut and **enable** Link **⇒** No beat time jump or audible discontinuity should occur in the App.
+
+**Note**: When joining an existing Link session, an app should adjust to the existing
+session's tempo and phase, which will usually result in a beat time jump. Apps that are
+already in a session should never have any kind of beat time or audio discontinuity when
+a new participant joins the session.
+
+## Start Stop States
+
+### STARTSTOPSTATE-1: Listening to start/stop commands from other peers.
+- Open App, set Link and Start Stop Sync to **Enabled**.
+- Open LinkHut, **enable** Link and Start Stop Sync and press **Play** **⇒** App should start playing according to its quantization.
+- Stop playback in LinkHut **⇒** App should stop playing.
+
+### STARTSTOPSTATE-2: Sending start/stop commands to other peers.
+- Open LinkHut, **enable** Link and Start Stop Sync and press **Play**.
+- Open App, set Link and Start Stop Sync to **Enabled** **⇒** App should not be
+playing while LinkHut continues playing.
+- Start playback in App **⇒** App should join playing according to its quantization.
+- Stop playback in App **⇒** App and LinkHut should stop playing.
+- Start playback in App **⇒** App and LinkHut should start playing according to
+their quantizations.
+
+## Audio Engine
+
+These cases verify the correct implementation of latency compensation within an app's
+audio engine.
+
+### AUDIOENGINE-1: Correct alignment of app audio with shared session
+
+- Connect the audio out of your computer to the audio in. Alternatively use
+[SoundFlower](https://github.com/mattingalls/Soundflower) to be able to record the output
+of your app and LinkHut.
+- Open LinkHut, **enable** Link and press **Play**.
+- Open App and **enable** Link.
+- Start playing audio (preferably a short, click-like sample) with notes on the same beats as LinkHut.
+- Record audio within application of choice.
+- Validate whether onset of the sample aligns with the pulse generated by LinkHut (tolerance: less than 3 ms).
diff --git a/tidal-link/link/assets/Ableton_Link_Badge-Black.eps b/tidal-link/link/assets/Ableton_Link_Badge-Black.eps
new file mode 100644
index 000000000..082662fc9
Binary files /dev/null and b/tidal-link/link/assets/Ableton_Link_Badge-Black.eps differ
diff --git a/tidal-link/link/assets/Ableton_Link_Badge-White.eps b/tidal-link/link/assets/Ableton_Link_Badge-White.eps
new file mode 100644
index 000000000..1bc5e7ad3
Binary files /dev/null and b/tidal-link/link/assets/Ableton_Link_Badge-White.eps differ
diff --git a/tidal-link/link/assets/Ableton_Link_Button_disabled.eps b/tidal-link/link/assets/Ableton_Link_Button_disabled.eps
new file mode 100644
index 000000000..885a4a037
Binary files /dev/null and b/tidal-link/link/assets/Ableton_Link_Button_disabled.eps differ
diff --git a/tidal-link/link/assets/Ableton_Link_Button_enabled.eps b/tidal-link/link/assets/Ableton_Link_Button_enabled.eps
new file mode 100644
index 000000000..e90e40476
Binary files /dev/null and b/tidal-link/link/assets/Ableton_Link_Button_enabled.eps differ
diff --git a/tidal-link/link/ci/build.py b/tidal-link/link/ci/build.py
new file mode 100644
index 000000000..2413071cf
--- /dev/null
+++ b/tidal-link/link/ci/build.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+
+import argparse
+import logging
+import os
+import sys
+
+from distutils.spawn import find_executable
+from subprocess import call
+
+
+def parse_args():
+ arg_parser = argparse.ArgumentParser()
+
+ arg_parser.add_argument(
+ '--cmake',
+ default=find_executable("cmake"),
+ help='Path to CMake executable (default: %(default)s)')
+
+ arg_parser.add_argument(
+ '-c', '--configuration',
+ help='Build configuration to use (not supported by IDE generators)')
+
+ arg_parser.add_argument(
+ '-a', '--arguments',
+ help='Arguments to pass to builder')
+
+ return arg_parser.parse_args(sys.argv[1:])
+
+
+def build_cmake_args(args, build_dir):
+ if args.cmake is None:
+ logging.error('CMake not found, please use the --cmake option')
+ return None
+
+ cmake_args = []
+ cmake_args.append(args.cmake)
+ cmake_args.append('--build')
+ cmake_args.append(build_dir)
+
+ if args.configuration is not None:
+ cmake_args.append('--config')
+ cmake_args.append(args.configuration)
+
+ if args.arguments is not None:
+ cmake_args.append('--')
+ for arg in args.arguments.split():
+ cmake_args.append(arg)
+
+ return cmake_args
+
+
+def build(args):
+ scripts_dir = os.path.dirname(os.path.realpath(__file__))
+ root_dir = os.path.join(scripts_dir, os.pardir)
+ build_dir = os.path.join(root_dir, 'build')
+ if not os.path.exists(build_dir):
+ logging.error(
+ 'Build directory not found, did you forget to run the configure.py script?')
+ return 2
+
+ cmake_args = build_cmake_args(args, build_dir)
+ if cmake_args is None:
+ return 1
+
+ logging.info('Running CMake')
+ return call(cmake_args)
+
+
+if __name__ == '__main__':
+ logging.basicConfig(format='%(message)s', level=logging.INFO, stream=sys.stdout)
+ sys.exit(build(parse_args()))
diff --git a/tidal-link/link/ci/check-formatting.py b/tidal-link/link/ci/check-formatting.py
new file mode 100644
index 000000000..d95eb1000
--- /dev/null
+++ b/tidal-link/link/ci/check-formatting.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+
+import argparse
+import logging
+import os
+import subprocess
+import sys
+
+
+def parse_args():
+ arg_parser = argparse.ArgumentParser()
+
+ arg_parser.add_argument(
+ '-c', '--clang-format',
+ default='clang-format-6.0',
+ help='Path to clang-format executable')
+
+ arg_parser.add_argument(
+ '-f', '--fix', action='store_true',
+ help='Automatically fix all files with formatting errors')
+
+ return arg_parser.parse_args(sys.argv[1:])
+
+
+def parse_clang_xml(xml):
+ for line in xml.splitlines():
+ if line.startswith(b' <
+# \___/|_| |_|_/_/\_\
+#
+
+if(UNIX)
+ # Common flags for all Unix compilers
+ set(build_flags_DEBUG_LIST
+ "-DDEBUG=1"
+ )
+ set(build_flags_RELEASE_LIST
+ "-DNDEBUG=1"
+ )
+
+ # Clang-specific flags
+ if(${CMAKE_CXX_COMPILER_ID} MATCHES Clang)
+ set(build_flags_COMMON_LIST
+ ${build_flags_COMMON_LIST}
+ "-Weverything"
+ "-Werror"
+ "-Wno-c++98-compat"
+ "-Wno-c++98-compat-pedantic"
+ "-Wno-deprecated"
+ "-Wno-disabled-macro-expansion"
+ "-Wno-exit-time-destructors"
+ "-Wno-padded"
+ "-Wno-poison-system-directories"
+ "-Wno-reserved-id-macro"
+ "-Wno-unknown-warning-option"
+ "-Wno-unused-member-function"
+ )
+
+ # GCC-specific flags
+ # Unfortunately, we can't use -Werror on GCC, since there is no way to suppress the
+ # warnings generated by -fpermissive.
+ elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL GNU)
+ set(build_flags_COMMON_LIST
+ ${build_flags_COMMON_LIST}
+ "-Werror"
+ "-Wno-multichar"
+ )
+ endif()
+
+ # ASan support
+ if(LINK_ENABLE_ASAN)
+ set(build_flags_COMMON_LIST
+ ${build_flags_COMMON_LIST}
+ "-fsanitize=address"
+ "-fno-omit-frame-pointer"
+ )
+ endif()
+
+# __ ___ _
+# \ \ / (_)_ __ __| | _____ _____
+# \ \ /\ / /| | '_ \ / _` |/ _ \ \ /\ / / __|
+# \ V V / | | | | | (_| | (_) \ V V /\__ \
+# \_/\_/ |_|_| |_|\__,_|\___/ \_/\_/ |___/
+#
+
+elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC)
+ add_definitions("/D_SCL_SECURE_NO_WARNINGS")
+ if(LINK_BUILD_VLD)
+ add_definitions("/DLINK_BUILD_VLD=1")
+ else()
+ add_definitions("/DLINK_BUILD_VLD=0")
+ endif()
+ if(LINK_WINDOWS_SETTHREADDESCRIPTION)
+ add_definitions("/DLINK_WINDOWS_SETTHREADDESCRIPTION")
+ endif()
+
+ set(build_flags_DEBUG_LIST
+ "/DDEBUG=1"
+ )
+ set(build_flags_RELEASE_LIST
+ "/DNDEBUG=1"
+ )
+
+ set(build_flags_COMMON_LIST
+ ${build_flags_COMMON_LIST}
+ "/MP"
+ "/Wall"
+ "/WX"
+ "/EHsc"
+
+ #############################
+ # Ignored compiler warnings #
+ #############################
+ "/wd4061" # Enumerator 'identifier' in switch of enum 'enumeration' is not explicitly handled by a case label
+ "/wd4265" # 'Class' : class has virtual functions, but destructor is not virtual
+ "/wd4350" # Behavior change: 'member1' called instead of 'member2'
+ "/wd4355" # 'This' : used in base member initializer list
+ "/wd4365" # 'Action': conversion from 'type_1' to 'type_2', signed/unsigned mismatch
+ "/wd4371" # Layout of class may have changed from a previous version of the compiler due to better packing of member 'member'
+ "/wd4503" # 'Identifier': decorated name length exceeded, name was truncated
+ "/wd4510" # 'Class': default constructor could not be generated
+ "/wd4512" # 'Class': assignment operator could not be generated
+ "/wd4514" # 'Function' : unreferenced inline function has been removed
+ "/wd4571" # Informational: catch(...) semantics changed since Visual C++ 7.1; structured exceptions (SEH) are no longer caught
+ "/wd4610" # 'Class': can never be instantiated - user defined constructor required
+ "/wd4625" # 'Derived class' : copy constructor was implicitly defined as deleted because a base class copy constructor is inaccessible or deleted
+ "/wd4626" # 'Derived class' : assignment operator was implicitly defined as deleted because a base class assignment operator is inaccessible or deleted
+ "/wd4628" # digraphs not supported with -Ze. Character sequence 'digraph' not interpreted as alternate token for 'char'
+ "/wd4640" # 'Instance': construction of local static object is not thread-safe
+ "/wd4710" # 'Function': function not inlined
+ "/wd4711" # Function 'function' selected for inline expansion
+ "/wd4723" # potential divide by 0
+ "/wd4738" # Storing 32-bit float result in memory, possible loss of performance
+ "/wd4820" # 'Bytes': bytes padding added after construct 'member_name'
+ "/wd4996" # Your code uses a function, class member, variable, or typedef that's marked deprecated
+ "/wd5045" # Compiler will insert Spectre mitigation for memory load if /Qspectre switch specified
+ "/wd5204" # 'class' : class has virtual functions, but destructor is not virtual
+ )
+
+ if(MSVC_VERSION VERSION_GREATER 1800)
+ set(build_flags_COMMON_LIST
+ ${build_flags_COMMON_LIST}
+ "/wd4464" # Relative include path contains '..'
+ "/wd4548" # Expression before comma has no effect; expected expression with side-effect
+ "/wd4623" # 'Derived class': default constructor could not be generated because a base class default constructor is inaccessible
+ "/wd4868" # Compiler may not enforce left-to-right evaluation order in braced initializer list
+ "/wd5026" # Move constructor was implicitly defined as deleted
+ "/wd5027" # Move assignment operator was implicitly defined as deleted
+ )
+ endif()
+
+ if(MSVC_VERSION VERSION_GREATER 1900)
+ set(build_flags_COMMON_LIST
+ ${build_flags_COMMON_LIST}
+ "/wd4987" # nonstandard extension used: 'throw (...)'
+ "/wd4774" # 'printf_s' : format string expected in argument 1 is not a string literal
+ "/wd5039" # "pointer or reference to potentially throwing function passed to extern C function under -EHc. Undefined behavior may occur if this function throws an exception."
+ )
+ endif()
+
+ if(NOT LINK_BUILD_ASIO)
+ set(build_flags_COMMON_LIST
+ ${build_flags_COMMON_LIST}
+ "/wd4917" # 'Symbol': a GUID can only be associated with a class, interface or namespace
+ # This compiler warning is generated by Microsoft's own ocidl.h, which is
+ # used when building the WASAPI driver.
+ )
+ endif()
+endif()
+
+# ____ _ __ _
+# / ___| ___| |_ / _| | __ _ __ _ ___
+# \___ \ / _ \ __| | |_| |/ _` |/ _` / __|
+# ___) | __/ |_ | _| | (_| | (_| \__ \
+# |____/ \___|\__| |_| |_|\__,_|\__, |___/
+# |___/
+
+# Translate lists to strings
+string(REPLACE ";" " " build_flags_COMMON "${build_flags_COMMON_LIST}")
+string(REPLACE ";" " " build_flags_DEBUG "${build_flags_DEBUG_LIST}")
+string(REPLACE ";" " " build_flags_RELEASE "${build_flags_RELEASE_LIST}")
+
+# Set flags for different build types
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${build_flags_COMMON}")
+set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${build_flags_DEBUG}")
+set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} ${build_flags_RELEASE}")
diff --git a/tidal-link/link/examples/CMakeLists.txt b/tidal-link/link/examples/CMakeLists.txt
new file mode 100644
index 000000000..f75b00b1b
--- /dev/null
+++ b/tidal-link/link/examples/CMakeLists.txt
@@ -0,0 +1,223 @@
+cmake_minimum_required(VERSION 3.0)
+project(LinkExamples)
+
+# _ ____ ___ ___
+# / \ / ___|_ _/ _ \
+# / _ \ \___ \| | | | |
+# / ___ \ ___) | | |_| |
+# /_/ \_\____/___\___/
+#
+
+if(WIN32)
+ function(configure_asio asio_sdk_path_OUT)
+ # ASIO-related path/file variables
+ set(asio_download_root "https:/download.steinberg.net/sdk_downloads")
+ set(asio_file_name "asiosdk_2.3.3_2019-06-14.zip")
+ set(asio_dir_name "asiosdk_2.3.3_2019-06-14")
+ set(asio_working_dir "${CMAKE_BINARY_DIR}/modules")
+ set(asio_output_path "${asio_working_dir}/${asio_file_name}")
+
+ message(STATUS "Downloading ASIO SDK")
+ file(DOWNLOAD "${asio_download_root}/${asio_file_name}" ${asio_output_path})
+ file(SHA1 ${asio_output_path} asio_zip_hash)
+ message(" ASIO SDK SHA1: ${asio_zip_hash}")
+
+ message(" Extracting ASIO SDK")
+ execute_process(COMMAND ${CMAKE_COMMAND} -E tar "xf" ${asio_output_path} --format=zip
+ WORKING_DIRECTORY ${asio_working_dir}
+ INPUT_FILE ${asio_output_path}
+ )
+
+ # Set the ASIO SDK path for the caller
+ set(${asio_sdk_path_OUT} "${asio_working_dir}/${asio_dir_name}" PARENT_SCOPE)
+ endfunction()
+endif()
+
+# _ _ _
+# / \ _ _ __| (_) ___
+# / _ \| | | |/ _` | |/ _ \
+# / ___ \ |_| | (_| | | (_) |
+# /_/ \_\__,_|\__,_|_|\___/
+#
+
+set(linkhut_audio_SOURCES)
+
+if(APPLE)
+ set(linkhut_audio_SOURCES
+ linkaudio/AudioPlatform_CoreAudio.hpp
+ linkaudio/AudioPlatform_CoreAudio.cpp
+ )
+elseif(WIN32)
+ if(LINK_BUILD_ASIO)
+ configure_asio(asio_sdk_path)
+
+ include_directories(${asio_sdk_path}/common)
+ include_directories(${asio_sdk_path}/host)
+ include_directories(${asio_sdk_path}/host/pc)
+
+ set(linkhut_audio_SOURCES
+ ${asio_sdk_path}/common/asio.cpp
+ ${asio_sdk_path}/host/asiodrivers.cpp
+ ${asio_sdk_path}/host/pc/asiolist.cpp
+ linkaudio/AudioPlatform_Asio.hpp
+ linkaudio/AudioPlatform_Asio.cpp
+ )
+ else()
+ message(WARNING "LinkHut has been configured to be built with the WASAPI audio "
+ "driver. This driver is considered experimental and has problems with low-latency "
+ "playback. Please consider using the ASIO driver instead.")
+ set(linkhut_audio_SOURCES
+ linkaudio/AudioPlatform_Wasapi.hpp
+ linkaudio/AudioPlatform_Wasapi.cpp
+ )
+ endif()
+elseif(CMAKE_SYSTEM_NAME MATCHES "Linux|kFreeBSD|GNU")
+ if(LINK_BUILD_JACK)
+ set(linkhut_audio_SOURCES
+ linkaudio/AudioPlatform_Jack.hpp
+ linkaudio/AudioPlatform_Jack.cpp
+ )
+ else()
+ set(linkhut_audio_SOURCES
+ linkaudio/AudioPlatform_Portaudio.hpp
+ linkaudio/AudioPlatform_Portaudio.cpp
+ )
+ endif()
+endif()
+
+include_directories(linkaudio)
+source_group("Audio Sources" FILES ${linkhut_audio_SOURCES})
+
+# ____
+# / ___|___ _ __ ___ _ __ ___ ___ _ __
+# | | / _ \| '_ ` _ \| '_ ` _ \ / _ \| '_ \
+# | |__| (_) | | | | | | | | | | | (_) | | | |
+# \____\___/|_| |_| |_|_| |_| |_|\___/|_| |_|
+#
+
+function(configure_linkhut_executable target)
+ if(CMAKE_SYSTEM_NAME MATCHES "Linux|kFreeBSD|GNU")
+ target_link_libraries(${target} atomic pthread)
+ endif()
+
+ target_link_libraries(${target} Ableton::Link)
+endfunction()
+
+function(configure_linkhut_audio_sources target)
+ if(APPLE)
+ target_link_libraries(${target} "-framework AudioUnit")
+ target_compile_definitions(${target} PRIVATE
+ -DLINKHUT_AUDIO_PLATFORM_COREAUDIO=1
+ )
+ elseif(CMAKE_SYSTEM_NAME MATCHES "Linux|kFreeBSD|GNU")
+ if(LINK_BUILD_JACK)
+ target_link_libraries(${target} jack)
+ target_compile_definitions(${target} PRIVATE
+ -DLINKHUT_AUDIO_PLATFORM_JACK=1
+ )
+ else()
+ target_link_libraries(${target} asound portaudio)
+ target_compile_definitions(${target} PRIVATE
+ -DLINKHUT_AUDIO_PLATFORM_PORTAUDIO=1
+ )
+ endif()
+ elseif(WIN32)
+ if(LINK_BUILD_ASIO)
+ # ASIO uses lots of old-school string APIs from the C stdlib
+ add_definitions("/D_CRT_SECURE_NO_WARNINGS")
+ target_compile_definitions(${target} PRIVATE
+ -DLINKHUT_AUDIO_PLATFORM_ASIO=1
+ )
+ else()
+ target_compile_definitions(${target} PRIVATE
+ -DLINKHUT_AUDIO_PLATFORM_WASAPI=1
+ )
+ endif()
+
+ target_link_libraries(${target} winmm)
+ endif()
+
+endfunction()
+
+if(WIN32)
+ # When building LinkHut, additional warnings are generated from third-party frameworks
+ set(extra_ignored_warnings_LIST
+ "/wd4127" # conditional expression is constant
+ "/wd4242" # 'identifier' : conversion from 'type1' to 'type2', possible loss of data
+ "/wd4619" # #pragma warning : there is no warning number 'number'
+ "/wd4668" # 'symbol' is not defined as a preprocessor macro, replacing with '0' for 'directives'
+ "/wd4702" # unreachable code
+ "/wd4946" # reinterpret_cast used between related classes: 'class1' and 'class2'
+ )
+ if(LINK_BUILD_ASIO)
+ set(extra_ignored_warnings_LIST
+ ${extra_ignored_warnings_LIST}
+ "/wd4267" # 'argument': conversion from '?' to '?', possible loss of data
+ "/wd4477" # 'printf': format string '%?' requires an argument of type '?'
+ )
+ else()
+ set(extra_ignored_warnings_LIST
+ ${extra_ignored_warnings_LIST}
+ "/wd4191" # 'operator/operation' : unsafe conversion from 'type of expression' to 'type required'
+ )
+ endif()
+ string(REPLACE ";" " " extra_ignored_warnings "${extra_ignored_warnings_LIST}")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${extra_ignored_warnings}")
+endif()
+
+# _ _ _ _ _ _
+# | | (_)_ __ | | _| | | |_ _| |_
+# | | | | '_ \| |/ / |_| | | | | __|
+# | |___| | | | | <| _ | |_| | |_
+# |_____|_|_| |_|_|\_\_| |_|\__,_|\__|
+#
+
+set(linkhut_HEADERS
+ linkaudio/AudioEngine.hpp
+ linkaudio/AudioPlatform.hpp
+ ${link_HEADERS}
+)
+
+set(linkhut_SOURCES
+ linkaudio/AudioEngine.cpp
+ linkhut/main.cpp
+)
+
+add_executable(LinkHut
+ ${linkhut_HEADERS}
+ ${linkhut_SOURCES}
+ ${linkhut_audio_SOURCES}
+)
+configure_linkhut_audio_sources(LinkHut)
+configure_linkhut_executable(LinkHut)
+source_group("LinkHut" FILES ${linkhut_HEADERS} ${linkhut_SOURCES})
+
+# _ _ _ _ _ _ ____ _ _ _
+# | | (_)_ __ | | _| | | |_ _| |_/ ___|(_) | ___ _ __ | |_
+# | | | | '_ \| |/ / |_| | | | | __\___ \| | |/ _ \ '_ \| __|
+# | |___| | | | | <| _ | |_| | |_ ___) | | | __/ | | | |_
+# |_____|_|_| |_|_|\_\_| |_|\__,_|\__|____/|_|_|\___|_| |_|\__|
+#
+
+set(linkhutsilent_HEADERS
+ linkaudio/AudioEngine.hpp
+ linkaudio/AudioPlatform_Dummy.hpp
+ ${link_HEADERS}
+)
+
+set(linkhutsilent_SOURCES
+ linkaudio/AudioEngine.cpp
+ linkhut/main.cpp
+)
+
+add_executable(LinkHutSilent
+ ${linkhutsilent_HEADERS}
+ ${linkhutsilent_SOURCES}
+)
+
+target_compile_definitions(LinkHutSilent PRIVATE
+ -DLINKHUT_AUDIO_PLATFORM_DUMMY=1
+)
+
+configure_linkhut_executable(LinkHutSilent)
+source_group("LinkHutSilent" FILES ${linkhutsilent_HEADERS} ${linkhutsilent_SOURCES})
diff --git a/tidal-link/link/examples/esp32/.gitignore b/tidal-link/link/examples/esp32/.gitignore
new file mode 100644
index 000000000..d054d8439
--- /dev/null
+++ b/tidal-link/link/examples/esp32/.gitignore
@@ -0,0 +1,3 @@
+build
+sdkconfig
+sdkconfig.old
diff --git a/tidal-link/link/examples/esp32/CMakeLists.txt b/tidal-link/link/examples/esp32/CMakeLists.txt
new file mode 100644
index 000000000..b6d956d74
--- /dev/null
+++ b/tidal-link/link/examples/esp32/CMakeLists.txt
@@ -0,0 +1,6 @@
+cmake_minimum_required(VERSION 3.5)
+
+set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
+
+include($ENV{IDF_PATH}/tools/cmake/project.cmake)
+project(link_esp32_example)
\ No newline at end of file
diff --git a/tidal-link/link/examples/esp32/README.md b/tidal-link/link/examples/esp32/README.md
new file mode 100644
index 000000000..3379db627
--- /dev/null
+++ b/tidal-link/link/examples/esp32/README.md
@@ -0,0 +1,12 @@
+# *E X P E R I M E N T A L*
+
+*Tested with esp-idf [v4.3.1](https://github.com/espressif/esp-idf/releases/tag/v4.3.1)*
+
+## Building and Running the Example
+
+* Setup esp-idf as described in [the documentation](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html)
+* Run `idf.py menuconfig` and setup WiFi credentials under
+`Example Connection Configuration`
+```
+idf.py build
+idf.py -p ${ESP32_SERIAL_PORT} flash
diff --git a/tidal-link/link/examples/esp32/main/CMakeLists.txt b/tidal-link/link/examples/esp32/main/CMakeLists.txt
new file mode 100644
index 000000000..64756eed6
--- /dev/null
+++ b/tidal-link/link/examples/esp32/main/CMakeLists.txt
@@ -0,0 +1,11 @@
+idf_component_register(SRCS main.cpp)
+
+if(NOT DEFINED LINK_ESP_TASK_CORE_ID)
+ set(LINK_ESP_TASK_CORE_ID tskNO_AFFINITY)
+endif()
+target_compile_definitions(${COMPONENT_LIB} PRIVATE LINK_ESP_TASK_CORE_ID=${LINK_ESP_TASK_CORE_ID})
+
+target_compile_options(${COMPONENT_LIB} PRIVATE -fexceptions)
+
+include(../../../AbletonLinkConfig.cmake)
+target_link_libraries(${COMPONENT_TARGET} Ableton::Link)
diff --git a/tidal-link/link/examples/esp32/main/main.cpp b/tidal-link/link/examples/esp32/main/main.cpp
new file mode 100644
index 000000000..b44f45923
--- /dev/null
+++ b/tidal-link/link/examples/esp32/main/main.cpp
@@ -0,0 +1,109 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define LED GPIO_NUM_2
+#define PRINT_LINK_STATE false
+
+unsigned int if_nametoindex(const char* ifName)
+{
+ return 0;
+}
+
+char* if_indextoname(unsigned int ifIndex, char* ifName)
+{
+ return nullptr;
+}
+
+void IRAM_ATTR timer_group0_isr(void* userParam)
+{
+ static BaseType_t xHigherPriorityTaskWoken = pdFALSE;
+
+ TIMERG0.int_clr_timers.t0 = 1;
+ TIMERG0.hw_timer[0].config.alarm_en = 1;
+
+ xSemaphoreGiveFromISR(userParam, &xHigherPriorityTaskWoken);
+ if (xHigherPriorityTaskWoken)
+ {
+ portYIELD_FROM_ISR();
+ }
+}
+
+void timerGroup0Init(int timerPeriodUS, void* userParam)
+{
+ timer_config_t config = {.alarm_en = TIMER_ALARM_EN,
+ .counter_en = TIMER_PAUSE,
+ .intr_type = TIMER_INTR_LEVEL,
+ .counter_dir = TIMER_COUNT_UP,
+ .auto_reload = TIMER_AUTORELOAD_EN,
+ .divider = 80};
+
+ timer_init(TIMER_GROUP_0, TIMER_0, &config);
+ timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0);
+ timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, timerPeriodUS);
+ timer_enable_intr(TIMER_GROUP_0, TIMER_0);
+ timer_isr_register(TIMER_GROUP_0, TIMER_0, &timer_group0_isr, userParam, 0, nullptr);
+
+ timer_start(TIMER_GROUP_0, TIMER_0);
+}
+
+void printTask(void* userParam)
+{
+ auto link = static_cast(userParam);
+ const auto quantum = 4.0;
+
+ while (true)
+ {
+ const auto sessionState = link->captureAppSessionState();
+ const auto numPeers = link->numPeers();
+ const auto time = link->clock().micros();
+ const auto beats = sessionState.beatAtTime(time, quantum);
+ std::cout << std::defaultfloat << "| peers: " << numPeers << " | "
+ << "tempo: " << sessionState.tempo() << " | " << std::fixed
+ << "beats: " << beats << " |" << std::endl;
+ vTaskDelay(800 / portTICK_PERIOD_MS);
+ }
+}
+
+void tickTask(void* userParam)
+{
+ SemaphoreHandle_t handle = static_cast(userParam);
+ ableton::Link link(120.0f);
+ link.enable(true);
+
+ if (PRINT_LINK_STATE)
+ {
+ xTaskCreate(printTask, "print", 8192, &link, 1, nullptr);
+ }
+
+ gpio_set_direction(LED, GPIO_MODE_OUTPUT);
+
+ while (true)
+ {
+ xSemaphoreTake(handle, portMAX_DELAY);
+
+ const auto state = link.captureAudioSessionState();
+ const auto phase = state.phaseAtTime(link.clock().micros(), 1.);
+ gpio_set_level(LED, fmodf(phase, 1.) < 0.1);
+ portYIELD();
+ }
+}
+
+extern "C" void app_main()
+{
+ ESP_ERROR_CHECK(nvs_flash_init());
+ esp_netif_init();
+ ESP_ERROR_CHECK(esp_event_loop_create_default());
+ ESP_ERROR_CHECK(example_connect());
+
+ SemaphoreHandle_t tickSemphr = xSemaphoreCreateBinary();
+ timerGroup0Init(100, tickSemphr);
+
+ xTaskCreate(tickTask, "tick", 8192, tickSemphr, configMAX_PRIORITIES - 1, nullptr);
+}
diff --git a/tidal-link/link/examples/esp32/sdkconfig.defaults b/tidal-link/link/examples/esp32/sdkconfig.defaults
new file mode 100644
index 000000000..75ebeae1c
--- /dev/null
+++ b/tidal-link/link/examples/esp32/sdkconfig.defaults
@@ -0,0 +1 @@
+CONFIG_COMPILER_CXX_EXCEPTIONS=y
diff --git a/tidal-link/link/examples/linkaudio/AudioEngine.cpp b/tidal-link/link/examples/linkaudio/AudioEngine.cpp
new file mode 100644
index 000000000..45df3a26e
--- /dev/null
+++ b/tidal-link/link/examples/linkaudio/AudioEngine.cpp
@@ -0,0 +1,240 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#include "AudioEngine.hpp"
+
+// Make sure to define this before is included for Windows
+#ifdef LINK_PLATFORM_WINDOWS
+#define _USE_MATH_DEFINES
+#endif
+#include
+#include
+
+namespace ableton
+{
+namespace linkaudio
+{
+
+AudioEngine::AudioEngine(Link& link)
+ : mLink(link)
+ , mSampleRate(44100.)
+ , mOutputLatency(std::chrono::microseconds{0})
+ , mSharedEngineData({0., false, false, 4., false})
+ , mLockfreeEngineData(mSharedEngineData)
+ , mTimeAtLastClick{}
+ , mIsPlaying(false)
+{
+ if (!mOutputLatency.is_lock_free())
+ {
+ std::cout << "WARNING: AudioEngine::mOutputLatency is not lock free!" << std::endl;
+ }
+}
+
+void AudioEngine::startPlaying()
+{
+ std::lock_guard lock(mEngineDataGuard);
+ mSharedEngineData.requestStart = true;
+}
+
+void AudioEngine::stopPlaying()
+{
+ std::lock_guard lock(mEngineDataGuard);
+ mSharedEngineData.requestStop = true;
+}
+
+bool AudioEngine::isPlaying() const
+{
+ return mLink.captureAppSessionState().isPlaying();
+}
+
+double AudioEngine::beatTime() const
+{
+ const auto sessionState = mLink.captureAppSessionState();
+ return sessionState.beatAtTime(mLink.clock().micros(), mSharedEngineData.quantum);
+}
+
+void AudioEngine::setTempo(double tempo)
+{
+ std::lock_guard lock(mEngineDataGuard);
+ mSharedEngineData.requestedTempo = tempo;
+}
+
+double AudioEngine::quantum() const
+{
+ return mSharedEngineData.quantum;
+}
+
+void AudioEngine::setQuantum(double quantum)
+{
+ std::lock_guard lock(mEngineDataGuard);
+ mSharedEngineData.quantum = quantum;
+}
+
+bool AudioEngine::isStartStopSyncEnabled() const
+{
+ return mLink.isStartStopSyncEnabled();
+}
+
+void AudioEngine::setStartStopSyncEnabled(const bool enabled)
+{
+ mLink.enableStartStopSync(enabled);
+}
+
+void AudioEngine::setBufferSize(std::size_t size)
+{
+ mBuffer = std::vector(size, 0.);
+}
+
+void AudioEngine::setSampleRate(double sampleRate)
+{
+ mSampleRate = sampleRate;
+}
+
+AudioEngine::EngineData AudioEngine::pullEngineData()
+{
+ auto engineData = EngineData{};
+ if (mEngineDataGuard.try_lock())
+ {
+ engineData.requestedTempo = mSharedEngineData.requestedTempo;
+ mSharedEngineData.requestedTempo = 0;
+ engineData.requestStart = mSharedEngineData.requestStart;
+ mSharedEngineData.requestStart = false;
+ engineData.requestStop = mSharedEngineData.requestStop;
+ mSharedEngineData.requestStop = false;
+
+ mLockfreeEngineData.quantum = mSharedEngineData.quantum;
+ mLockfreeEngineData.startStopSyncOn = mSharedEngineData.startStopSyncOn;
+
+ mEngineDataGuard.unlock();
+ }
+ engineData.quantum = mLockfreeEngineData.quantum;
+
+ return engineData;
+}
+
+void AudioEngine::renderMetronomeIntoBuffer(const Link::SessionState sessionState,
+ const double quantum,
+ const std::chrono::microseconds beginHostTime,
+ const std::size_t numSamples)
+{
+ using namespace std::chrono;
+
+ // Metronome frequencies
+ static const double highTone = 1567.98;
+ static const double lowTone = 1108.73;
+ // 100ms click duration
+ static const auto clickDuration = duration{0.1};
+
+ // The number of microseconds that elapse between samples
+ const auto microsPerSample = 1e6 / mSampleRate;
+
+ for (std::size_t i = 0; i < numSamples; ++i)
+ {
+ double amplitude = 0.;
+ // Compute the host time for this sample and the last.
+ const auto hostTime =
+ beginHostTime + microseconds(llround(static_cast(i) * microsPerSample));
+ const auto lastSampleHostTime = hostTime - microseconds(llround(microsPerSample));
+
+ // Only make sound for positive beat magnitudes. Negative beat
+ // magnitudes are count-in beats.
+ if (sessionState.beatAtTime(hostTime, quantum) >= 0.)
+ {
+ // If the phase wraps around between the last sample and the
+ // current one with respect to a 1 beat quantum, then a click
+ // should occur.
+ if (sessionState.phaseAtTime(hostTime, 1)
+ < sessionState.phaseAtTime(lastSampleHostTime, 1))
+ {
+ mTimeAtLastClick = hostTime;
+ }
+
+ const auto secondsAfterClick =
+ duration_cast>(hostTime - mTimeAtLastClick);
+
+ // If we're within the click duration of the last beat, render
+ // the click tone into this sample
+ if (secondsAfterClick < clickDuration)
+ {
+ // If the phase of the last beat with respect to the current
+ // quantum was zero, then it was at a quantum boundary and we
+ // want to use the high tone. For other beats within the
+ // quantum, use the low tone.
+ const auto freq =
+ floor(sessionState.phaseAtTime(hostTime, quantum)) == 0 ? highTone : lowTone;
+
+ // Simple cosine synth
+ amplitude = cos(2 * M_PI * secondsAfterClick.count() * freq)
+ * (1 - sin(5 * M_PI * secondsAfterClick.count()));
+ }
+ }
+ mBuffer[i] = amplitude;
+ }
+}
+
+void AudioEngine::audioCallback(
+ const std::chrono::microseconds hostTime, const std::size_t numSamples)
+{
+ const auto engineData = pullEngineData();
+
+ auto sessionState = mLink.captureAudioSessionState();
+
+ // Clear the buffer
+ std::fill(mBuffer.begin(), mBuffer.end(), 0);
+
+ if (engineData.requestStart)
+ {
+ sessionState.setIsPlaying(true, hostTime);
+ }
+
+ if (engineData.requestStop)
+ {
+ sessionState.setIsPlaying(false, hostTime);
+ }
+
+ if (!mIsPlaying && sessionState.isPlaying())
+ {
+ // Reset the timeline so that beat 0 corresponds to the time when transport starts
+ sessionState.requestBeatAtStartPlayingTime(0, engineData.quantum);
+ mIsPlaying = true;
+ }
+ else if (mIsPlaying && !sessionState.isPlaying())
+ {
+ mIsPlaying = false;
+ }
+
+ if (engineData.requestedTempo > 0)
+ {
+ // Set the newly requested tempo from the beginning of this buffer
+ sessionState.setTempo(engineData.requestedTempo, hostTime);
+ }
+
+ // Timeline modifications are complete, commit the results
+ mLink.commitAudioSessionState(sessionState);
+
+ if (mIsPlaying)
+ {
+ // As long as the engine is playing, generate metronome clicks in
+ // the buffer at the appropriate beats.
+ renderMetronomeIntoBuffer(sessionState, engineData.quantum, hostTime, numSamples);
+ }
+}
+
+} // namespace linkaudio
+} // namespace ableton
diff --git a/tidal-link/link/examples/linkaudio/AudioEngine.hpp b/tidal-link/link/examples/linkaudio/AudioEngine.hpp
new file mode 100644
index 000000000..f9c94d51c
--- /dev/null
+++ b/tidal-link/link/examples/linkaudio/AudioEngine.hpp
@@ -0,0 +1,81 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#pragma once
+
+// Make sure to define this before is included for Windows
+#define _USE_MATH_DEFINES
+#include
+#include
+#include
+
+namespace ableton
+{
+namespace linkaudio
+{
+
+class AudioEngine
+{
+public:
+ AudioEngine(Link& link);
+ void startPlaying();
+ void stopPlaying();
+ bool isPlaying() const;
+ double beatTime() const;
+ void setTempo(double tempo);
+ double quantum() const;
+ void setQuantum(double quantum);
+ bool isStartStopSyncEnabled() const;
+ void setStartStopSyncEnabled(bool enabled);
+
+private:
+ struct EngineData
+ {
+ double requestedTempo;
+ bool requestStart;
+ bool requestStop;
+ double quantum;
+ bool startStopSyncOn;
+ };
+
+ void setBufferSize(std::size_t size);
+ void setSampleRate(double sampleRate);
+ EngineData pullEngineData();
+ void renderMetronomeIntoBuffer(Link::SessionState sessionState,
+ double quantum,
+ std::chrono::microseconds beginHostTime,
+ std::size_t numSamples);
+ void audioCallback(const std::chrono::microseconds hostTime, std::size_t numSamples);
+
+ Link& mLink;
+ double mSampleRate;
+ std::atomic mOutputLatency;
+ std::vector mBuffer;
+ EngineData mSharedEngineData;
+ EngineData mLockfreeEngineData;
+ std::chrono::microseconds mTimeAtLastClick;
+ bool mIsPlaying;
+ std::mutex mEngineDataGuard;
+
+ friend class AudioPlatform;
+};
+
+
+} // namespace linkaudio
+} // namespace ableton
diff --git a/tidal-link/link/examples/linkaudio/AudioPlatform.hpp b/tidal-link/link/examples/linkaudio/AudioPlatform.hpp
new file mode 100644
index 000000000..9ebc24830
--- /dev/null
+++ b/tidal-link/link/examples/linkaudio/AudioPlatform.hpp
@@ -0,0 +1,44 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#pragma once
+
+#if defined(LINKHUT_AUDIO_PLATFORM_ASIO)
+#include "AudioPlatform_Asio.hpp"
+#endif
+
+#if defined(LINKHUT_AUDIO_PLATFORM_COREAUDIO)
+#include "AudioPlatform_CoreAudio.hpp"
+#endif
+
+#if defined(LINKHUT_AUDIO_PLATFORM_DUMMY)
+#include "AudioPlatform_Dummy.hpp"
+#endif
+
+#if defined(LINKHUT_AUDIO_PLATFORM_JACK)
+#include "AudioPlatform_Jack.hpp"
+#endif
+
+#if defined(LINKHUT_AUDIO_PLATFORM_PORTAUDIO)
+#include "AudioPlatform_Portaudio.hpp"
+#endif
+
+#if defined(LINKHUT_AUDIO_PLATFORM_WASAPI)
+#include "AudioPlatform_Wasapi.hpp"
+#endif
diff --git a/tidal-link/link/examples/linkaudio/AudioPlatform_Asio.cpp b/tidal-link/link/examples/linkaudio/AudioPlatform_Asio.cpp
new file mode 100644
index 000000000..8e22845c8
--- /dev/null
+++ b/tidal-link/link/examples/linkaudio/AudioPlatform_Asio.cpp
@@ -0,0 +1,315 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#include "AudioPlatform_Asio.hpp"
+
+namespace ableton
+{
+namespace linkaudio
+{
+
+void fatalError(const ASIOError result, const std::string& function)
+{
+ std::cerr << "Call to ASIO function " << function << " failed";
+ if (result != ASE_OK)
+ {
+ std::cerr << " (ASIO error code " << result << ")";
+ }
+ std::cerr << std::endl;
+ std::terminate();
+}
+
+double asioSamplesToDouble(const ASIOSamples& samples)
+{
+ return samples.lo + samples.hi * std::pow(2, 32);
+}
+
+// ASIO processing callbacks
+ASIOTime* bufferSwitchTimeInfo(ASIOTime* timeInfo, long index, ASIOBool)
+{
+ AudioPlatform* platform = AudioPlatform::singleton();
+ if (platform)
+ {
+ platform->audioCallback(timeInfo, index);
+ }
+ return nullptr;
+}
+
+void bufferSwitch(long index, ASIOBool processNow)
+{
+ ASIOTime timeInfo{};
+ ASIOError result = ASIOGetSamplePosition(
+ &timeInfo.timeInfo.samplePosition, &timeInfo.timeInfo.systemTime);
+ if (result != ASE_OK)
+ {
+ std::cerr << "ASIOGetSamplePosition failed with ASIO error: " << result << std::endl;
+ }
+ else
+ {
+ timeInfo.timeInfo.flags = kSystemTimeValid | kSamplePositionValid;
+ }
+
+ bufferSwitchTimeInfo(&timeInfo, index, processNow);
+}
+
+AudioPlatform* AudioPlatform::_singleton = nullptr;
+
+AudioPlatform* AudioPlatform::singleton()
+{
+ return _singleton;
+}
+
+void AudioPlatform::setSingleton(AudioPlatform* platform)
+{
+ _singleton = platform;
+}
+
+AudioPlatform::AudioPlatform(Link& link)
+ : mEngine(link)
+{
+ initialize();
+ mEngine.setBufferSize(mDriverInfo.preferredSize);
+ mEngine.setSampleRate(mDriverInfo.sampleRate);
+ setSingleton(this);
+ start();
+}
+
+AudioPlatform::~AudioPlatform()
+{
+ stop();
+ ASIODisposeBuffers();
+ ASIOExit();
+ if (asioDrivers != nullptr)
+ {
+ asioDrivers->removeCurrentDriver();
+ }
+
+ setSingleton(nullptr);
+}
+
+void AudioPlatform::audioCallback(ASIOTime* timeInfo, long index)
+{
+ auto hostTime = std::chrono::microseconds(0);
+ if (timeInfo->timeInfo.flags & kSystemTimeValid)
+ {
+ hostTime = mHostTimeFilter.sampleTimeToHostTime(
+ asioSamplesToDouble(timeInfo->timeInfo.samplePosition));
+ }
+
+ const auto bufferBeginAtOutput = hostTime + mEngine.mOutputLatency.load();
+
+ ASIOBufferInfo* bufferInfos = mDriverInfo.bufferInfos;
+ const long numSamples = mDriverInfo.preferredSize;
+ const long numChannels = mDriverInfo.numBuffers;
+ const double maxAmp = std::numeric_limits::max();
+
+ mEngine.audioCallback(bufferBeginAtOutput, numSamples);
+
+ for (long i = 0; i < numSamples; ++i)
+ {
+ for (long j = 0; j < numChannels; ++j)
+ {
+ int* buffer = static_cast(bufferInfos[j].buffers[index]);
+ buffer[i] = static_cast(mEngine.mBuffer[i] * maxAmp);
+ }
+ }
+
+ if (mDriverInfo.outputReady)
+ {
+ ASIOOutputReady();
+ }
+}
+
+void AudioPlatform::createAsioBuffers()
+{
+ DriverInfo& driverInfo = mDriverInfo;
+ ASIOBufferInfo* bufferInfo = driverInfo.bufferInfos;
+ driverInfo.numBuffers = 0;
+
+ // Prepare input channels. Though this is not necessarily required, the opened input
+ // channels will not work.
+ int numInputBuffers;
+ if (driverInfo.inputChannels > LINK_ASIO_INPUT_CHANNELS)
+ {
+ numInputBuffers = LINK_ASIO_INPUT_CHANNELS;
+ }
+ else
+ {
+ numInputBuffers = driverInfo.inputChannels;
+ }
+
+ for (long i = 0; i < numInputBuffers; ++i, ++bufferInfo)
+ {
+ bufferInfo->isInput = ASIOTrue;
+ bufferInfo->channelNum = i;
+ bufferInfo->buffers[0] = bufferInfo->buffers[1] = nullptr;
+ }
+
+ // Prepare output channels
+ int numOutputBuffers;
+ if (driverInfo.outputChannels > LINK_ASIO_OUTPUT_CHANNELS)
+ {
+ numOutputBuffers = LINK_ASIO_OUTPUT_CHANNELS;
+ }
+ else
+ {
+ numOutputBuffers = driverInfo.outputChannels;
+ }
+
+ for (long i = 0; i < numOutputBuffers; i++, bufferInfo++)
+ {
+ bufferInfo->isInput = ASIOFalse;
+ bufferInfo->channelNum = i;
+ bufferInfo->buffers[0] = bufferInfo->buffers[1] = nullptr;
+ }
+
+ driverInfo.numBuffers = numInputBuffers + numOutputBuffers;
+ ASIOError result = ASIOCreateBuffers(driverInfo.bufferInfos, driverInfo.numBuffers,
+ driverInfo.preferredSize, &(mAsioCallbacks));
+ if (result != ASE_OK)
+ {
+ fatalError(result, "ASIOCreateBuffers");
+ }
+
+ // Now get all buffer details, sample word length, name, word clock group and latency
+ for (long i = 0; i < driverInfo.numBuffers; ++i)
+ {
+ driverInfo.channelInfos[i].channel = driverInfo.bufferInfos[i].channelNum;
+ driverInfo.channelInfos[i].isInput = driverInfo.bufferInfos[i].isInput;
+
+ result = ASIOGetChannelInfo(&driverInfo.channelInfos[i]);
+ if (result != ASE_OK)
+ {
+ fatalError(result, "ASIOGetChannelInfo");
+ }
+
+ std::clog << "ASIOGetChannelInfo successful, type: "
+ << (driverInfo.bufferInfos[i].isInput ? "input" : "output")
+ << ", channel: " << i
+ << ", sample type: " << driverInfo.channelInfos[i].type << std::endl;
+
+ if (driverInfo.channelInfos[i].type != ASIOSTInt32LSB)
+ {
+ fatalError(ASE_OK, "Unsupported sample type!");
+ }
+ }
+
+ long inputLatency, outputLatency;
+ result = ASIOGetLatencies(&inputLatency, &outputLatency);
+ if (result != ASE_OK)
+ {
+ fatalError(result, "ASIOGetLatencies");
+ }
+ std::clog << "Driver input latency: " << inputLatency << "usec"
+ << ", output latency: " << outputLatency << "usec" << std::endl;
+
+ const double bufferSize = driverInfo.preferredSize / driverInfo.sampleRate;
+ auto outputLatencyMicros =
+ std::chrono::microseconds(llround(outputLatency / driverInfo.sampleRate));
+ outputLatencyMicros += std::chrono::microseconds(llround(1.0e6 * bufferSize));
+
+ mEngine.mOutputLatency.store(outputLatencyMicros);
+
+ std::clog << "Total latency: " << outputLatencyMicros.count() << "usec" << std::endl;
+}
+
+void AudioPlatform::initializeDriverInfo()
+{
+ ASIOError result =
+ ASIOGetChannels(&mDriverInfo.inputChannels, &mDriverInfo.outputChannels);
+ if (result != ASE_OK)
+ {
+ fatalError(result, "ASIOGetChannels");
+ }
+ std::clog << "ASIOGetChannels succeeded, inputs:" << mDriverInfo.inputChannels
+ << ", outputs: " << mDriverInfo.outputChannels << std::endl;
+
+ long minSize, maxSize, granularity;
+ result =
+ ASIOGetBufferSize(&minSize, &maxSize, &mDriverInfo.preferredSize, &granularity);
+ if (result != ASE_OK)
+ {
+ fatalError(result, "ASIOGetBufferSize");
+ }
+ std::clog << "ASIOGetBufferSize succeeded, min: " << minSize << ", max: " << maxSize
+ << ", preferred: " << mDriverInfo.preferredSize
+ << ", granularity: " << granularity << std::endl;
+
+ result = ASIOGetSampleRate(&mDriverInfo.sampleRate);
+ if (result != ASE_OK)
+ {
+ fatalError(result, "ASIOGetSampleRate");
+ }
+ std::clog << "ASIOGetSampleRate succeeded, sampleRate: " << mDriverInfo.sampleRate
+ << "Hz" << std::endl;
+
+ // Check wether the driver requires the ASIOOutputReady() optimization, which can be
+ // used by the driver to reduce output latency by one block
+ mDriverInfo.outputReady = (ASIOOutputReady() == ASE_OK);
+ std::clog << "ASIOOutputReady optimization is "
+ << (mDriverInfo.outputReady ? "enabled" : "disabled") << std::endl;
+}
+
+void AudioPlatform::initialize()
+{
+ if (!loadAsioDriver(LINK_ASIO_DRIVER_NAME))
+ {
+ std::cerr << "Failed opening ASIO driver for device named '" << LINK_ASIO_DRIVER_NAME
+ << "', is the driver installed?" << std::endl;
+ std::terminate();
+ }
+
+ ASIOError result = ASIOInit(&mDriverInfo.driverInfo);
+ if (result != ASE_OK)
+ {
+ fatalError(result, "ASIOInit");
+ }
+
+ std::clog << "ASIOInit succeeded, asioVersion: " << mDriverInfo.driverInfo.asioVersion
+ << ", driverVersion: " << mDriverInfo.driverInfo.driverVersion
+ << ", name: " << mDriverInfo.driverInfo.name << std::endl;
+
+ initializeDriverInfo();
+
+ ASIOCallbacks* callbacks = &(mAsioCallbacks);
+ callbacks->bufferSwitch = &bufferSwitch;
+ callbacks->bufferSwitchTimeInfo = &bufferSwitchTimeInfo;
+ createAsioBuffers();
+}
+
+void AudioPlatform::start()
+{
+ ASIOError result = ASIOStart();
+ if (result != ASE_OK)
+ {
+ fatalError(result, "ASIOStart");
+ }
+}
+
+void AudioPlatform::stop()
+{
+ ASIOError result = ASIOStop();
+ if (result != ASE_OK)
+ {
+ fatalError(result, "ASIOStop");
+ }
+}
+
+} // namespace linkaudio
+} // namespace ableton
diff --git a/tidal-link/link/examples/linkaudio/AudioPlatform_Asio.hpp b/tidal-link/link/examples/linkaudio/AudioPlatform_Asio.hpp
new file mode 100644
index 000000000..b3286744c
--- /dev/null
+++ b/tidal-link/link/examples/linkaudio/AudioPlatform_Asio.hpp
@@ -0,0 +1,103 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#pragma once
+
+#include "AudioEngine.hpp"
+
+#include
+#include
+
+#include "asiosys.h" // Should be included before asio.h
+
+#include "asio.h"
+#include "asiodrivers.h"
+
+// External functions in the ASIO SDK which aren't declared in the SDK headers
+extern AsioDrivers* asioDrivers;
+bool loadAsioDriver(char* name);
+
+namespace ableton
+{
+namespace linkaudio
+{
+
+#ifndef LINK_ASIO_DRIVER_NAME
+#define LINK_ASIO_DRIVER_NAME "ASIO4ALL v2"
+#endif
+#ifndef LINK_ASIO_INPUT_CHANNELS
+#define LINK_ASIO_INPUT_CHANNELS 0
+#endif
+#ifndef LINK_ASIO_OUTPUT_CHANNELS
+#define LINK_ASIO_OUTPUT_CHANNELS 2
+#endif
+
+struct DriverInfo
+{
+ ASIODriverInfo driverInfo;
+ long inputChannels;
+ long outputChannels;
+ long preferredSize;
+ ASIOSampleRate sampleRate;
+ bool outputReady;
+ long numBuffers;
+ ASIOBufferInfo bufferInfos[LINK_ASIO_INPUT_CHANNELS + LINK_ASIO_OUTPUT_CHANNELS];
+ ASIOChannelInfo channelInfos[LINK_ASIO_INPUT_CHANNELS + LINK_ASIO_OUTPUT_CHANNELS];
+};
+
+// Helper functions
+
+// Convenience function to print out an ASIO error code along with the function called
+void fatalError(const ASIOError result, const std::string& function);
+double asioSamplesToDouble(const ASIOSamples& samples);
+
+ASIOTime* bufferSwitchTimeInfo(ASIOTime* timeInfo, long index, ASIOBool);
+void bufferSwitch(long index, ASIOBool processNow);
+
+class AudioPlatform
+{
+public:
+ AudioPlatform(Link& link);
+ ~AudioPlatform();
+
+ void audioCallback(ASIOTime* timeInfo, long index);
+
+ AudioEngine mEngine;
+
+ // Unfortunately, the ASIO SDK does not allow passing void* user data to callback
+ // functions, so we need to keep a singleton instance of the audio engine
+ static AudioPlatform* singleton();
+ static void setSingleton(AudioPlatform* platform);
+
+private:
+ void createAsioBuffers();
+ void initializeDriverInfo();
+ void initialize();
+ void start();
+ void stop();
+
+ DriverInfo mDriverInfo;
+ ASIOCallbacks mAsioCallbacks;
+ link::HostTimeFilter mHostTimeFilter;
+
+ static AudioPlatform* _singleton;
+};
+
+} // namespace linkaudio
+} // namespace ableton
diff --git a/tidal-link/link/examples/linkaudio/AudioPlatform_CoreAudio.cpp b/tidal-link/link/examples/linkaudio/AudioPlatform_CoreAudio.cpp
new file mode 100644
index 000000000..d4b4552b8
--- /dev/null
+++ b/tidal-link/link/examples/linkaudio/AudioPlatform_CoreAudio.cpp
@@ -0,0 +1,214 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#include "AudioPlatform_CoreAudio.hpp"
+#include
+#include
+#include
+
+namespace ableton
+{
+namespace linkaudio
+{
+
+AudioPlatform::AudioPlatform(Link& link)
+ : mEngine(link)
+{
+ initialize();
+ start();
+}
+
+AudioPlatform::~AudioPlatform()
+{
+ stop();
+ uninitialize();
+}
+
+OSStatus AudioPlatform::audioCallback(void* inRefCon,
+ AudioUnitRenderActionFlags*,
+ const AudioTimeStamp* inTimeStamp,
+ UInt32,
+ UInt32 inNumberFrames,
+ AudioBufferList* ioData)
+{
+ AudioEngine* engine = static_cast(inRefCon);
+
+ const auto bufferBeginAtOutput =
+ engine->mLink.clock().ticksToMicros(inTimeStamp->mHostTime)
+ + engine->mOutputLatency.load();
+
+ engine->audioCallback(bufferBeginAtOutput, inNumberFrames);
+
+ for (std::size_t i = 0; i < inNumberFrames; ++i)
+ {
+ for (UInt32 j = 0; j < ioData->mNumberBuffers; ++j)
+ {
+ SInt16* bufData = static_cast(ioData->mBuffers[j].mData);
+ bufData[i] = static_cast(32761. * engine->mBuffer[i]);
+ }
+ }
+
+ return noErr;
+}
+
+void AudioPlatform::initialize()
+{
+ AudioComponentDescription cd = {};
+ cd.componentManufacturer = kAudioUnitManufacturer_Apple;
+ cd.componentFlags = 0;
+ cd.componentFlagsMask = 0;
+ cd.componentType = kAudioUnitType_Output;
+ cd.componentSubType = kAudioUnitSubType_DefaultOutput;
+
+ AudioComponent component = AudioComponentFindNext(nullptr, &cd);
+ OSStatus result = AudioComponentInstanceNew(component, &mIoUnit);
+ if (result)
+ {
+ std::cerr << "Could not get Audio Unit. " << result << std::endl;
+ std::terminate();
+ }
+
+ UInt32 size = sizeof(mEngine.mSampleRate);
+ result = AudioUnitGetProperty(mIoUnit, kAudioUnitProperty_SampleRate,
+ kAudioUnitScope_Output, 0, &mEngine.mSampleRate, &size);
+ if (result)
+ {
+ std::cerr << "Could not get sample rate. " << result << std::endl;
+ std::terminate();
+ }
+ std::clog << "SAMPLE RATE: " << mEngine.mSampleRate << std::endl;
+
+ AudioStreamBasicDescription asbd = {};
+ asbd.mFormatID = kAudioFormatLinearPCM;
+ asbd.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked
+ | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsNonInterleaved;
+ asbd.mChannelsPerFrame = 2;
+ asbd.mBytesPerPacket = sizeof(SInt16);
+ asbd.mFramesPerPacket = 1;
+ asbd.mBytesPerFrame = sizeof(SInt16);
+ asbd.mBitsPerChannel = 8 * sizeof(SInt16);
+ asbd.mSampleRate = mEngine.mSampleRate;
+
+ result = AudioUnitSetProperty(mIoUnit, kAudioUnitProperty_StreamFormat,
+ kAudioUnitScope_Input, 0, &asbd, sizeof(asbd));
+ if (result)
+ {
+ std::cerr << "Could not set stream format. " << result << std::endl;
+ }
+
+ char deviceName[512];
+ size = sizeof(deviceName);
+ result = AudioUnitGetProperty(mIoUnit, kAudioDevicePropertyDeviceName,
+ kAudioUnitScope_Global, 0, &deviceName, &size);
+ if (result)
+ {
+ std::cerr << "Could not get device name. " << result << std::endl;
+ std::terminate();
+ }
+ std::clog << "DEVICE NAME: " << deviceName << std::endl;
+
+ UInt32 bufferSize = 512;
+ size = sizeof(bufferSize);
+ result = AudioUnitSetProperty(mIoUnit, kAudioDevicePropertyBufferFrameSize,
+ kAudioUnitScope_Global, 0, &bufferSize, size);
+ if (result)
+ {
+ std::cerr << "Could not set buffer size. " << result << std::endl;
+ std::terminate();
+ }
+ mEngine.setBufferSize(bufferSize);
+
+ UInt32 propertyResult = 0;
+ size = sizeof(propertyResult);
+ result = AudioUnitGetProperty(mIoUnit, kAudioDevicePropertyBufferFrameSize,
+ kAudioUnitScope_Global, 0, &propertyResult, &size);
+ if (result)
+ {
+ std::cerr << "Could not get buffer size. " << result << std::endl;
+ std::terminate();
+ }
+ std::clog << "BUFFER SIZE: " << propertyResult << " samples, "
+ << propertyResult / mEngine.mSampleRate * 1e3 << " ms." << std::endl;
+
+ // the buffer, stream and safety-offset latencies are part of inTimeStamp->mHostTime
+ // within the audio callback.
+ UInt32 deviceLatency = 0;
+ size = sizeof(deviceLatency);
+ result = AudioUnitGetProperty(mIoUnit, kAudioDevicePropertyLatency,
+ kAudioUnitScope_Output, 0, &deviceLatency, &size);
+ if (result)
+ {
+ std::cerr << "Could not get output device latency. " << result << std::endl;
+ std::terminate();
+ }
+ std::clog << "OUTPUT DEVICE LATENCY: " << deviceLatency << " samples, "
+ << deviceLatency / mEngine.mSampleRate * 1e3 << " ms." << std::endl;
+
+ using namespace std::chrono;
+ const double latency = static_cast(deviceLatency) / mEngine.mSampleRate;
+ mEngine.mOutputLatency.store(duration_cast(duration{latency}));
+
+ AURenderCallbackStruct ioRemoteInput;
+ ioRemoteInput.inputProc = audioCallback;
+ ioRemoteInput.inputProcRefCon = &mEngine;
+
+ result = AudioUnitSetProperty(mIoUnit, kAudioUnitProperty_SetRenderCallback,
+ kAudioUnitScope_Input, 0, &ioRemoteInput, sizeof(ioRemoteInput));
+ if (result)
+ {
+ std::cerr << "Could not set render callback. " << result << std::endl;
+ }
+
+ result = AudioUnitInitialize(mIoUnit);
+ if (result)
+ {
+ std::cerr << "Could not initialize audio unit. " << result << std::endl;
+ }
+}
+
+void AudioPlatform::uninitialize()
+{
+ OSStatus result = AudioUnitUninitialize(mIoUnit);
+ if (result)
+ {
+ std::cerr << "Could not uninitialize Audio Unit. " << result << std::endl;
+ }
+}
+
+void AudioPlatform::start()
+{
+ OSStatus result = AudioOutputUnitStart(mIoUnit);
+ if (result)
+ {
+ std::cerr << "Could not start Audio Unit. " << result << std::endl;
+ std::terminate();
+ }
+}
+
+void AudioPlatform::stop()
+{
+ OSStatus result = AudioOutputUnitStop(mIoUnit);
+ if (result)
+ {
+ std::cerr << "Could not stop Audio Unit. " << result << std::endl;
+ }
+}
+
+} // namespace linkaudio
+} // namespace ableton
diff --git a/tidal-link/link/examples/linkaudio/AudioPlatform_CoreAudio.hpp b/tidal-link/link/examples/linkaudio/AudioPlatform_CoreAudio.hpp
new file mode 100644
index 000000000..7e2600fb3
--- /dev/null
+++ b/tidal-link/link/examples/linkaudio/AudioPlatform_CoreAudio.hpp
@@ -0,0 +1,54 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#pragma once
+
+#include "AudioEngine.hpp"
+#include
+
+namespace ableton
+{
+namespace linkaudio
+{
+
+class AudioPlatform
+{
+public:
+ AudioPlatform(Link& link);
+ ~AudioPlatform();
+ AudioEngine mEngine;
+
+private:
+ static OSStatus audioCallback(void* inRefCon,
+ AudioUnitRenderActionFlags*,
+ const AudioTimeStamp* inTimeStamp,
+ UInt32,
+ UInt32 inNumberFrames,
+ AudioBufferList* ioData);
+
+ void initialize();
+ void uninitialize();
+ void start();
+ void stop();
+
+ AudioUnit mIoUnit;
+};
+
+} // namespace linkaudio
+} // namespace ableton
diff --git a/tidal-link/link/examples/linkaudio/AudioPlatform_Dummy.hpp b/tidal-link/link/examples/linkaudio/AudioPlatform_Dummy.hpp
new file mode 100644
index 000000000..b5ff745ca
--- /dev/null
+++ b/tidal-link/link/examples/linkaudio/AudioPlatform_Dummy.hpp
@@ -0,0 +1,112 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#pragma once
+
+#include
+
+namespace ableton
+{
+namespace linkaudio
+{
+
+class AudioPlatform
+{
+ class AudioEngine
+ {
+ public:
+ AudioEngine(Link& link)
+ : mLink(link)
+ , mQuantum(4.)
+ {
+ }
+
+ void startPlaying()
+ {
+ auto sessionState = mLink.captureAppSessionState();
+ sessionState.setIsPlayingAndRequestBeatAtTime(true, now(), 0., mQuantum);
+ mLink.commitAppSessionState(sessionState);
+ }
+
+ void stopPlaying()
+ {
+ auto sessionState = mLink.captureAppSessionState();
+ sessionState.setIsPlaying(false, now());
+ mLink.commitAppSessionState(sessionState);
+ }
+
+ bool isPlaying() const
+ {
+ return mLink.captureAppSessionState().isPlaying();
+ }
+
+ double beatTime() const
+ {
+ auto sessionState = mLink.captureAppSessionState();
+ return sessionState.beatAtTime(now(), mQuantum);
+ }
+
+ void setTempo(double tempo)
+ {
+ auto sessionState = mLink.captureAppSessionState();
+ sessionState.setTempo(tempo, now());
+ mLink.commitAppSessionState(sessionState);
+ }
+
+ double quantum() const
+ {
+ return mQuantum;
+ }
+
+ void setQuantum(double quantum)
+ {
+ mQuantum = quantum;
+ }
+
+ bool isStartStopSyncEnabled() const
+ {
+ return mLink.isStartStopSyncEnabled();
+ }
+
+ void setStartStopSyncEnabled(bool enabled)
+ {
+ mLink.enableStartStopSync(enabled);
+ }
+
+ private:
+ std::chrono::microseconds now() const
+ {
+ return mLink.clock().micros();
+ }
+
+ Link& mLink;
+ double mQuantum;
+ };
+
+public:
+ AudioPlatform(Link& link)
+ : mEngine(link)
+ {
+ }
+
+ AudioEngine mEngine;
+};
+
+} // namespace linkaudio
+} // namespace ableton
diff --git a/tidal-link/link/examples/linkaudio/AudioPlatform_Jack.cpp b/tidal-link/link/examples/linkaudio/AudioPlatform_Jack.cpp
new file mode 100644
index 000000000..76a778d8a
--- /dev/null
+++ b/tidal-link/link/examples/linkaudio/AudioPlatform_Jack.cpp
@@ -0,0 +1,188 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#include "AudioPlatform_Jack.hpp"
+#include
+#include
+#include
+
+namespace ableton
+{
+namespace linkaudio
+{
+
+AudioPlatform::AudioPlatform(Link& link)
+ : mEngine(link)
+ , mSampleTime(0.)
+ , mpJackClient(nullptr)
+ , mpJackPorts(nullptr)
+{
+ initialize();
+ start();
+}
+
+AudioPlatform::~AudioPlatform()
+{
+ stop();
+ uninitialize();
+}
+
+int AudioPlatform::audioCallback(jack_nframes_t nframes, void* pvUserData)
+{
+ AudioPlatform* pAudioPlatform = static_cast(pvUserData);
+ return pAudioPlatform->audioCallback(nframes);
+}
+
+void AudioPlatform::latencyCallback(jack_latency_callback_mode_t, void* pvUserData)
+{
+ AudioPlatform* pAudioPlatform = static_cast(pvUserData);
+ pAudioPlatform->updateLatency();
+}
+
+void AudioPlatform::updateLatency()
+{
+ jack_latency_range_t latencyRange;
+ jack_port_get_latency_range(mpJackPorts[0], JackPlaybackLatency, &latencyRange);
+ mEngine.mOutputLatency.store(
+ std::chrono::microseconds(llround(1.0e6 * latencyRange.max / mEngine.mSampleRate)));
+}
+
+int AudioPlatform::audioCallback(jack_nframes_t nframes)
+{
+ using namespace std::chrono;
+ AudioEngine& engine = mEngine;
+
+ const auto hostTime = mHostTimeFilter.sampleTimeToHostTime(mSampleTime);
+
+ mSampleTime += nframes;
+
+ const auto bufferBeginAtOutput = hostTime + engine.mOutputLatency.load();
+
+ engine.audioCallback(bufferBeginAtOutput, nframes);
+
+ for (int k = 0; k < 2; ++k)
+ {
+ float* buffer = static_cast(jack_port_get_buffer(mpJackPorts[k], nframes));
+ for (unsigned long i = 0; i < nframes; ++i)
+ buffer[i] = static_cast(engine.mBuffer[i]);
+ }
+
+ return 0;
+}
+
+void AudioPlatform::initialize()
+{
+ jack_status_t status = JackFailure;
+ mpJackClient = jack_client_open("LinkHut", JackNullOption, &status);
+ if (mpJackClient == nullptr)
+ {
+ std::cerr << "Could not initialize Audio Engine. ";
+ std::cerr << "JACK: " << std::endl;
+ if (status & JackFailure)
+ std::cerr << "Overall operation failed." << std::endl;
+ if (status & JackInvalidOption)
+ std::cerr << "Invalid or unsupported option." << std::endl;
+ if (status & JackNameNotUnique)
+ std::cerr << "Client name not unique." << std::endl;
+ if (status & JackServerStarted)
+ std::cerr << "Server is started." << std::endl;
+ if (status & JackServerFailed)
+ std::cerr << "Unable to connect to server." << std::endl;
+ if (status & JackServerError)
+ std::cerr << "Server communication error." << std::endl;
+ if (status & JackNoSuchClient)
+ std::cerr << "Client does not exist." << std::endl;
+ if (status & JackLoadFailure)
+ std::cerr << "Unable to load internal client." << std::endl;
+ if (status & JackInitFailure)
+ std::cerr << "Unable to initialize client." << std::endl;
+ if (status & JackShmFailure)
+ std::cerr << "Unable to access shared memory." << std::endl;
+ if (status & JackVersionError)
+ std::cerr << "Client protocol version mismatch." << std::endl;
+ std::cerr << std::endl;
+ std::terminate();
+ }
+
+ const double bufferSize = jack_get_buffer_size(mpJackClient);
+ const double sampleRate = jack_get_sample_rate(mpJackClient);
+ mEngine.setBufferSize(static_cast(bufferSize));
+ mEngine.setSampleRate(sampleRate);
+
+ jack_set_latency_callback(mpJackClient, AudioPlatform::latencyCallback, this);
+
+ mpJackPorts = new jack_port_t*[2];
+
+ for (int k = 0; k < 2; ++k)
+ {
+ const std::string port_name = "out_" + std::to_string(k + 1);
+ mpJackPorts[k] = jack_port_register(
+ mpJackClient, port_name.c_str(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
+ if (mpJackPorts[k] == nullptr)
+ {
+ std::cerr << "Could not get Audio Device. " << std::endl;
+ jack_client_close(mpJackClient);
+ std::terminate();
+ }
+ }
+
+ jack_set_process_callback(mpJackClient, AudioPlatform::audioCallback, this);
+}
+
+void AudioPlatform::uninitialize()
+{
+ for (int k = 0; k < 2; ++k)
+ {
+ jack_port_unregister(mpJackClient, mpJackPorts[k]);
+ mpJackPorts[k] = nullptr;
+ }
+ delete[] mpJackPorts;
+ mpJackPorts = nullptr;
+
+ jack_client_close(mpJackClient);
+ mpJackClient = nullptr;
+}
+
+void AudioPlatform::start()
+{
+ jack_activate(mpJackClient);
+
+ const char** playback_ports = jack_get_ports(
+ mpJackClient, nullptr, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput | JackPortIsPhysical);
+
+ if (playback_ports)
+ {
+ const std::string client_name = jack_get_client_name(mpJackClient);
+ for (int k = 0; k < 2; ++k)
+ {
+ const std::string port_name = "out_" + std::to_string(k + 1);
+ const std::string client_port = client_name + ':' + port_name;
+ jack_connect(mpJackClient, client_port.c_str(), playback_ports[k]);
+ }
+ jack_free(playback_ports);
+ }
+}
+
+void AudioPlatform::stop()
+{
+ jack_deactivate(mpJackClient);
+}
+
+} // namespace linkaudio
+} // namespace ableton
diff --git a/tidal-link/link/examples/linkaudio/AudioPlatform_Jack.hpp b/tidal-link/link/examples/linkaudio/AudioPlatform_Jack.hpp
new file mode 100644
index 000000000..0b7999c37
--- /dev/null
+++ b/tidal-link/link/examples/linkaudio/AudioPlatform_Jack.hpp
@@ -0,0 +1,58 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#pragma once
+
+#include "AudioEngine.hpp"
+#include
+#include
+#include
+
+namespace ableton
+{
+namespace linkaudio
+{
+
+class AudioPlatform
+{
+public:
+ AudioPlatform(Link& link);
+ ~AudioPlatform();
+
+ AudioEngine mEngine;
+
+private:
+ static int audioCallback(jack_nframes_t nframes, void* pvUserData);
+ int audioCallback(jack_nframes_t nframes);
+ static void latencyCallback(jack_latency_callback_mode_t mode, void* pvUserData);
+ void updateLatency();
+
+ void initialize();
+ void uninitialize();
+ void start();
+ void stop();
+
+ link::HostTimeFilter mHostTimeFilter;
+ double mSampleTime;
+ jack_client_t* mpJackClient;
+ jack_port_t** mpJackPorts;
+};
+
+} // namespace linkaudio
+} // namespace ableton
diff --git a/tidal-link/link/examples/linkaudio/AudioPlatform_Portaudio.cpp b/tidal-link/link/examples/linkaudio/AudioPlatform_Portaudio.cpp
new file mode 100644
index 000000000..c35f1008f
--- /dev/null
+++ b/tidal-link/link/examples/linkaudio/AudioPlatform_Portaudio.cpp
@@ -0,0 +1,156 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#include "AudioPlatform_Portaudio.hpp"
+#include
+#include
+
+namespace ableton
+{
+namespace linkaudio
+{
+
+AudioPlatform::AudioPlatform(Link& link)
+ : mEngine(link)
+ , mSampleTime(0.)
+{
+ mEngine.setSampleRate(44100.);
+ mEngine.setBufferSize(512);
+ initialize();
+ start();
+}
+
+AudioPlatform::~AudioPlatform()
+{
+ stop();
+ uninitialize();
+}
+
+int AudioPlatform::audioCallback(const void* /*inputBuffer*/,
+ void* outputBuffer,
+ unsigned long inNumFrames,
+ const PaStreamCallbackTimeInfo* /*timeInfo*/,
+ PaStreamCallbackFlags /*statusFlags*/,
+ void* userData)
+{
+ using namespace std::chrono;
+ float* buffer = static_cast(outputBuffer);
+ AudioPlatform& platform = *static_cast(userData);
+ AudioEngine& engine = platform.mEngine;
+
+ const auto hostTime =
+ platform.mHostTimeFilter.sampleTimeToHostTime(platform.mSampleTime);
+
+ platform.mSampleTime += static_cast(inNumFrames);
+
+ const auto bufferBeginAtOutput = hostTime + engine.mOutputLatency.load();
+
+ engine.audioCallback(bufferBeginAtOutput, inNumFrames);
+
+ for (unsigned long i = 0; i < inNumFrames; ++i)
+ {
+ buffer[i * 2] = static_cast(engine.mBuffer[i]);
+ buffer[i * 2 + 1] = static_cast(engine.mBuffer[i]);
+ }
+
+ return paContinue;
+}
+
+void AudioPlatform::initialize()
+{
+ PaError result = Pa_Initialize();
+ if (result)
+ {
+ std::cerr << "Could not initialize Audio Engine. " << result << std::endl;
+ std::terminate();
+ }
+
+ PaStreamParameters outputParameters;
+ outputParameters.device = Pa_GetDefaultOutputDevice();
+ if (outputParameters.device == paNoDevice)
+ {
+ std::cerr << "Could not get Audio Device. " << std::endl;
+ std::terminate();
+ }
+
+ outputParameters.channelCount = 2;
+ outputParameters.sampleFormat = paFloat32;
+ outputParameters.suggestedLatency =
+ Pa_GetDeviceInfo(outputParameters.device)->defaultLowOutputLatency;
+ outputParameters.hostApiSpecificStreamInfo = nullptr;
+ mEngine.mOutputLatency.store(
+ std::chrono::microseconds(llround(outputParameters.suggestedLatency * 1.0e6)));
+ result = Pa_OpenStream(&pStream, nullptr, &outputParameters, mEngine.mSampleRate,
+ mEngine.mBuffer.size(), paClipOff, &audioCallback, this);
+
+ if (result)
+ {
+ std::cerr << "Could not open stream. " << result << std::endl;
+ std::terminate();
+ }
+
+ if (!pStream)
+ {
+ std::cerr << "No valid audio stream." << std::endl;
+ std::terminate();
+ }
+}
+
+void AudioPlatform::uninitialize()
+{
+ PaError result = Pa_CloseStream(pStream);
+ if (result)
+ {
+ std::cerr << "Could not close Audio Stream. " << result << std::endl;
+ }
+ Pa_Terminate();
+
+ if (!pStream)
+ {
+ std::cerr << "No valid audio stream." << std::endl;
+ std::terminate();
+ }
+}
+
+void AudioPlatform::start()
+{
+ PaError result = Pa_StartStream(pStream);
+ if (result)
+ {
+ std::cerr << "Could not start Audio Stream. " << result << std::endl;
+ }
+}
+
+void AudioPlatform::stop()
+{
+ if (pStream == nullptr)
+ {
+ return;
+ }
+
+ PaError result = Pa_StopStream(pStream);
+ if (result)
+ {
+ std::cerr << "Could not stop Audio Stream. " << result << std::endl;
+ std::terminate();
+ }
+}
+
+} // namespace linkaudio
+} // namespace ableton
diff --git a/tidal-link/link/examples/linkaudio/AudioPlatform_Portaudio.hpp b/tidal-link/link/examples/linkaudio/AudioPlatform_Portaudio.hpp
new file mode 100644
index 000000000..032d14793
--- /dev/null
+++ b/tidal-link/link/examples/linkaudio/AudioPlatform_Portaudio.hpp
@@ -0,0 +1,59 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#pragma once
+
+#include "AudioEngine.hpp"
+#include
+#include
+#include
+
+namespace ableton
+{
+namespace linkaudio
+{
+
+class AudioPlatform
+{
+public:
+ AudioPlatform(Link& link);
+ ~AudioPlatform();
+
+ AudioEngine mEngine;
+
+private:
+ static int audioCallback(const void* inputBuffer,
+ void* outputBuffer,
+ unsigned long inNumFrames,
+ const PaStreamCallbackTimeInfo* timeInfo,
+ PaStreamCallbackFlags statusFlags,
+ void* userData);
+
+ void initialize();
+ void uninitialize();
+ void start();
+ void stop();
+
+ link::HostTimeFilter mHostTimeFilter;
+ double mSampleTime;
+ PaStream* pStream;
+};
+
+} // namespace linkaudio
+} // namespace ableton
diff --git a/tidal-link/link/examples/linkaudio/AudioPlatform_Wasapi.cpp b/tidal-link/link/examples/linkaudio/AudioPlatform_Wasapi.cpp
new file mode 100644
index 000000000..efb73f8bd
--- /dev/null
+++ b/tidal-link/link/examples/linkaudio/AudioPlatform_Wasapi.cpp
@@ -0,0 +1,331 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#include "AudioPlatform_Wasapi.hpp"
+#include
+#include
+
+// WARNING: This file provides an audio driver for Windows using WASAPI. This driver is
+// considered experimental and has problems with low-latency playback. Please consider
+// using the ASIO driver instead.
+
+namespace ableton
+{
+namespace linkaudio
+{
+
+// GUID identifiers used to when looking up COM enumerators and devices
+static const IID kMMDeviceEnumeratorId = __uuidof(MMDeviceEnumerator);
+static const IID kIMMDeviceEnumeratorId = __uuidof(IMMDeviceEnumerator);
+static const IID kAudioClientId = __uuidof(IAudioClient);
+static const IID kAudioRenderClientId = __uuidof(IAudioRenderClient);
+
+// Controls how large the driver's ring buffer will be, expressed in terms of
+// 100-nanosecond units. This value also influences the overall driver latency.
+static const REFERENCE_TIME kBufferDuration = 1000000;
+
+// How long to block the runloop while waiting for an event callback.
+static const DWORD kWaitTimeoutInMs = 2000;
+
+void fatalError(HRESULT result, LPCTSTR context)
+{
+ if (result > 0)
+ {
+ _com_error error(result);
+ LPCTSTR errorMessage = error.ErrorMessage();
+ std::cerr << context << ": " << errorMessage << std::endl;
+ }
+ else
+ {
+ std::cerr << context << std::endl;
+ }
+
+ std::terminate();
+}
+
+DWORD renderAudioRunloop(LPVOID lpParam)
+{
+ AudioPlatform* platform = static_cast(lpParam);
+ return platform->audioRunloop();
+}
+
+AudioPlatform::AudioPlatform(Link& link)
+ : mEngine(link)
+ , mSampleTime(0)
+ , mDevice(nullptr)
+ , mAudioClient(nullptr)
+ , mRenderClient(nullptr)
+ , mStreamFormat(nullptr)
+ , mEventHandle(nullptr)
+ , mAudioThreadHandle(nullptr)
+ , mIsRunning(false)
+{
+ initialize();
+ mEngine.setBufferSize(bufferSize());
+ mEngine.setSampleRate(mStreamFormat->nSamplesPerSec);
+ start();
+}
+
+AudioPlatform::~AudioPlatform()
+{
+ // WARNING: Here be dragons!
+ // The WASAPI driver is not thread-safe, and crashes may occur when shutting down due
+ // to these fields being concurrently accessed in the audio thread. Introducing a mutex
+ // in the audio thread is not an appropriate solution to fix this race condition; a more
+ // robust solution needs to be considered instead.
+
+ if (mDevice != nullptr)
+ {
+ mDevice->Release();
+ }
+ if (mAudioClient != nullptr)
+ {
+ mAudioClient->Release();
+ }
+ if (mRenderClient != nullptr)
+ {
+ mRenderClient->Release();
+ }
+ CoTaskMemFree(mStreamFormat);
+}
+
+UINT32 AudioPlatform::bufferSize()
+{
+ UINT32 bufferSize;
+ HRESULT result = mAudioClient->GetBufferSize(&bufferSize);
+ if (FAILED(result))
+ {
+ fatalError(result, "Could not get buffer size");
+ return 0; // not reached
+ }
+
+ return bufferSize;
+}
+
+void AudioPlatform::initialize()
+{
+ HRESULT result = CoInitialize(nullptr);
+ if (FAILED(result))
+ {
+ fatalError(result, "Could not initialize COM library");
+ }
+
+ IMMDeviceEnumerator* enumerator = nullptr;
+ result = CoCreateInstance(kMMDeviceEnumeratorId, nullptr, CLSCTX_ALL,
+ kIMMDeviceEnumeratorId, (void**)&enumerator);
+ if (FAILED(result))
+ {
+ fatalError(result, "Could not create device instance");
+ }
+
+ result = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &(mDevice));
+ if (FAILED(result))
+ {
+ fatalError(result, "Could not get default audio endpoint");
+ }
+ else
+ {
+ enumerator->Release();
+ enumerator = nullptr;
+ }
+
+ result =
+ mDevice->Activate(kAudioClientId, CLSCTX_ALL, nullptr, (void**)&(mAudioClient));
+ if (FAILED(result))
+ {
+ fatalError(result, "Could not activate audio device");
+ }
+
+ result = mAudioClient->GetMixFormat(&(mStreamFormat));
+ if (FAILED(result))
+ {
+ fatalError(result, "Could not get mix format");
+ }
+
+ if (mStreamFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE)
+ {
+ WAVEFORMATEXTENSIBLE* streamFormatEx =
+ reinterpret_cast(mStreamFormat);
+ if (streamFormatEx->SubFormat != KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)
+ {
+ fatalError(0, "Sorry, only IEEE floating point streams are supported");
+ }
+ }
+ else
+ {
+ fatalError(0, "Sorry, only extensible wave streams are supported");
+ }
+
+ result = mAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED,
+ AUDCLNT_STREAMFLAGS_EVENTCALLBACK, kBufferDuration, 0, mStreamFormat, nullptr);
+ if (FAILED(result))
+ {
+ fatalError(result, "Could not initialize audio device");
+ }
+
+ mEventHandle = CreateEvent(nullptr, false, false, nullptr);
+ if (mEventHandle == nullptr)
+ {
+ fatalError(result, "Could not create event handle");
+ }
+
+ result = mAudioClient->GetService(kAudioRenderClientId, (void**)&(mRenderClient));
+ if (FAILED(result))
+ {
+ fatalError(result, "Could not get audio render service");
+ }
+
+ mIsRunning = true;
+ LPTHREAD_START_ROUTINE threadEntryPoint =
+ reinterpret_cast(renderAudioRunloop);
+ mAudioThreadHandle = CreateThread(nullptr, 0, threadEntryPoint, this, 0, nullptr);
+ if (mAudioThreadHandle == nullptr)
+ {
+ fatalError(GetLastError(), "Could not create audio thread");
+ }
+}
+
+void AudioPlatform::start()
+{
+ UINT32 bufSize = bufferSize();
+ BYTE* buffer;
+ HRESULT result = mRenderClient->GetBuffer(bufSize, &buffer);
+ if (FAILED(result))
+ {
+ fatalError(result, "Could not get render client buffer (in start audio engine)");
+ }
+
+ result = mRenderClient->ReleaseBuffer(bufSize, 0);
+ if (FAILED(result))
+ {
+ fatalError(result, "Could not release buffer");
+ }
+
+ result = mAudioClient->SetEventHandle(mEventHandle);
+ if (FAILED(result))
+ {
+ fatalError(result, "Could not set event handle to audio client");
+ }
+
+ REFERENCE_TIME latency;
+ result = mAudioClient->GetStreamLatency(&latency);
+ if (FAILED(result))
+ {
+ fatalError(result, "Could not get stream latency");
+ }
+
+ result = mAudioClient->Start();
+ if (FAILED(result))
+ {
+ fatalError(result, "Could not start audio client");
+ }
+}
+
+DWORD AudioPlatform::audioRunloop()
+{
+ while (mIsRunning)
+ {
+ DWORD wait = WaitForSingleObject(mEventHandle, kWaitTimeoutInMs);
+ if (wait != WAIT_OBJECT_0)
+ {
+ mIsRunning = false;
+ mAudioClient->Stop();
+ return wait;
+ }
+
+ // Get the amount of padding, which basically is the amount of data in the driver's
+ // ring buffer that is filled with unread data. Thus, subtracting this amount from
+ // the buffer size gives the effective buffer size, which is the amount of frames
+ // that can be safely written to the driver.
+ UINT32 paddingFrames;
+ HRESULT result = mAudioClient->GetCurrentPadding(&paddingFrames);
+ if (FAILED(result))
+ {
+ fatalError(result, "Could not get number of padding frames");
+ }
+
+ const UINT32 numSamples = bufferSize() - paddingFrames;
+
+ BYTE* buffer;
+ result = mRenderClient->GetBuffer(numSamples, &buffer);
+ if (FAILED(result))
+ {
+ fatalError(result, "Could not get render client buffer (in callback)");
+ }
+
+ const double sampleRate = static_cast(mStreamFormat->nSamplesPerSec);
+ using namespace std::chrono;
+ const auto bufferDuration =
+ duration_cast(duration{numSamples / sampleRate});
+
+ const auto hostTime = mHostTimeFilter.sampleTimeToHostTime(mSampleTime);
+
+ mSampleTime += numSamples;
+
+ const auto bufferBeginAtOutput = hostTime + mEngine.mOutputLatency.load();
+
+ mEngine.audioCallback(bufferBeginAtOutput, numSamples);
+
+ float* floatBuffer = reinterpret_cast(buffer);
+ for (WORD i = 0; i < numSamples; ++i)
+ {
+ if (i >= mEngine.mBuffer.size())
+ {
+ break;
+ }
+ for (WORD j = 0; j < mStreamFormat->nChannels; ++j)
+ {
+ floatBuffer[j + (i * mStreamFormat->nChannels)] =
+ static_cast(mEngine.mBuffer[i]);
+ }
+ }
+
+ // Write the buffer to the audio driver and subsequently free the buffer memory
+ result = mRenderClient->ReleaseBuffer(numSamples, 0);
+ if (FAILED(result))
+ {
+ fatalError(result, "Error rendering data");
+ }
+ } // end of runloop
+
+ mIsRunning = false;
+ return 0;
+}
+
+
+// void fillBuffer(MetronomeSynth& metronome,
+// const UINT32 startFrame,
+// const UINT32 numSamples,
+// const UINT32 numChannels,
+// BYTE* buffer)
+//{
+// float* floatBuffer = reinterpret_cast(buffer);
+// UINT32 frame = startFrame;
+// while (frame < numSamples * numChannels)
+// {
+// const float sample = static_cast(metronome.getSample());
+// for (UINT32 channel = 0; channel < numChannels; ++channel)
+// {
+// floatBuffer[frame++] = sample;
+// }
+// }
+//}
+
+} // namespace linkaudio
+} // namespace ableton
diff --git a/tidal-link/link/examples/linkaudio/AudioPlatform_Wasapi.hpp b/tidal-link/link/examples/linkaudio/AudioPlatform_Wasapi.hpp
new file mode 100644
index 000000000..9fa61be94
--- /dev/null
+++ b/tidal-link/link/examples/linkaudio/AudioPlatform_Wasapi.hpp
@@ -0,0 +1,72 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#pragma once
+
+#include "AudioEngine.hpp"
+#include
+#include
+#include
+#include
+
+// WARNING: This file provides an audio driver for Windows using WASAPI. This driver is
+// considered experimental and has problems with low-latency playback. Please consider
+// using the ASIO driver instead.
+
+namespace ableton
+{
+namespace linkaudio
+{
+
+// Convenience function to look up the human-readable WinAPI error code, print it out,
+// and then terminate the application.
+void fatalError(HRESULT result, LPCTSTR context);
+
+DWORD renderAudioRunloop(LPVOID);
+
+class AudioPlatform
+{
+public:
+ AudioPlatform(Link& link);
+ ~AudioPlatform();
+
+ DWORD audioRunloop();
+
+ AudioEngine mEngine;
+
+private:
+ UINT32 bufferSize();
+
+ void initialize();
+ void start();
+
+ link::HostTimeFilter mHostTimeFilter;
+ double mSampleTime;
+
+ IMMDevice* mDevice;
+ IAudioClient* mAudioClient;
+ IAudioRenderClient* mRenderClient;
+ WAVEFORMATEX* mStreamFormat;
+ HANDLE mEventHandle;
+ HANDLE mAudioThreadHandle;
+ std::atomic mIsRunning;
+};
+
+} // namespace linkaudio
+} // namespace ableton
diff --git a/tidal-link/link/examples/linkhut/main.cpp b/tidal-link/link/examples/linkhut/main.cpp
new file mode 100644
index 000000000..92cb504b6
--- /dev/null
+++ b/tidal-link/link/examples/linkhut/main.cpp
@@ -0,0 +1,208 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#include "AudioPlatform.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#if defined(LINK_PLATFORM_UNIX)
+#include
+#endif
+
+namespace
+{
+
+struct State
+{
+ std::atomic running;
+ ableton::Link link;
+ ableton::linkaudio::AudioPlatform audioPlatform;
+
+ State()
+ : running(true)
+ , link(120.)
+ , audioPlatform(link)
+ {
+ }
+};
+
+void disableBufferedInput()
+{
+#if defined(LINK_PLATFORM_UNIX)
+ termios t;
+ tcgetattr(STDIN_FILENO, &t);
+ t.c_lflag &= static_cast(~ICANON);
+ tcsetattr(STDIN_FILENO, TCSANOW, &t);
+#endif
+}
+
+void enableBufferedInput()
+{
+#if defined(LINK_PLATFORM_UNIX)
+ termios t;
+ tcgetattr(STDIN_FILENO, &t);
+ t.c_lflag |= ICANON;
+ tcsetattr(STDIN_FILENO, TCSANOW, &t);
+#endif
+}
+
+void clearLine()
+{
+ std::cout << " \r" << std::flush;
+ std::cout.fill(' ');
+}
+
+void printHelp()
+{
+ std::cout << std::endl << " < L I N K H U T >" << std::endl << std::endl;
+ std::cout << "usage:" << std::endl;
+ std::cout << " enable / disable Link: a" << std::endl;
+ std::cout << " start / stop: space" << std::endl;
+ std::cout << " decrease / increase tempo: w / e" << std::endl;
+ std::cout << " decrease / increase quantum: r / t" << std::endl;
+ std::cout << " enable / disable start stop sync: s" << std::endl;
+ std::cout << " quit: q" << std::endl << std::endl;
+}
+
+void printStateHeader()
+{
+ std::cout
+ << "enabled | num peers | quantum | start stop sync | tempo | beats | metro"
+ << std::endl;
+}
+
+void printState(const std::chrono::microseconds time,
+ const ableton::Link::SessionState sessionState,
+ const bool linkEnabled,
+ const std::size_t numPeers,
+ const double quantum,
+ const bool startStopSyncOn)
+{
+ using namespace std;
+ const auto enabled = linkEnabled ? "yes" : "no";
+ const auto beats = sessionState.beatAtTime(time, quantum);
+ const auto phase = sessionState.phaseAtTime(time, quantum);
+ const auto startStop = startStopSyncOn ? "yes" : "no";
+ const auto isPlaying = sessionState.isPlaying() ? "[playing]" : "[stopped]";
+ cout << defaultfloat << left << setw(7) << enabled << " | " << setw(9) << numPeers
+ << " | " << setw(7) << quantum << " | " << setw(3) << startStop << " " << setw(11)
+ << isPlaying << " | " << fixed << setw(7) << sessionState.tempo() << " | " << fixed
+ << setprecision(2) << setw(7) << beats << " | ";
+ for (int i = 0; i < ceil(quantum); ++i)
+ {
+ if (i < phase)
+ {
+ std::cout << 'X';
+ }
+ else
+ {
+ std::cout << 'O';
+ }
+ }
+ clearLine();
+}
+
+void input(State& state)
+{
+ char in;
+
+ for (;;)
+ {
+#if defined(LINK_PLATFORM_WINDOWS)
+ HANDLE stdinHandle = GetStdHandle(STD_INPUT_HANDLE);
+ DWORD numCharsRead;
+ INPUT_RECORD inputRecord;
+ do
+ {
+ ReadConsoleInput(stdinHandle, &inputRecord, 1, &numCharsRead);
+ } while ((inputRecord.EventType != KEY_EVENT) || inputRecord.Event.KeyEvent.bKeyDown);
+ in = inputRecord.Event.KeyEvent.uChar.AsciiChar;
+#elif defined(LINK_PLATFORM_UNIX)
+ in = static_cast(std::cin.get());
+#endif
+
+ const auto tempo = state.link.captureAppSessionState().tempo();
+ auto& engine = state.audioPlatform.mEngine;
+
+ switch (in)
+ {
+ case 'q':
+ state.running = false;
+ clearLine();
+ return;
+ case 'a':
+ state.link.enable(!state.link.isEnabled());
+ break;
+ case 'w':
+ engine.setTempo(tempo - 1);
+ break;
+ case 'e':
+ engine.setTempo(tempo + 1);
+ break;
+ case 'r':
+ engine.setQuantum(engine.quantum() - 1);
+ break;
+ case 't':
+ engine.setQuantum(std::max(1., engine.quantum() + 1));
+ break;
+ case 's':
+ engine.setStartStopSyncEnabled(!engine.isStartStopSyncEnabled());
+ break;
+ case ' ':
+ if (engine.isPlaying())
+ {
+ engine.stopPlaying();
+ }
+ else
+ {
+ engine.startPlaying();
+ }
+ break;
+ }
+ }
+}
+
+} // namespace
+
+int main(int, char**)
+{
+ State state;
+ printHelp();
+ printStateHeader();
+ std::thread thread(input, std::ref(state));
+ disableBufferedInput();
+
+ while (state.running)
+ {
+ const auto time = state.link.clock().micros();
+ auto sessionState = state.link.captureAppSessionState();
+ printState(time, sessionState, state.link.isEnabled(), state.link.numPeers(),
+ state.audioPlatform.mEngine.quantum(),
+ state.audioPlatform.mEngine.isStartStopSyncEnabled());
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+ }
+
+ enableBufferedInput();
+ thread.join();
+ return 0;
+}
diff --git a/tidal-link/link/extensions/abl_link/.clang-format b/tidal-link/link/extensions/abl_link/.clang-format
new file mode 100644
index 000000000..8fb4cfc56
--- /dev/null
+++ b/tidal-link/link/extensions/abl_link/.clang-format
@@ -0,0 +1,44 @@
+AccessModifierOffset: -2
+AlignAfterOpenBracket: DontAlign
+AlignEscapedNewlinesLeft: false
+AlignOperands: true
+AlignTrailingComments: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowShortBlocksOnASingleLine: false
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: true
+AlwaysBreakTemplateDeclarations: true
+BinPackArguments: true
+BinPackParameters: false
+BreakBeforeBinaryOperators: NonAssignment
+BreakBeforeBraces: Allman
+BreakBeforeTernaryOperators: true
+ColumnLimit: 90
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+ConstructorInitializerIndentWidth: 2
+ContinuationIndentWidth: 2
+DerivePointerAlignment: false
+IndentCaseLabels: false
+IndentFunctionDeclarationAfterType: false
+IndentWidth: 2
+IndentWrappedFunctionNames: false
+KeepEmptyLinesAtTheStartOfBlocks: true
+MaxEmptyLinesToKeep: 2
+PenaltyBreakBeforeFirstCallParameter: 0
+PenaltyReturnTypeOnItsOwnLine: 1000
+PointerAlignment: Right
+SpaceAfterCStyleCast: false
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeParens: ControlStatements
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles: false
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+UseTab: Never
diff --git a/tidal-link/link/extensions/abl_link/CMakeLists.txt b/tidal-link/link/extensions/abl_link/CMakeLists.txt
new file mode 100644
index 000000000..bccb5971d
--- /dev/null
+++ b/tidal-link/link/extensions/abl_link/CMakeLists.txt
@@ -0,0 +1,19 @@
+# _ _ _ _ _
+# | (_)_ __ | | __ | |__ _ _| |_
+# | | | '_ \| |/ / | '_ \| | | | __|
+# | | | | | | < | | | | |_| | |_
+# |_|_|_| |_|_|\_\___|_| |_|\__,_|\__|
+# |_____|
+
+set(link_hut_SOURCES
+ ${CMAKE_CURRENT_LIST_DIR}/examples/link_hut/main.c
+)
+
+source_group("link_hut" FILES ${link_hut_SOURCES})
+
+add_executable(link_hut ${link_hut_SOURCES})
+
+set_property(TARGET link_hut PROPERTY C_STANDARD 11)
+
+target_link_libraries(link_hut abl_link)
+
diff --git a/tidal-link/link/extensions/abl_link/README.md b/tidal-link/link/extensions/abl_link/README.md
new file mode 100644
index 000000000..70f60a267
--- /dev/null
+++ b/tidal-link/link/extensions/abl_link/README.md
@@ -0,0 +1,17 @@
+# abl_link
+
+Plain C 11 wrapper for Ableton Link.
+
+# Building and Running abl_link Examples
+
+The `abl_link` library and the `link_hut` example application are built as part of the main CMake project in this repository.
+
+# Integrating abl_link into CMake-based Projects
+
+If you are using CMake, you can integrate `abl_link` by including both, the `Ableton::Link` and the `abl_link` configs:
+
+```cmake
+include($PATH_TO_LINK/AbletonLinkConfig.cmake)
+include($PATH_TO_LINK/extensions/abl_link/abl_link.cmake)
+target_link_libraries($YOUR_TARGET abl_link)
+```
diff --git a/tidal-link/link/extensions/abl_link/abl_link.cmake b/tidal-link/link/extensions/abl_link/abl_link.cmake
new file mode 100644
index 000000000..00efd1ee2
--- /dev/null
+++ b/tidal-link/link/extensions/abl_link/abl_link.cmake
@@ -0,0 +1,19 @@
+if(CMAKE_VERSION VERSION_LESS 3.0)
+ message(FATAL_ERROR "CMake 3.0 or greater is required")
+endif()
+
+add_library(abl_link STATIC
+ ${CMAKE_CURRENT_LIST_DIR}/src/abl_link.cpp
+)
+
+target_include_directories(abl_link PUBLIC
+ ${CMAKE_CURRENT_LIST_DIR}/include
+)
+
+set_property(TARGET abl_link PROPERTY C_STANDARD 11)
+
+target_link_libraries(abl_link Ableton::Link)
+
+if(CMAKE_SYSTEM_NAME MATCHES "Linux|kFreeBSD|GNU")
+ target_link_libraries(abl_link atomic pthread)
+endif()
diff --git a/tidal-link/link/extensions/abl_link/examples/link_hut/main.c b/tidal-link/link/extensions/abl_link/examples/link_hut/main.c
new file mode 100644
index 000000000..df7d9d810
--- /dev/null
+++ b/tidal-link/link/extensions/abl_link/examples/link_hut/main.c
@@ -0,0 +1,269 @@
+/* Copyright 2021, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#include
+#include
+#include
+#include
+#include
+#if defined(LINK_PLATFORM_UNIX)
+#include
+#include
+#include
+#elif defined(LINK_PLATFORM_WINDOWS)
+#pragma warning(push, 0)
+#pragma warning(disable : 4255) // 'no function prototype given' in winuser.h
+#pragma warning(disable : 4668) // undefined preprocessor macro in winioctl.h
+#pragma warning(disable : 5105) // "/wd5105" # "macro expansion producing 'defined' has
+ // undefined behavior" in winbase.h
+#include
+#pragma warning(pop)
+#pragma warning(disable : 4100) // unreferenced formal parameter in main
+#endif
+
+#include
+
+typedef struct state
+{
+ abl_link link;
+ abl_link_session_state session_state;
+ bool running;
+ double quantum;
+} state;
+
+struct state *new_state(void)
+{
+ struct state *s = malloc(sizeof(state));
+ s->link = abl_link_create(120);
+ s->session_state = abl_link_create_session_state();
+ s->running = true;
+ s->quantum = 4;
+ return s;
+}
+
+void delete_state(state *state)
+{
+ abl_link_destroy_session_state(state->session_state);
+ abl_link_destroy(state->link);
+ free(state);
+}
+
+void disable_buffered_input(void)
+{
+#if defined(LINK_PLATFORM_UNIX)
+ struct termios t;
+ tcgetattr(STDIN_FILENO, &t);
+ t.c_lflag &= ~ICANON;
+ tcsetattr(STDIN_FILENO, TCSANOW, &t);
+#endif
+}
+
+void enable_buffered_input(void)
+{
+#if defined(LINK_PLATFORM_UNIX)
+ struct termios t;
+ tcgetattr(STDIN_FILENO, &t);
+ t.c_lflag |= ICANON;
+ tcsetattr(STDIN_FILENO, TCSANOW, &t);
+#endif
+}
+
+bool wait_for_input(void)
+{
+#if defined(LINK_PLATFORM_UNIX)
+ fd_set selectset;
+ struct timeval timeout = {0, 50000};
+ int ret;
+ FD_ZERO(&selectset);
+ FD_SET(0, &selectset);
+ ret = select(1, &selectset, NULL, NULL, &timeout);
+ if (ret > 0)
+ {
+ return true;
+ }
+#elif (LINK_PLATFORM_WINDOWS)
+ HANDLE handle = GetStdHandle(STD_INPUT_HANDLE);
+ if (WaitForSingleObject(handle, 50) == WAIT_OBJECT_0)
+ {
+ return true;
+ }
+#else
+#error "Missing implementation"
+#endif
+ return false;
+}
+
+void clear_line(void)
+{
+ printf(" \r");
+ fflush(stdout);
+}
+
+void clear_input(void)
+{
+#if defined(LINK_PLATFORM_WINDOWS)
+ {
+ HANDLE handle = GetStdHandle(STD_INPUT_HANDLE);
+ INPUT_RECORD r[512];
+ DWORD read;
+ ReadConsoleInput(handle, r, 512, &read);
+ }
+#endif
+}
+
+void print_help(void)
+{
+ printf("\n\n < L I N K H U T >\n\n");
+ printf("usage:\n");
+ printf(" enable / disable Link: a\n");
+ printf(" start / stop: space\n");
+ printf(" decrease / increase tempo: w / e\n");
+ printf(" decrease / increase quantum: r / t\n");
+ printf(" enable / disable start stop sync: s\n");
+ printf(" quit: q\n");
+}
+
+void print_state_header(void)
+{
+ printf(
+ "\nenabled | num peers | quantum | start stop sync | tempo | beats | metro\n");
+}
+
+void print_state(state *state)
+{
+ abl_link_capture_app_session_state(state->link, state->session_state);
+ const uint64_t time = abl_link_clock_micros(state->link);
+ const char *enabled = abl_link_is_enabled(state->link) ? "yes" : "no";
+ const uint64_t num_peers = abl_link_num_peers(state->link);
+ const char *start_stop =
+ abl_link_is_start_stop_sync_enabled(state->link) ? "yes" : " no";
+ const char *playing =
+ abl_link_is_playing(state->session_state) ? "[playing]" : "[stopped]";
+ const double tempo = abl_link_tempo(state->session_state);
+ const double beats = abl_link_beat_at_time(state->session_state, time, state->quantum);
+ const double phase = abl_link_phase_at_time(state->session_state, time, state->quantum);
+ printf("%7s | ", enabled);
+ printf("%9" PRIu64 " | ", num_peers);
+ printf("%7.f | ", state->quantum);
+ printf("%3s %11s | ", start_stop, playing);
+ printf("%7.2f | ", tempo);
+ printf("%7.2f | ", beats);
+ for (int i = 0; i < ceil(state->quantum); ++i)
+ {
+ if (i < phase)
+ {
+ printf("X");
+ }
+ else
+ {
+ printf("O");
+ }
+ }
+}
+
+void input(state *state)
+{
+ char in;
+
+#if defined(LINK_PLATFORM_UNIX)
+ in = (char)fgetc(stdin);
+#elif defined(LINK_PLATFORM_WINDOWS)
+ HANDLE stdinHandle = GetStdHandle(STD_INPUT_HANDLE);
+ DWORD numCharsRead;
+ INPUT_RECORD inputRecord;
+ do
+ {
+ ReadConsoleInput(stdinHandle, &inputRecord, 1, &numCharsRead);
+ } while ((inputRecord.EventType != KEY_EVENT) || inputRecord.Event.KeyEvent.bKeyDown);
+ in = inputRecord.Event.KeyEvent.uChar.AsciiChar;
+#else
+#error "Missing implementation"
+#endif
+
+ abl_link_capture_app_session_state(state->link, state->session_state);
+ const double tempo = abl_link_tempo(state->session_state);
+ const uint64_t timestamp = abl_link_clock_micros(state->link);
+ const bool enabled = abl_link_is_enabled(state->link);
+ switch (in)
+ {
+ case 'q':
+ state->running = false;
+ clear_line();
+ return;
+ case 'a':
+ abl_link_enable(state->link, !enabled);
+ break;
+ case 'w':
+ abl_link_set_tempo(state->session_state, tempo - 1, timestamp);
+ break;
+ case 'e':
+ abl_link_set_tempo(state->session_state, tempo + 1, timestamp);
+ break;
+ case 'r':
+ state->quantum -= 1;
+ break;
+ case 't':
+ state->quantum += 1;
+ break;
+ case 's':
+ abl_link_enable_start_stop_sync(
+ state->link, !abl_link_is_start_stop_sync_enabled(state->link));
+ break;
+ case ' ':
+ if (abl_link_is_playing(state->session_state))
+ {
+ abl_link_set_is_playing(
+ state->session_state, false, abl_link_clock_micros(state->link));
+ }
+ else
+ {
+ abl_link_set_is_playing_and_request_beat_at_time(state->session_state, true,
+ abl_link_clock_micros(state->link), 0, state->quantum);
+ }
+ break;
+ }
+ abl_link_commit_app_session_state(state->link, state->session_state);
+}
+
+int main(int nargs, char **args)
+{
+ state *state = new_state();
+
+ print_help();
+ print_state_header();
+ disable_buffered_input();
+ clear_input();
+
+ while (state->running)
+ {
+ clear_line();
+ if (wait_for_input())
+ {
+ input(state);
+ }
+ else
+ {
+ print_state(state);
+ }
+ }
+
+ enable_buffered_input();
+ delete_state(state);
+ return 0;
+}
diff --git a/tidal-link/link/extensions/abl_link/include/abl_link.h b/tidal-link/link/extensions/abl_link/include/abl_link.h
new file mode 100644
index 000000000..34ac71b0f
--- /dev/null
+++ b/tidal-link/link/extensions/abl_link/include/abl_link.h
@@ -0,0 +1,352 @@
+/* Copyright 2021, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif // __cplusplus
+
+ /*!
+ * @discussion Each abl_link instance has its own session state which
+ * represents a beat timeline and a transport start/stop state. The
+ * timeline starts running from beat 0 at the initial tempo when
+ * constructed. The timeline always advances at a speed defined by
+ * its current tempo, even if transport is stopped. Synchronizing to the
+ * transport start/stop state of Link is optional for every peer.
+ * The transport start/stop state is only shared with other peers when
+ * start/stop synchronization is enabled.
+ *
+ * An abl_link instance is initially disabled after construction, which
+ * means that it will not communicate on the network. Once enabled,
+ * an abl_link instance initiates network communication in an effort to
+ * discover other peers. When peers are discovered, they immediately
+ * become part of a shared Link session.
+ *
+ * Each function documents its thread-safety and
+ * realtime-safety properties. When a function is marked thread-safe,
+ * it means it is safe to call from multiple threads
+ * concurrently. When a function is marked realtime-safe, it means that
+ * it does not block and is appropriate for use in the thread that
+ * performs audio IO.
+ *
+ * One session state capture/commit function pair for use
+ * in the audio thread and one for all other application contexts is provided.
+ * In general, modifying the session state should be done in the audio
+ * thread for the most accurate timing results. The ability to modify
+ * the session state from application threads should only be used in
+ * cases where an application's audio thread is not actively running
+ * or if it doesn't generate audio at all. Modifying the Link session
+ * state from both the audio thread and an application thread
+ * concurrently is not advised and will potentially lead to unexpected
+ * behavior.
+ */
+
+ /*! @brief The representation of an abl_link instance*/
+ typedef struct abl_link
+ {
+ void *impl;
+ } abl_link;
+
+ /*! @brief Construct a new abl_link instance with an initial tempo.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ */
+ abl_link abl_link_create(double bpm);
+
+ /*! @brief Delete an abl_link instance.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ */
+ void abl_link_destroy(abl_link link);
+
+ /*! @brief Is Link currently enabled?
+ * Thread-safe: yes
+ * Realtime-safe: yes
+ */
+ bool abl_link_is_enabled(abl_link link);
+
+ /*! @brief Enable/disable Link.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ */
+ void abl_link_enable(abl_link link, bool enable);
+
+ /*! @brief: Is start/stop synchronization enabled?
+ * Thread-safe: yes
+ * Realtime-safe: no
+ */
+ bool abl_link_is_start_stop_sync_enabled(abl_link link);
+
+ /*! @brief: Enable start/stop synchronization.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ */
+ void abl_link_enable_start_stop_sync(abl_link link, bool enabled);
+
+ /*! @brief How many peers are currently connected in a Link session?
+ * Thread-safe: yes
+ * Realtime-safe: yes
+ */
+ uint64_t abl_link_num_peers(abl_link link);
+
+ /*! @brief Register a callback to be notified when the number of
+ * peers in the Link session changes.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ *
+ * @discussion The callback is invoked on a Link-managed thread.
+ */
+ typedef void (*abl_link_num_peers_callback)(uint64_t num_peers, void *context);
+ void abl_link_set_num_peers_callback(
+ abl_link link, abl_link_num_peers_callback callback, void *context);
+
+ /*! @brief Register a callback to be notified when the session
+ * tempo changes.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ *
+ * @discussion The callback is invoked on a Link-managed thread.
+ */
+ typedef void (*abl_link_tempo_callback)(double tempo, void *context);
+ void abl_link_set_tempo_callback(
+ abl_link link, abl_link_tempo_callback callback, void *context);
+
+ /*! brief: Register a callback to be notified when the state of
+ * start/stop isPlaying changes.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ *
+ * @discussion The callback is invoked on a Link-managed thread.
+ */
+ typedef void (*abl_link_start_stop_callback)(bool is_playing, void *context);
+ void abl_link_set_start_stop_callback(
+ abl_link link, abl_link_start_stop_callback callback, void *context);
+
+ /*! brief: Get the current link clock time in microseconds.
+ * Thread-safe: yes
+ * Realtime-safe: yes
+ */
+ int64_t abl_link_clock_micros(abl_link link);
+
+ /*! @brief The representation of the current local state of a client in a Link Session
+ *
+ * @discussion A session state represents a timeline and the start/stop
+ * state. The timeline is a representation of a mapping between time and
+ * beats for varying quanta. The start/stop state represents the user
+ * intention to start or stop transport at a specific time. Start stop
+ * synchronization is an optional feature that allows to share the user
+ * request to start or stop transport between a subgroup of peers in a
+ * Link session. When observing a change of start/stop state, audio
+ * playback of a peer should be started or stopped the same way it would
+ * have happened if the user had requested that change at the according
+ * time locally. The start/stop state can only be changed by the user.
+ * This means that the current local start/stop state persists when
+ * joining or leaving a Link session. After joining a Link session
+ * start/stop change requests will be communicated to all connected peers.
+ */
+ typedef struct abl_link_session_state
+ {
+ void *impl;
+ } abl_link_session_state;
+
+ /*! @brief Create a new session_state instance.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ *
+ * @discussion The session_state is to be used with the abl_link_capture... and
+ * abl_link_commit... functions to capture snapshots of the current link state and pass
+ * changes to the link session.
+ */
+ abl_link_session_state abl_link_create_session_state(void);
+
+ /*! @brief Delete a session_state instance.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ */
+ void abl_link_destroy_session_state(abl_link_session_state abl_link_session_state);
+
+ /*! @brief Capture the current Link Session State from the audio thread.
+ * Thread-safe: no
+ * Realtime-safe: yes
+ *
+ * @discussion This function should ONLY be called in the audio thread and must not be
+ * accessed from any other threads. After capturing the session_state holds a snapshot
+ * of the current Link Session State, so it should be used in a local scope. The
+ * session_state should not be created on the audio thread.
+ */
+ void abl_link_capture_audio_session_state(
+ abl_link link, abl_link_session_state session_state);
+
+ /*! @brief Commit the given Session State to the Link session from the
+ * audio thread.
+ * Thread-safe: no
+ * Realtime-safe: yes
+ *
+ * @discussion This function should ONLY be called in the audio thread. The given
+ * session_state will replace the current Link state. Modifications will be
+ * communicated to other peers in the session.
+ */
+ void abl_link_commit_audio_session_state(
+ abl_link link, abl_link_session_state session_state);
+
+ /*! @brief Capture the current Link Session State from an application thread.
+ * Thread-safe: no
+ * Realtime-safe: yes
+ *
+ * @discussion Provides a mechanism for capturing the Link Session State from an
+ * application thread (other than the audio thread). After capturing the session_state
+ * contains a snapshot of the current Link state, so it should be used in a local
+ * scope.
+ */
+ void abl_link_capture_app_session_state(
+ abl_link link, abl_link_session_state session_state);
+
+ /*! @brief Commit the given Session State to the Link session from an
+ * application thread.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ *
+ * @discussion The given session_state will replace the current Link Session State.
+ * Modifications of the Session State will be communicated to other peers in the
+ * session.
+ */
+ void abl_link_commit_app_session_state(
+ abl_link link, abl_link_session_state session_state);
+
+ /*! @brief: The tempo of the timeline, in Beats Per Minute.
+ *
+ * @discussion This is a stable value that is appropriate for display to the user. Beat
+ * time progress will not necessarily match this tempo exactly because of clock drift
+ * compensation.
+ */
+ double abl_link_tempo(abl_link_session_state session_state);
+
+ /*! @brief: Set the timeline tempo to the given bpm value, taking effect at the given
+ * time.
+ */
+ void abl_link_set_tempo(
+ abl_link_session_state session_state, double bpm, int64_t at_time);
+
+ /*! @brief: Get the beat value corresponding to the given time for the given quantum.
+ *
+ * @discussion: The magnitude of the resulting beat value is unique to this Link
+ * client, but its phase with respect to the provided quantum is shared among all
+ * session peers. For non-negative beat values, the following property holds:
+ * fmod(beatAtTime(t, q), q) == phaseAtTime(t, q)
+ */
+ double abl_link_beat_at_time(
+ abl_link_session_state session_state, int64_t time, double quantum);
+
+ /*! @brief: Get the session phase at the given time for the given quantum.
+ *
+ * @discussion: The result is in the interval [0, quantum). The result is equivalent to
+ * fmod(beatAtTime(t, q), q) for non-negative beat values. This function is convenient
+ * if the client application is only interested in the phase and not the beat
+ * magnitude. Also, unlike fmod, it handles negative beat values correctly.
+ */
+ double abl_link_phase_at_time(
+ abl_link_session_state session_state, int64_t time, double quantum);
+
+ /*! @brief: Get the time at which the given beat occurs for the given quantum.
+ *
+ * @discussion: The inverse of beatAtTime, assuming a constant tempo.
+ * beatAtTime(timeAtBeat(b, q), q) === b.
+ */
+ int64_t abl_link_time_at_beat(
+ abl_link_session_state session_state, double beat, double quantum);
+
+ /*! @brief: Attempt to map the given beat to the given time in the context of the given
+ * quantum.
+ *
+ * @discussion: This function behaves differently depending on the state of the
+ * session. If no other peers are connected, then this abl_link instance is in a
+ * session by itself and is free to re-map the beat/time relationship whenever it
+ * pleases. In this case, beatAtTime(time, quantum) == beat after this funtion has been
+ * called.
+ *
+ * If there are other peers in the session, this abl_link instance should not abruptly
+ * re-map the beat/time relationship in the session because that would lead to beat
+ * discontinuities among the other peers. In this case, the given beat will be mapped
+ * to the next time value greater than the given time with the same phase as the given
+ * beat.
+ *
+ * This function is specifically designed to enable the concept of "quantized launch"
+ * in client applications. If there are no other peers in the session, then an event
+ * (such as starting transport) happens immediately when it is requested. If there are
+ * other peers, however, we wait until the next time at which the session phase matches
+ * the phase of the event, thereby executing the event in-phase with the other peers in
+ * the session. The client application only needs to invoke this function to achieve
+ * this behavior and should not need to explicitly check the number of peers.
+ */
+ void abl_link_request_beat_at_time(
+ abl_link_session_state session_state, double beat, int64_t time, double quantum);
+
+ /*! @brief: Rudely re-map the beat/time relationship for all peers in a session.
+ *
+ * @discussion: DANGER: This function should only be needed in certain special
+ * circumstances. Most applications should not use it. It is very similar to
+ * requestBeatAtTime except that it does not fall back to the quantizing behavior when
+ * it is in a session with other peers. Calling this function will unconditionally map
+ * the given beat to the given time and broadcast the result to the session. This is
+ * very anti-social behavior and should be avoided.
+ *
+ * One of the few legitimate uses of this function is to synchronize a Link session
+ * with an external clock source. By periodically forcing the beat/time mapping
+ * according to an external clock source, a peer can effectively bridge that clock into
+ * a Link session. Much care must be taken at the application layer when implementing
+ * such a feature so that users do not accidentally disrupt Link sessions that they may
+ * join.
+ */
+ void abl_link_force_beat_at_time(
+ abl_link_session_state session_state, double beat, uint64_t time, double quantum);
+
+ /*! @brief: Set if transport should be playing or stopped, taking effect at the given
+ * time.
+ */
+ void abl_link_set_is_playing(
+ abl_link_session_state session_state, bool is_playing, uint64_t time);
+
+ /*! @brief: Is transport playing? */
+ bool abl_link_is_playing(abl_link_session_state session_state);
+
+ /*! @brief: Get the time at which a transport start/stop occurs */
+ uint64_t abl_link_time_for_is_playing(abl_link_session_state session_state);
+
+ /*! @brief: Convenience function to attempt to map the given beat to the time
+ * when transport is starting to play in context of the given quantum.
+ * This function evaluates to a no-op if abl_link_is_playing equals false.
+ */
+ void abl_link_request_beat_at_start_playing_time(
+ abl_link_session_state session_state, double beat, double quantum);
+
+ /*! @brief: Convenience function to start or stop transport at a given time and attempt
+ * to map the given beat to this time in context of the given quantum. */
+ void abl_link_set_is_playing_and_request_beat_at_time(
+ abl_link_session_state session_state,
+ bool is_playing,
+ uint64_t time,
+ double beat,
+ double quantum);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif // __cplusplus
diff --git a/tidal-link/link/extensions/abl_link/src/abl_link.cpp b/tidal-link/link/extensions/abl_link/src/abl_link.cpp
new file mode 100644
index 000000000..e34f6b522
--- /dev/null
+++ b/tidal-link/link/extensions/abl_link/src/abl_link.cpp
@@ -0,0 +1,214 @@
+/* Copyright 2021, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#include
+#include
+
+extern "C"
+{
+ abl_link abl_link_create(double bpm)
+ {
+ return abl_link{reinterpret_cast(new ableton::Link(bpm))};
+ }
+
+ void abl_link_destroy(abl_link link)
+ {
+ delete reinterpret_cast(link.impl);
+ }
+
+ bool abl_link_is_enabled(abl_link link)
+ {
+ return reinterpret_cast(link.impl)->isEnabled();
+ }
+
+ void abl_link_enable(abl_link link, bool enabled)
+ {
+ reinterpret_cast(link.impl)->enable(enabled);
+ }
+
+ bool abl_link_is_start_stop_sync_enabled(abl_link link)
+ {
+ return reinterpret_cast(link.impl)->isStartStopSyncEnabled();
+ }
+
+ void abl_link_enable_start_stop_sync(abl_link link, bool enabled)
+ {
+ reinterpret_cast(link.impl)->enableStartStopSync(enabled);
+ }
+
+ uint64_t abl_link_num_peers(abl_link link)
+ {
+ return reinterpret_cast(link.impl)->numPeers();
+ }
+
+ void abl_link_set_num_peers_callback(
+ abl_link link, abl_link_num_peers_callback callback, void *context)
+ {
+ reinterpret_cast(link.impl)->setNumPeersCallback(
+ [callback, context](
+ std::size_t numPeers) { (*callback)(static_cast(numPeers), context); });
+ }
+
+ void abl_link_set_tempo_callback(
+ abl_link link, abl_link_tempo_callback callback, void *context)
+ {
+ reinterpret_cast(link.impl)->setTempoCallback(
+ [callback, context](double tempo) { (*callback)(tempo, context); });
+ }
+
+ void abl_link_set_start_stop_callback(
+ abl_link link, abl_link_start_stop_callback callback, void *context)
+ {
+ reinterpret_cast(link.impl)->setStartStopCallback(
+ [callback, context](bool isPlaying) { (*callback)(isPlaying, context); });
+ }
+
+ int64_t abl_link_clock_micros(abl_link link)
+ {
+ return reinterpret_cast(link.impl)->clock().micros().count();
+ }
+
+ abl_link_session_state abl_link_create_session_state(void)
+ {
+ return abl_link_session_state{reinterpret_cast(
+ new ableton::Link::SessionState{ableton::link::ApiState{}, {}})};
+ }
+
+ void abl_link_destroy_session_state(abl_link_session_state session_state)
+ {
+ delete reinterpret_cast(session_state.impl);
+ }
+
+ void abl_link_capture_app_session_state(
+ abl_link link, abl_link_session_state session_state)
+ {
+ *reinterpret_cast(session_state.impl) =
+ reinterpret_cast(link.impl)->captureAppSessionState();
+ }
+
+ void abl_link_commit_app_session_state(
+ abl_link link, abl_link_session_state session_state)
+ {
+ reinterpret_cast(link.impl)->commitAppSessionState(
+ *reinterpret_cast(session_state.impl));
+ }
+
+ void abl_link_capture_audio_session_state(
+ abl_link link, abl_link_session_state session_state)
+ {
+ *reinterpret_cast(session_state.impl) =
+ reinterpret_cast(link.impl)->captureAudioSessionState();
+ }
+
+ void abl_link_commit_audio_session_state(
+ abl_link link, abl_link_session_state session_state)
+ {
+ reinterpret_cast(link.impl)->commitAudioSessionState(
+ *reinterpret_cast(session_state.impl));
+ }
+
+ double abl_link_tempo(abl_link_session_state session_state)
+ {
+ return reinterpret_cast(session_state.impl)->tempo();
+ }
+
+ void abl_link_set_tempo(
+ abl_link_session_state session_state, double bpm, int64_t at_time)
+ {
+ reinterpret_cast(session_state.impl)
+ ->setTempo(bpm, std::chrono::microseconds{at_time});
+ }
+
+ double abl_link_beat_at_time(
+ abl_link_session_state session_state, int64_t time, double quantum)
+ {
+ auto micros = std::chrono::microseconds{time};
+ return reinterpret_cast(session_state.impl)
+ ->beatAtTime(micros, quantum);
+ }
+
+ double abl_link_phase_at_time(
+ abl_link_session_state session_state, int64_t time, double quantum)
+ {
+ return reinterpret_cast(session_state.impl)
+ ->phaseAtTime(std::chrono::microseconds{time}, quantum);
+ }
+
+ int64_t abl_link_time_at_beat(
+ abl_link_session_state session_state, double beat, double quantum)
+ {
+ return reinterpret_cast(session_state.impl)
+ ->timeAtBeat(beat, quantum)
+ .count();
+ }
+
+ void abl_link_request_beat_at_time(
+ abl_link_session_state session_state, double beat, int64_t time, double quantum)
+ {
+ reinterpret_cast(session_state.impl)
+ ->requestBeatAtTime(beat, std::chrono::microseconds{time}, quantum);
+ }
+
+ void abl_link_force_beat_at_time(
+ abl_link_session_state session_state, double beat, uint64_t time, double quantum)
+ {
+ reinterpret_cast(session_state.impl)
+ ->forceBeatAtTime(beat, std::chrono::microseconds{time}, quantum);
+ }
+
+ void abl_link_set_is_playing(
+ abl_link_session_state session_state, bool is_playing, uint64_t time)
+ {
+ reinterpret_cast(session_state.impl)
+ ->setIsPlaying(is_playing, std::chrono::microseconds(time));
+ }
+
+ bool abl_link_is_playing(abl_link_session_state session_state)
+ {
+ return reinterpret_cast(session_state.impl)
+ ->isPlaying();
+ }
+
+ uint64_t abl_link_time_for_is_playing(abl_link_session_state session_state)
+ {
+ return static_cast(
+ reinterpret_cast(session_state.impl)
+ ->timeForIsPlaying()
+ .count());
+ }
+
+ void abl_link_request_beat_at_start_playing_time(
+ abl_link_session_state session_state, double beat, double quantum)
+ {
+ reinterpret_cast(session_state.impl)
+ ->requestBeatAtStartPlayingTime(beat, quantum);
+ }
+
+ void abl_link_set_is_playing_and_request_beat_at_time(
+ abl_link_session_state session_state,
+ bool is_playing,
+ uint64_t time,
+ double beat,
+ double quantum)
+ {
+ reinterpret_cast(session_state.impl)
+ ->setIsPlayingAndRequestBeatAtTime(
+ is_playing, std::chrono::microseconds{time}, beat, quantum);
+ }
+}
diff --git a/tidal-link/link/include/CMakeLists.txt b/tidal-link/link/include/CMakeLists.txt
new file mode 100644
index 000000000..1c7b5ed96
--- /dev/null
+++ b/tidal-link/link/include/CMakeLists.txt
@@ -0,0 +1,186 @@
+cmake_minimum_required(VERSION 3.0)
+project(LinkCore)
+
+# ____
+# / ___|___ _ __ ___
+# | | / _ \| '__/ _ \
+# | |__| (_) | | | __/
+# \____\___/|_| \___|
+#
+
+set(link_core_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ableton/link)
+set(link_core_HEADERS
+ ${link_core_DIR}/Beats.hpp
+ ${link_core_DIR}/ClientSessionTimelines.hpp
+ ${link_core_DIR}/Controller.hpp
+ ${link_core_DIR}/Gateway.hpp
+ ${link_core_DIR}/GhostXForm.hpp
+ ${link_core_DIR}/HostTimeFilter.hpp
+ ${link_core_DIR}/LinearRegression.hpp
+ ${link_core_DIR}/Measurement.hpp
+ ${link_core_DIR}/MeasurementEndpointV4.hpp
+ ${link_core_DIR}/MeasurementService.hpp
+ ${link_core_DIR}/Median.hpp
+ ${link_core_DIR}/NodeId.hpp
+ ${link_core_DIR}/NodeState.hpp
+ ${link_core_DIR}/PayloadEntries.hpp
+ ${link_core_DIR}/Optional.hpp
+ ${link_core_DIR}/Peers.hpp
+ ${link_core_DIR}/PeerState.hpp
+ ${link_core_DIR}/Phase.hpp
+ ${link_core_DIR}/PingResponder.hpp
+ ${link_core_DIR}/SessionId.hpp
+ ${link_core_DIR}/SessionState.hpp
+ ${link_core_DIR}/Sessions.hpp
+ ${link_core_DIR}/StartStopState.hpp
+ ${link_core_DIR}/Tempo.hpp
+ ${link_core_DIR}/Timeline.hpp
+ ${link_core_DIR}/TripleBuffer.hpp
+ ${link_core_DIR}/v1/Messages.hpp
+ PARENT_SCOPE
+)
+
+# ____ _
+# | _ \(_)___ ___ _____ _____ _ __ _ _
+# | | | | / __|/ __/ _ \ \ / / _ \ '__| | | |
+# | |_| | \__ \ (_| (_) \ V / __/ | | |_| |
+# |____/|_|___/\___\___/ \_/ \___|_| \__, |
+# |___/
+
+set(link_discovery_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ableton/discovery)
+set(link_discovery_HEADERS
+ ${link_discovery_DIR}/InterfaceScanner.hpp
+ ${link_discovery_DIR}/IpV4Interface.hpp
+ ${link_discovery_DIR}/MessageTypes.hpp
+ ${link_discovery_DIR}/NetworkByteStreamSerializable.hpp
+ ${link_discovery_DIR}/Payload.hpp
+ ${link_discovery_DIR}/PeerGateway.hpp
+ ${link_discovery_DIR}/PeerGateways.hpp
+ ${link_discovery_DIR}/Service.hpp
+ ${link_discovery_DIR}/UdpMessenger.hpp
+ ${link_discovery_DIR}/v1/Messages.hpp
+ PARENT_SCOPE
+)
+
+# ____ _ _ __
+# | _ \| | __ _| |_ / _| ___ _ __ _ __ ___
+# | |_) | |/ _` | __| |_ / _ \| '__| '_ ` _ \
+# | __/| | (_| | |_| _| (_) | | | | | | | |
+# |_| |_|\__,_|\__|_| \___/|_| |_| |_| |_|
+#
+
+set(link_platform_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ableton/platforms)
+set(link_platform_HEADERS
+ ${link_platform_DIR}/Config.hpp
+ ${link_platform_DIR}/asio/AsioTimer.hpp
+ ${link_platform_DIR}/asio/AsioWrapper.hpp
+ ${link_platform_DIR}/asio/Context.hpp
+ ${link_platform_DIR}/asio/LockFreeCallbackDispatcher.hpp
+ ${link_platform_DIR}/asio/Socket.hpp
+ ${link_platform_DIR}/asio/Util.hpp
+)
+
+if(ESP_PLATFORM)
+ set(link_platform_HEADERS
+ ${link_platform_HEADERS}
+ ${link_platform_DIR}/esp32/Clock.hpp
+ ${link_platform_DIR}/esp32/Context.hpp
+ ${link_platform_DIR}/esp32/Esp32.hpp
+ ${link_platform_DIR}/esp32/Random.hpp
+ ${link_platform_DIR}/esp32/ScanIpIfAddrs.hpp
+ )
+elseif(UNIX)
+ set(link_platform_HEADERS
+ ${link_platform_HEADERS}
+ ${link_platform_DIR}/posix/ScanIpIfAddrs.hpp
+ )
+
+ if(APPLE)
+ set(link_platform_HEADERS
+ ${link_platform_HEADERS}
+ ${link_platform_DIR}/darwin/Clock.hpp
+ ${link_platform_DIR}/darwin/Darwin.hpp
+ ${link_platform_DIR}/darwin/ThreadFactory.hpp
+ ${link_platform_DIR}/stl/Random.hpp
+ )
+ elseif(CMAKE_SYSTEM_NAME MATCHES "Linux|kFreeBSD|GNU")
+ set(link_platform_HEADERS
+ ${link_platform_HEADERS}
+ ${link_platform_DIR}/linux/Clock.hpp
+ ${link_platform_DIR}/linux/Linux.hpp
+ ${link_platform_DIR}/stl/Clock.hpp
+ ${link_platform_DIR}/stl/Random.hpp
+ )
+ if(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+ set(link_platform_HEADERS
+ ${link_platform_HEADERS}
+ ${link_platform_DIR}/linux/ThreadFactory.hpp
+ )
+ endif()
+ endif()
+elseif(WIN32)
+ set(link_platform_HEADERS
+ ${link_platform_HEADERS}
+ ${link_platform_DIR}/stl/Random.hpp
+ ${link_platform_DIR}/windows/Clock.hpp
+ ${link_platform_DIR}/windows/ScanIpIfAddrs.hpp
+ ${link_platform_DIR}/windows/ThreadFactory.hpp
+ ${link_platform_DIR}/windows/Windows.hpp
+ )
+endif()
+set(link_platform_HEADERS
+ ${link_platform_HEADERS}
+ PARENT_SCOPE
+)
+
+# _ _ _ _ _
+# | | | | |_(_) |
+# | | | | __| | |
+# | |_| | |_| | |
+# \___/ \__|_|_|
+#
+
+set(link_util_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ableton/util)
+set(link_util_HEADERS
+ ${link_util_DIR}/Injected.hpp
+ ${link_util_DIR}/Log.hpp
+ ${link_util_DIR}/SafeAsyncHandler.hpp
+ ${link_util_DIR}/SampleTiming.hpp
+ PARENT_SCOPE
+)
+
+# _____ _ _
+# | ____|_ ___ __ ___ _ __| |_ ___ __| |
+# | _| \ \/ / '_ \ / _ \| '__| __/ _ \/ _` |
+# | |___ > <| |_) | (_) | | | || __/ (_| |
+# |_____/_/\_\ .__/ \___/|_| \__\___|\__,_|
+# |_|
+
+# This list contains all of the headers needed by most Link projects.
+# Usually, just adding this variable to your linker targets will place
+# all relevant Link headers in your project.
+set(link_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ableton)
+set(link_HEADERS
+ ${link_core_HEADERS}
+ ${link_discovery_HEADERS}
+ ${link_platform_HEADERS}
+ ${link_util_HEADERS}
+ ${link_DIR}/Link.hpp
+ ${link_DIR}/Link.ipp
+ PARENT_SCOPE
+)
+
+set(link_test_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ableton/test)
+set(link_test_HEADERS
+ ${link_discovery_DIR}/test/Interface.hpp
+ ${link_discovery_DIR}/test/PayloadEntries.hpp
+ ${link_discovery_DIR}/test/Socket.hpp
+ ${link_util_DIR}/test/IoService.hpp
+ ${link_util_DIR}/test/Timer.hpp
+ ${link_test_DIR}/CatchWrapper.hpp
+ ${link_test_DIR}/serial_io/Context.hpp
+ ${link_test_DIR}/serial_io/Fixture.hpp
+ ${link_test_DIR}/serial_io/SchedulerTree.hpp
+ ${link_test_DIR}/serial_io/Timer.hpp
+ PARENT_SCOPE
+)
diff --git a/tidal-link/link/include/ableton/Link.hpp b/tidal-link/link/include/ableton/Link.hpp
new file mode 100644
index 000000000..8b915977e
--- /dev/null
+++ b/tidal-link/link/include/ableton/Link.hpp
@@ -0,0 +1,403 @@
+/*! @file Link.hpp
+ * @copyright 2016, Ableton AG, Berlin. All rights reserved.
+ * @brief Library for cross-device shared tempo and quantized beat grid
+ *
+ * @license:
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+namespace ableton
+{
+
+/*! @class Link and BasicLink
+ * @brief Classes representing a participant in a Link session.
+ * The BasicLink type allows to customize the clock. The Link type
+ * uses the recommended platform-dependent representation of the
+ * system clock as defined in platforms/Config.hpp.
+ * It's preferred to use Link instead of BasicLink.
+ *
+ * @discussion Each Link instance has its own session state which
+ * represents a beat timeline and a transport start/stop state. The
+ * timeline starts running from beat 0 at the initial tempo when
+ * constructed. The timeline always advances at a speed defined by
+ * its current tempo, even if transport is stopped. Synchronizing to the
+ * transport start/stop state of Link is optional for every peer.
+ * The transport start/stop state is only shared with other peers when
+ * start/stop synchronization is enabled.
+ *
+ * A Link instance is initially disabled after construction, which
+ * means that it will not communicate on the network. Once enabled,
+ * a Link instance initiates network communication in an effort to
+ * discover other peers. When peers are discovered, they immediately
+ * become part of a shared Link session.
+ *
+ * Each method of the Link type documents its thread-safety and
+ * realtime-safety properties. When a method is marked thread-safe,
+ * it means it is safe to call from multiple threads
+ * concurrently. When a method is marked realtime-safe, it means that
+ * it does not block and is appropriate for use in the thread that
+ * performs audio IO.
+ *
+ * Link provides one session state capture/commit method pair for use
+ * in the audio thread and one for all other application contexts. In
+ * general, modifying the session state should be done in the audio
+ * thread for the most accurate timing results. The ability to modify
+ * the session state from application threads should only be used in
+ * cases where an application's audio thread is not actively running
+ * or if it doesn't generate audio at all. Modifying the Link session
+ * state from both the audio thread and an application thread
+ * concurrently is not advised and will potentially lead to unexpected
+ * behavior.
+ *
+ * Only use the BasicLink class if the default platform clock does not
+ * fulfill other requirements of the client application. Please note this
+ * will require providing a custom Clock implementation. See the clock()
+ * documentation for details.
+ */
+template
+class BasicLink
+{
+public:
+ class SessionState;
+
+ /*! @brief Construct with an initial tempo. */
+ BasicLink(double bpm);
+
+ /*! @brief Link instances cannot be copied or moved */
+ BasicLink(const BasicLink&) = delete;
+ BasicLink& operator=(const BasicLink&) = delete;
+ BasicLink(BasicLink&&) = delete;
+ BasicLink& operator=(BasicLink&&) = delete;
+
+ /*! @brief Is Link currently enabled?
+ * Thread-safe: yes
+ * Realtime-safe: yes
+ */
+ bool isEnabled() const;
+
+ /*! @brief Enable/disable Link.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ */
+ void enable(bool bEnable);
+
+ /*! @brief: Is start/stop synchronization enabled?
+ * Thread-safe: yes
+ * Realtime-safe: no
+ */
+ bool isStartStopSyncEnabled() const;
+
+ /*! @brief: Enable start/stop synchronization.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ */
+ void enableStartStopSync(bool bEnable);
+
+ /*! @brief How many peers are currently connected in a Link session?
+ * Thread-safe: yes
+ * Realtime-safe: yes
+ */
+ std::size_t numPeers() const;
+
+ /*! @brief Register a callback to be notified when the number of
+ * peers in the Link session changes.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ *
+ * @discussion The callback is invoked on a Link-managed thread.
+ *
+ * @param callback The callback signature is:
+ * void (std::size_t numPeers)
+ */
+ template
+ void setNumPeersCallback(Callback callback);
+
+ /*! @brief Register a callback to be notified when the session
+ * tempo changes.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ *
+ * @discussion The callback is invoked on a Link-managed thread.
+ *
+ * @param callback The callback signature is: void (double bpm)
+ */
+ template
+ void setTempoCallback(Callback callback);
+
+ /*! brief: Register a callback to be notified when the state of
+ * start/stop isPlaying changes.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ *
+ * @discussion The callback is invoked on a Link-managed thread.
+ *
+ * @param callback The callback signature is:
+ * void (bool isPlaying)
+ */
+ template
+ void setStartStopCallback(Callback callback);
+
+ /*! @brief The clock used by Link.
+ * Thread-safe: yes
+ * Realtime-safe: yes
+ *
+ * @discussion The Clock type is a platform-dependent representation
+ * of the system clock. It exposes a micros() method, which is a
+ * normalized representation of the current system time in
+ * std::chrono::microseconds.
+ */
+ Clock clock() const;
+
+ /*! @brief Capture the current Link Session State from the audio thread.
+ * Thread-safe: no
+ * Realtime-safe: yes
+ *
+ * @discussion This method should ONLY be called in the audio thread
+ * and must not be accessed from any other threads. The returned
+ * object stores a snapshot of the current Link Session State, so it
+ * should be captured and used in a local scope. Storing the
+ * Session State for later use in a different context is not advised
+ * because it will provide an outdated view.
+ */
+ SessionState captureAudioSessionState() const;
+
+ /*! @brief Commit the given Session State to the Link session from the
+ * audio thread.
+ * Thread-safe: no
+ * Realtime-safe: yes
+ *
+ * @discussion This method should ONLY be called in the audio
+ * thread. The given Session State will replace the current Link
+ * state. Modifications will be communicated to other peers in the
+ * session.
+ */
+ void commitAudioSessionState(SessionState state);
+
+ /*! @brief Capture the current Link Session State from an application
+ * thread.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ *
+ * @discussion Provides a mechanism for capturing the Link Session
+ * State from an application thread (other than the audio thread).
+ * The returned Session State stores a snapshot of the current Link
+ * state, so it should be captured and used in a local scope.
+ * Storing the it for later use in a different context is not
+ * advised because it will provide an outdated view.
+ */
+ SessionState captureAppSessionState() const;
+
+ /*! @brief Commit the given Session State to the Link session from an
+ * application thread.
+ * Thread-safe: yes
+ * Realtime-safe: no
+ *
+ * @discussion The given Session State will replace the current Link
+ * Session State. Modifications of the Session State will be
+ * communicated to other peers in the session.
+ */
+ void commitAppSessionState(SessionState state);
+
+ /*! @class SessionState
+ * @brief Representation of a timeline and the start/stop state
+ *
+ * @discussion A SessionState object is intended for use in a local scope within
+ * a single thread - none of its methods are thread-safe. All of its methods are
+ * non-blocking, so it is safe to use from a realtime thread.
+ * It provides functions to observe and manipulate the timeline and start/stop
+ * state.
+ *
+ * The timeline is a representation of a mapping between time and beats for varying
+ * quanta.
+ * The start/stop state represents the user intention to start or stop transport at
+ * a specific time. Start stop synchronization is an optional feature that allows to
+ * share the user request to start or stop transport between a subgroup of peers in
+ * a Link session. When observing a change of start/stop state, audio playback of a
+ * peer should be started or stopped the same way it would have happened if the user
+ * had requested that change at the according time locally. The start/stop state can
+ * only be changed by the user. This means that the current local start/stop state
+ * persists when joining or leaving a Link session. After joining a Link session
+ * start/stop change requests will be communicated to all connected peers.
+ */
+ class SessionState
+ {
+ public:
+ SessionState(const link::ApiState state, const bool bRespectQuantum);
+
+ /*! @brief: The tempo of the timeline, in Beats Per Minute.
+ *
+ * @discussion This is a stable value that is appropriate for display
+ * to the user. Beat time progress will not necessarily match this tempo
+ * exactly because of clock drift compensation.
+ */
+ double tempo() const;
+
+ /*! @brief: Set the timeline tempo to the given bpm value, taking
+ * effect at the given time.
+ */
+ void setTempo(double bpm, std::chrono::microseconds atTime);
+
+ /*! @brief: Get the beat value corresponding to the given time
+ * for the given quantum.
+ *
+ * @discussion: The magnitude of the resulting beat value is
+ * unique to this Link instance, but its phase with respect to
+ * the provided quantum is shared among all session
+ * peers. For non-negative beat values, the following
+ * property holds: fmod(beatAtTime(t, q), q) == phaseAtTime(t, q)
+ */
+ double beatAtTime(std::chrono::microseconds time, double quantum) const;
+
+ /*! @brief: Get the session phase at the given time for the given
+ * quantum.
+ *
+ * @discussion: The result is in the interval [0, quantum). The
+ * result is equivalent to fmod(beatAtTime(t, q), q) for
+ * non-negative beat values. This method is convenient if the
+ * client is only interested in the phase and not the beat
+ * magnitude. Also, unlike fmod, it handles negative beat values
+ * correctly.
+ */
+ double phaseAtTime(std::chrono::microseconds time, double quantum) const;
+
+ /*! @brief: Get the time at which the given beat occurs for the
+ * given quantum.
+ *
+ * @discussion: The inverse of beatAtTime, assuming a constant
+ * tempo. beatAtTime(timeAtBeat(b, q), q) === b.
+ */
+ std::chrono::microseconds timeAtBeat(double beat, double quantum) const;
+
+ /*! @brief: Attempt to map the given beat to the given time in the
+ * context of the given quantum.
+ *
+ * @discussion: This method behaves differently depending on the
+ * state of the session. If no other peers are connected,
+ * then this instance is in a session by itself and is free to
+ * re-map the beat/time relationship whenever it pleases. In this
+ * case, beatAtTime(time, quantum) == beat after this method has
+ * been called.
+ *
+ * If there are other peers in the session, this instance
+ * should not abruptly re-map the beat/time relationship in the
+ * session because that would lead to beat discontinuities among
+ * the other peers. In this case, the given beat will be mapped
+ * to the next time value greater than the given time with the
+ * same phase as the given beat.
+ *
+ * This method is specifically designed to enable the concept of
+ * "quantized launch" in client applications. If there are no other
+ * peers in the session, then an event (such as starting
+ * transport) happens immediately when it is requested. If there
+ * are other peers, however, we wait until the next time at which
+ * the session phase matches the phase of the event, thereby
+ * executing the event in-phase with the other peers in the
+ * session. The client only needs to invoke this method to
+ * achieve this behavior and should not need to explicitly check
+ * the number of peers.
+ */
+ void requestBeatAtTime(double beat, std::chrono::microseconds time, double quantum);
+
+ /*! @brief: Rudely re-map the beat/time relationship for all peers
+ * in a session.
+ *
+ * @discussion: DANGER: This method should only be needed in
+ * certain special circumstances. Most applications should not
+ * use it. It is very similar to requestBeatAtTime except that it
+ * does not fall back to the quantizing behavior when it is in a
+ * session with other peers. Calling this method will
+ * unconditionally map the given beat to the given time and
+ * broadcast the result to the session. This is very anti-social
+ * behavior and should be avoided.
+ *
+ * One of the few legitimate uses of this method is to
+ * synchronize a Link session with an external clock source. By
+ * periodically forcing the beat/time mapping according to an
+ * external clock source, a peer can effectively bridge that
+ * clock into a Link session. Much care must be taken at the
+ * application layer when implementing such a feature so that
+ * users do not accidentally disrupt Link sessions that they may
+ * join.
+ */
+ void forceBeatAtTime(double beat, std::chrono::microseconds time, double quantum);
+
+ /*! @brief: Set if transport should be playing or stopped, taking effect
+ * at the given time.
+ */
+ void setIsPlaying(bool isPlaying, std::chrono::microseconds time);
+
+ /*! @brief: Is transport playing? */
+ bool isPlaying() const;
+
+ /*! @brief: Get the time at which a transport start/stop occurs */
+ std::chrono::microseconds timeForIsPlaying() const;
+
+ /*! @brief: Convenience function to attempt to map the given beat to the time
+ * when transport is starting to play in context of the given quantum.
+ * This function evaluates to a no-op if isPlaying() equals false.
+ */
+ void requestBeatAtStartPlayingTime(double beat, double quantum);
+
+ /*! @brief: Convenience function to start or stop transport at a given time and
+ * attempt to map the given beat to this time in context of the given quantum.
+ */
+ void setIsPlayingAndRequestBeatAtTime(
+ bool isPlaying, std::chrono::microseconds time, double beat, double quantum);
+
+ private:
+ friend BasicLink;
+ link::ApiState mOriginalState;
+ link::ApiState mState;
+ bool mbRespectQuantum;
+ };
+
+private:
+ using Controller = ableton::link::Controller;
+
+ std::mutex mCallbackMutex;
+ link::PeerCountCallback mPeerCountCallback = [](std::size_t) {};
+ link::TempoCallback mTempoCallback = [](link::Tempo) {};
+ link::StartStopStateCallback mStartStopCallback = [](bool) {};
+ Clock mClock;
+ Controller mController;
+};
+
+class Link : public BasicLink
+{
+public:
+ using Clock = link::platform::Clock;
+
+ Link(double bpm)
+ : BasicLink(bpm)
+ {
+ }
+};
+
+} // namespace ableton
+
+#include
diff --git a/tidal-link/link/include/ableton/Link.ipp b/tidal-link/link/include/ableton/Link.ipp
new file mode 100644
index 000000000..f8cbce832
--- /dev/null
+++ b/tidal-link/link/include/ableton/Link.ipp
@@ -0,0 +1,280 @@
+/* Copyright 2016, Ableton AG, Berlin. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you would like to incorporate Link into a proprietary software application,
+ * please contact .
+ */
+
+#pragma once
+
+#include
+
+namespace ableton
+{
+namespace detail
+{
+
+template
+inline typename BasicLink::SessionState toSessionState(
+ const link::ClientState& state, const bool isConnected)
+{
+ return {{state.timeline, {state.startStopState.isPlaying, state.startStopState.time}},
+ isConnected};
+}
+
+inline link::IncomingClientState toIncomingClientState(const link::ApiState& state,
+ const link::ApiState& originalState,
+ const std::chrono::microseconds timestamp)
+{
+ const auto timeline = originalState.timeline != state.timeline
+ ? link::OptionalTimeline{state.timeline}
+ : link::OptionalTimeline{};
+ const auto startStopState =
+ originalState.startStopState != state.startStopState
+ ? link::OptionalClientStartStopState{{state.startStopState.isPlaying,
+ state.startStopState.time, timestamp}}
+ : link::OptionalClientStartStopState{};
+ return {timeline, startStopState, timestamp};
+}
+
+} // namespace detail
+
+template
+inline BasicLink::BasicLink(const double bpm)
+ : mController(link::Tempo(bpm),
+ [this](const std::size_t peers) {
+ std::lock_guard lock(mCallbackMutex);
+ mPeerCountCallback(peers);
+ },
+ [this](const link::Tempo tempo) {
+ std::lock_guard lock(mCallbackMutex);
+ mTempoCallback(tempo);
+ },
+ [this](const bool isPlaying) {
+ std::lock_guard lock(mCallbackMutex);
+ mStartStopCallback(isPlaying);
+ },
+ mClock)
+{
+}
+
+template
+inline bool BasicLink::isEnabled() const
+{
+ return mController.isEnabled();
+}
+
+template
+inline void BasicLink::enable(const bool bEnable)
+{
+ mController.enable(bEnable);
+}
+
+template
+inline bool BasicLink::isStartStopSyncEnabled() const
+{
+ return mController.isStartStopSyncEnabled();
+}
+
+template
+inline void BasicLink::enableStartStopSync(bool bEnable)
+{
+ mController.enableStartStopSync(bEnable);
+}
+
+template
+inline std::size_t BasicLink::numPeers() const
+{
+ return mController.numPeers();
+}
+
+template
+template
+void BasicLink::setNumPeersCallback(Callback callback)
+{
+ std::lock_guard lock(mCallbackMutex);
+ mPeerCountCallback = [callback](const std::size_t numPeers) { callback(numPeers); };
+}
+
+template
+template
+void BasicLink::setTempoCallback(Callback callback)
+{
+ std::lock_guard lock(mCallbackMutex);
+ mTempoCallback = [callback](const link::Tempo tempo) { callback(tempo.bpm()); };
+}
+
+template
+template
+void BasicLink::setStartStopCallback(Callback callback)
+{
+ std::lock_guard lock(mCallbackMutex);
+ mStartStopCallback = callback;
+}
+
+template
+inline Clock BasicLink::clock() const
+{
+ return mClock;
+}
+
+template
+inline typename BasicLink::SessionState BasicLink<
+ Clock>::captureAudioSessionState() const
+{
+ return detail::toSessionState(mController.clientStateRtSafe(), numPeers() > 0);
+}
+
+template
+inline void BasicLink::commitAudioSessionState(
+ const typename BasicLink::SessionState state)
+{
+ mController.setClientStateRtSafe(
+ detail::toIncomingClientState(state.mState, state.mOriginalState, mClock.micros()));
+}
+
+template
+inline typename BasicLink::SessionState BasicLink::captureAppSessionState()
+ const
+{
+ return detail::toSessionState(mController.clientState(), numPeers() > 0);
+}
+
+template
+inline void BasicLink::commitAppSessionState(
+ const typename BasicLink::SessionState state)
+{
+ mController.setClientState(
+ detail::toIncomingClientState(state.mState, state.mOriginalState, mClock.micros()));
+}
+
+// Link::SessionState
+
+template
+inline BasicLink::SessionState::SessionState(
+ const link::ApiState state, const bool bRespectQuantum)
+ : mOriginalState(state)
+ , mState(state)
+ , mbRespectQuantum(bRespectQuantum)
+{
+}
+
+template
+inline double BasicLink::SessionState::tempo() const
+{
+ return mState.timeline.tempo.bpm();
+}
+
+template
+inline void BasicLink::SessionState::setTempo(
+ const double bpm, const std::chrono::microseconds atTime)
+{
+ const auto desiredTl = link::clampTempo(
+ link::Timeline{link::Tempo(bpm), mState.timeline.toBeats(atTime), atTime});
+ mState.timeline.tempo = desiredTl.tempo;
+ mState.timeline.timeOrigin = desiredTl.fromBeats(mState.timeline.beatOrigin);
+}
+
+template
+inline double BasicLink::SessionState::beatAtTime(
+ const std::chrono::microseconds time, const double quantum) const
+{
+ return link::toPhaseEncodedBeats(mState.timeline, time, link::Beats{quantum})
+ .floating();
+}
+
+template
+inline double BasicLink::SessionState::phaseAtTime(
+ const std::chrono::microseconds time, const double quantum) const
+{
+ return link::phase(link::Beats{beatAtTime(time, quantum)}, link::Beats{quantum})
+ .floating();
+}
+
+template
+inline std::chrono::microseconds BasicLink