{- | Simple, blocking note\/beat playback built directly on
'Termtab.Audio.AudioEngine' — no transport control (play\/pause\/stop),
no position tracking, just "turn this on, sleep, turn it off" for as
many beats as given. Used where a single synchronous sequence needs to
be auditioned (e.g. previewing a track in isolation); full 'Termtab.Types.Song'
playback with pause\/resume\/seek instead goes through
"Termtab.Audio.PlaybackThread", which reuses 'durationToMicroseconds'
from here for its own timing.
-}
module Termtab.Audio.Playback (
    durationToMicroseconds,
    playNote,
    playSequence,
) where

import Control.Concurrent (threadDelay)

import Termtab.Audio (AudioEngine, engineNoteOff, engineNoteOn)
import Termtab.Types

{- | Convert a 'Duration' at a given 'Tempo' (BPM) to microseconds.

A quarter note at 120 BPM = 500,000 µs (0.5 seconds).
-}
durationToMicroseconds :: Duration -> Tempo -> Int
durationToMicroseconds :: Duration -> Tempo -> Int
durationToMicroseconds Duration
dur (Tempo Int
bpm) =
    let quarterUs :: Int
quarterUs = Int
60_000_000 Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
bpm
     in Duration -> Int -> Int
durationMultiplier Duration
dur Int
quarterUs

{- | Scale a base (quarter-note) microsecond value to the given 'Duration',
used by 'durationToMicroseconds'. 'Dotted' adds half again; 'Triplet'
takes two-thirds — both recurse on their wrapped 'Duration' so nested
combinations (e.g. a dotted triplet) scale correctly.
-}
durationMultiplier :: Duration -> Int -> Int
durationMultiplier :: Duration -> Int -> Int
durationMultiplier Duration
Whole Int
base = Int
base Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
4
durationMultiplier Duration
Half Int
base = Int
base Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
2
durationMultiplier Duration
Quarter Int
base = Int
base
durationMultiplier Duration
Eighth Int
base = Int
base Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
2
durationMultiplier Duration
Sixteenth Int
base = Int
base Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
4
durationMultiplier Duration
Thirty2nd Int
base = Int
base Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
8
durationMultiplier (Dotted Duration
d) Int
base = Duration -> Int -> Int
durationMultiplier Duration
d Int
base Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Duration -> Int -> Int
durationMultiplier Duration
d Int
base Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
2
durationMultiplier (Triplet Duration
d) Int
base = Duration -> Int -> Int
durationMultiplier Duration
d Int
base Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
2 Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
3

-- | Play a single note for the given duration, then turn it off.
playNote :: AudioEngine -> MidiChannel -> Pitch -> Velocity -> Duration -> Tempo -> IO ()
playNote :: AudioEngine
-> MidiChannel -> Pitch -> Velocity -> Duration -> Tempo -> IO ()
playNote AudioEngine
engine MidiChannel
ch Pitch
pitch Velocity
vel Duration
dur Tempo
tempo = do
    AudioEngine -> MidiChannel -> Pitch -> Velocity -> IO ()
engineNoteOn AudioEngine
engine MidiChannel
ch Pitch
pitch Velocity
vel
    Int -> IO ()
threadDelay (Duration -> Tempo -> Int
durationToMicroseconds Duration
dur Tempo
tempo)
    AudioEngine -> MidiChannel -> Pitch -> IO ()
engineNoteOff AudioEngine
engine MidiChannel
ch Pitch
pitch

-- | Play a sequence of beats on a given channel at a given tempo.
playSequence :: AudioEngine -> MidiChannel -> Tempo -> [Beat] -> IO ()
playSequence :: AudioEngine -> MidiChannel -> Tempo -> [Beat] -> IO ()
playSequence AudioEngine
engine MidiChannel
ch Tempo
tempo = (Beat -> IO ()) -> [Beat] -> IO ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ Beat -> IO ()
playBeat
  where
    playBeat :: Beat -> IO ()
playBeat Beat
beat
        | Beat -> Bool
beatIsRest Beat
beat = Int -> IO ()
threadDelay (Duration -> Tempo -> Int
durationToMicroseconds (Beat -> Duration
beatDuration Beat
beat) Tempo
tempo)
        | Bool
otherwise = do
            (Note -> IO ()) -> [Note] -> IO ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ (\Note
n -> AudioEngine -> MidiChannel -> Pitch -> Velocity -> IO ()
engineNoteOn AudioEngine
engine MidiChannel
ch (Note -> Pitch
notePitch Note
n) (Note -> Velocity
noteVelocity Note
n)) (Beat -> [Note]
beatNotes Beat
beat)
            Int -> IO ()
threadDelay (Duration -> Tempo -> Int
durationToMicroseconds (Beat -> Duration
beatDuration Beat
beat) Tempo
tempo)
            (Note -> IO ()) -> [Note] -> IO ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ (AudioEngine -> MidiChannel -> Pitch -> IO ()
engineNoteOff AudioEngine
engine MidiChannel
ch (Pitch -> IO ()) -> (Note -> Pitch) -> Note -> IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Note -> Pitch
notePitch) (Beat -> [Note]
beatNotes Beat
beat)