{- | Backend-agnostic handle for sound playback.

'AudioEngine' hides which synthesis backend is actually in use behind a
single opaque type; today the only implemented backend is FluidSynth
(see "Termtab.Audio.FluidSynth"). The @engine*@ functions here are
direct, synchronous calls meant for simple one-off sounds (e.g. an
editor \"preview this note\" action); driving full-song playback with
correct timing instead goes through "Termtab.Audio.Playback" and
"Termtab.Audio.PlaybackThread", which queue 'AudioCommand's (re-exported
from "Termtab.Audio.Types") rather than calling these directly.
-}
module Termtab.Audio (
    AudioEngine,
    startAudioEngine,
    stopAudioEngine,
    withAudioEngine,
    -- Direct synchronous calls (for console playback)
    engineNoteOn,
    engineNoteOff,
    engineProgramChange,
    -- Re-exports
    module Termtab.Audio.Types,
) where

import Control.Exception (bracket)

import Termtab.Audio.FluidSynth qualified as FS
import Termtab.Audio.Types
import Termtab.Types (MidiChannel, MidiProgram, Pitch, Velocity)

{- | An initialized backend, ready to receive note events. Opaque to
callers outside this module — dispatch on which backend is active
happens only in the @engine*@ functions below via 'BackendHandle'.
-}
newtype AudioEngine = AudioEngine
    { AudioEngine -> BackendHandle
aeBackend :: BackendHandle
    }

{- | The concrete backend behind an 'AudioEngine'. Currently only wraps
FluidSynth; a 'BackendPortMidi' variant would be added here once
"Termtab.Audio.PortMidi" backs an actual engine rather than raw MIDI I/O.
-}
newtype BackendHandle = FluidSynthHandle FS.FluidSynthEngine

{- | Initialize the requested backend. 'BackendPortMidi' is not yet backed
by an engine implementation and calling it @error@s immediately —
callers should only pass 'BackendFluidSynth' until that lands.
-}
startAudioEngine :: BackendType -> AudioConfig -> IO AudioEngine
startAudioEngine :: BackendType -> AudioConfig -> IO AudioEngine
startAudioEngine BackendType
BackendFluidSynth AudioConfig
cfg = do
    engine <- AudioConfig -> IO FluidSynthEngine
FS.initFluidSynth AudioConfig
cfg
    return AudioEngine{aeBackend = FluidSynthHandle engine}
startAudioEngine BackendType
BackendPortMidi AudioConfig
_ =
    [Char] -> IO AudioEngine
forall a. HasCallStack => [Char] -> a
error [Char]
"PortMidi backend not yet implemented"

{- | Release the resources held by an 'AudioEngine'. Prefer 'withAudioEngine'
over calling this directly so shutdown still happens on exception.
-}
stopAudioEngine :: AudioEngine -> IO ()
stopAudioEngine :: AudioEngine -> IO ()
stopAudioEngine AudioEngine
engine =
    case AudioEngine -> BackendHandle
aeBackend AudioEngine
engine of
        FluidSynthHandle FluidSynthEngine
fs -> FluidSynthEngine -> IO ()
FS.shutdownFluidSynth FluidSynthEngine
fs

{- | Start a backend, run an action with it, and shut it down afterwards
even if the action throws (via @bracket@).
-}
withAudioEngine :: BackendType -> AudioConfig -> (AudioEngine -> IO a) -> IO a
withAudioEngine :: forall a.
BackendType -> AudioConfig -> (AudioEngine -> IO a) -> IO a
withAudioEngine BackendType
backend AudioConfig
cfg =
    IO AudioEngine
-> (AudioEngine -> IO ()) -> (AudioEngine -> IO a) -> IO a
forall a b c. IO a -> (a -> IO b) -> (a -> IO c) -> IO c
bracket (BackendType -> AudioConfig -> IO AudioEngine
startAudioEngine BackendType
backend AudioConfig
cfg) AudioEngine -> IO ()
stopAudioEngine

-- | Send a note-on directly to the audio backend (synchronous).
engineNoteOn :: AudioEngine -> MidiChannel -> Pitch -> Velocity -> IO ()
engineNoteOn :: AudioEngine -> MidiChannel -> Pitch -> Velocity -> IO ()
engineNoteOn AudioEngine
engine = case AudioEngine -> BackendHandle
aeBackend AudioEngine
engine of
    FluidSynthHandle FluidSynthEngine
fs -> FluidSynthEngine -> MidiChannel -> Pitch -> Velocity -> IO ()
FS.noteOn FluidSynthEngine
fs

-- | Send a note-off directly to the audio backend (synchronous).
engineNoteOff :: AudioEngine -> MidiChannel -> Pitch -> IO ()
engineNoteOff :: AudioEngine -> MidiChannel -> Pitch -> IO ()
engineNoteOff AudioEngine
engine = case AudioEngine -> BackendHandle
aeBackend AudioEngine
engine of
    FluidSynthHandle FluidSynthEngine
fs -> FluidSynthEngine -> MidiChannel -> Pitch -> IO ()
FS.noteOff FluidSynthEngine
fs

-- | Send a program change directly to the audio backend (synchronous).
engineProgramChange :: AudioEngine -> MidiChannel -> MidiProgram -> IO ()
engineProgramChange :: AudioEngine -> MidiChannel -> MidiProgram -> IO ()
engineProgramChange AudioEngine
engine = case AudioEngine -> BackendHandle
aeBackend AudioEngine
engine of
    FluidSynthHandle FluidSynthEngine
fs -> FluidSynthEngine -> MidiChannel -> MidiProgram -> IO ()
FS.programChange FluidSynthEngine
fs