Skip to content

Instantly share code, notes, and snippets.

@jarmitage
Created December 11, 2025 17:30
Show Gist options
  • Select an option

  • Save jarmitage/690e84a3ba16dcf894bb183e94de4936 to your computer and use it in GitHub Desktop.

Select an option

Save jarmitage/690e84a3ba16dcf894bb183e94de4936 to your computer and use it in GitHub Desktop.
TidalCycles prompt live monitor, see e.g. https://www.youtube.com/watch?v=_N0cikVFu-o

How CycleMonitor Works

This module creates a background thread that continuously prints timing/cycle information to the terminal.

Core Functions

  1. streamGetnowMods - Gets the current cycle position and calculates where you are within multiple time subdivisions (mod, mod×2, mod×4, mod×8, mod×16, mod×32):
streamGetnowMods :: Double -> Stream -> IO [Double]
streamGetnowMods mod s = do
  let mods = take 6 $ iterate (*2) mod
  result <- streamGetnow s
  return $! map (mod' result) mods
  1. streamGetnowModsString - Formats this into a readable string like c:35 | 1/8 3/16 7/32...:
streamGetnowModsString :: Double -> Stream -> IO String
streamGetnowModsString mod s = do
  let mods = take 6 $ iterate (*2) mod
  results <- streamGetnowMods mod s
  cyclePos <- streamGetnow s
  let formattedResults = printf "c:%.0f |" (cyclePos+1) : zipWith (\x y -> printf "%d/%.0f" (floor x + 1) y) results mods
  return $ unwords formattedResults
  1. printEverySecond - The main loop that prints BPM + cycle position. Despite the name, it actually delays based on CPS (cycles per second), so it updates roughly once per cycle:
printEverySecond :: Double -> Stream -> IO ()
printEverySecond mod s = CM.forever $ do
    result <- streamGetnowModsString mod s
    bpm <- getbpm s
    let bpmString = "t:" ++ printf "%.2f" bpm
    cps <- streamGetcps s
    let delay = floor (1 / cps * 1000000) -- convert cps to microseconds
    threadDelay delay
    putStrLn (bpmString ++ " | " ++ result)

How It's Started/Stopped

startPrinting spawns a background thread using forkIO and returns an MVar (a thread-safe variable) for control:

startPrinting :: Double -> Stream -> IO (MVar ())
startPrinting mod s = do
    stopVar <- newEmptyMVar
    _ <- forkIO $ do
        let loop = do
                stopped <- tryTakeMVar stopVar
                CM.when (stopped == Nothing) $ do
                    printEverySecond mod s
                    loop
        loop
    return stopVar

stopPrinting signals the thread to stop by putting a value in the MVar:

stopPrinting :: MVar () -> IO ()
stopPrinting stopVar = do
    putMVar stopVar ()
    return ()

Usage Pattern

To use it in your boot/session:

-- Start monitoring (8 = base cycle subdivision)
monitor <- startPrinting 8 stream

-- Later, to stop:
stopPrinting monitor

The output looks like: t:120.00 | c:35 | 1/8 3/16 7/32 15/64 31/128 63/256

This tells you the BPM, current cycle number, and your position within increasingly longer cycle periods (useful for tracking where you are in longer structural patterns like ur patterns).

-- Research VSCode plugin/sidebar?
-- https://code.visualstudio.com/api/references/vscode-api#StatusBarItem
-- https://github.com/microsoft/vscode-extension-samples/blob/main/statusbar-sample/src/extension.ts
-- https://github.com/tidalcycles/vscode-tidalcycles/blob/master/src/main.ts
-- t:120 | l0 . c:0 . 190_operon(1,2,5 | pc,bd,syn): [35/64, -29 | 1:20s, 50s]
-- TODO:
-- Link clients
-- streamIDs/pattern names
-- elapsed / remaining of current ur patterns (in cycles and seconds)
-- track names
-- fft sparkline?
-- mods: Unicode fractions? Custom glyphs/random emojis?
-- intech grid values (not raw MIDI but transformed val, e.g. fast/nudge amounts)
-- set index, track names
:{
import Data.Fixed (mod')
import Text.Printf (printf)
import Control.Concurrent
import Control.Monad hiding (when)
import qualified Control.Monad as CM
streamGetnowMods :: Double -> Stream -> IO [Double]
streamGetnowMods mod s = do
let mods = take 6 $ iterate (*2) mod
result <- streamGetnow s
return $! map (mod' result) mods
streamGetnowModsString :: Double -> Stream -> IO String
streamGetnowModsString mod s = do
let mods = take 6 $ iterate (*2) mod
results <- streamGetnowMods mod s
cyclePos <- streamGetnow s
let formattedResults = printf "c:%.0f |" (cyclePos+1) : zipWith (\x y -> printf "%d/%.0f" (floor x + 1) y) results mods
return $ unwords formattedResults
printEverySecond :: Double -> Stream -> IO ()
printEverySecond mod s = CM.forever $ do
result <- streamGetnowModsString mod s
bpm <- getbpm s
let bpmString = "t:" ++ printf "%.2f" bpm
cps <- streamGetcps s
let delay = floor (1 / cps * 1000000) -- convert cps to microseconds
threadDelay delay
putStrLn (bpmString ++ " | " ++ result)
startPrinting :: Double -> Stream -> IO (MVar ())
startPrinting mod s = do
stopVar <- newEmptyMVar
_ <- forkIO $ do
let loop = do
stopped <- tryTakeMVar stopVar
CM.when (stopped == Nothing) $ do
printEverySecond mod s
loop
loop
return stopVar
stopPrinting :: MVar () -> IO ()
stopPrinting stopVar = do
putMVar stopVar ()
return ()
:}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment