-- Copyright 2018 Evan Laforge
-- This program is distributed under the terms of the GNU General Public
-- License 3.0, see COPYING or http://www.gnu.org/licenses/gpl-3.0.txt

-- | This has the protocol to talk to a running play_cache and tell it to play
-- samples in realtime.  This is for the audio preview aka "MIDI thru" feature
-- for im.  Since each im patch may respond in its own way to a Note, this
-- relies on the patch itself exporting a 'ThruFunction' to find the
-- appropriate sample.
--
-- This used to use OSC, but it turned out OSC wasn't really getting me
-- anything, and its restrictions were troublesome, so now it's a custom
-- format as emitted by 'serialize'.
module Synth.Shared.Thru (
    ThruFunction, Note(..)
    , Message(..), Play(..)
    , send
) where
import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Char8 as Char8

import qualified Util.Network as Network
import qualified Derive.Attrs as Attrs
import qualified Perform.Pitch as Pitch
import qualified Synth.Shared.Config as Config

import           Global


-- | This is a specialized version of 'Cmd.Cmd.ThruFunction'.  Being more
-- specialized means I don't have to directly depend on "Cmd.Cmd" from here.
type ThruFunction = [Note] -> Either Error Message

data Message = Plays [Play] | Stop
    deriving (Int -> Message -> ShowS
[Message] -> ShowS
Message -> String
(Int -> Message -> ShowS)
-> (Message -> String) -> ([Message] -> ShowS) -> Show Message
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [Message] -> ShowS
$cshowList :: [Message] -> ShowS
show :: Message -> String
$cshow :: Message -> String
showsPrec :: Int -> Message -> ShowS
$cshowsPrec :: Int -> Message -> ShowS
Show)

data Note = Note {
    Note -> NoteNumber
_pitch :: !Pitch.NoteNumber
    , Note -> Double
_velocity :: !Double
    , Note -> Attributes
_attributes :: !Attrs.Attributes
    , Note -> Int
_startOffset :: !Int
    } deriving (Int -> Note -> ShowS
[Note] -> ShowS
Note -> String
(Int -> Note -> ShowS)
-> (Note -> String) -> ([Note] -> ShowS) -> Show Note
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [Note] -> ShowS
$cshowList :: [Note] -> ShowS
show :: Note -> String
$cshow :: Note -> String
showsPrec :: Int -> Note -> ShowS
$cshowsPrec :: Int -> Note -> ShowS
Show)

type Error = Text
type Frames = Int

data Play = Play {
    Play -> String
_sample :: !FilePath
    , Play -> Int
_offset :: !Frames
    , Play -> Double
_ratio :: !Double
    , Play -> Double
_volume :: !Double
    } deriving (Play -> Play -> Bool
(Play -> Play -> Bool) -> (Play -> Play -> Bool) -> Eq Play
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: Play -> Play -> Bool
$c/= :: Play -> Play -> Bool
== :: Play -> Play -> Bool
$c== :: Play -> Play -> Bool
Eq, Int -> Play -> ShowS
[Play] -> ShowS
Play -> String
(Int -> Play -> ShowS)
-> (Play -> String) -> ([Play] -> ShowS) -> Show Play
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [Play] -> ShowS
$cshowList :: [Play] -> ShowS
show :: Play -> String
$cshow :: Play -> String
showsPrec :: Int -> Play -> ShowS
$cshowsPrec :: Int -> Play -> ShowS
Show)

send :: Message -> IO ()
send :: Message -> IO ()
send Message
msg = Addr -> (Handle -> IO ()) -> IO ()
Network.withHandle_ (PortNumber -> Addr
Network.TCP PortNumber
Config.thruPort) ((Handle -> IO ()) -> IO ()) -> (Handle -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \Handle
hdl ->
    Handle -> ByteString -> IO ()
ByteString.hPut Handle
hdl (ByteString -> IO ()) -> ByteString -> IO ()
forall a b. (a -> b) -> a -> b
$ Message -> ByteString
serialize Message
msg

-- | This serializes to a protocol with null-terminated fields, where a message
-- is terminated with '\n'.  That makes it easy to read with getline(), and
-- easy to parse each field as a string.
serialize :: Message -> Char8.ByteString
serialize :: Message -> ByteString
serialize (Plays [Play]
plays) =
    (ByteString -> ByteString) -> [ByteString] -> ByteString
forall b a. Monoid b => (a -> b) -> [a] -> b
mconcatMap (ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<>ByteString
"\0") ((Play -> [ByteString]) -> [Play] -> [ByteString]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap Play -> [ByteString]
serialize1 [Play]
plays) ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString
"\n"
    where
    serialize1 :: Play -> [ByteString]
serialize1 (Play String
sample Int
offset Double
ratio Double
volume) =
        (String -> ByteString) -> [String] -> [ByteString]
forall a b. (a -> b) -> [a] -> [b]
map String -> ByteString
Char8.pack [String
sample, Int -> String
forall a. Show a => a -> String
show Int
offset, Double -> String
forall a. Show a => a -> String
show Double
ratio, Double -> String
forall a. Show a => a -> String
show Double
volume]
serialize Message
Stop = ByteString
"stop\n"


{- NOTE [realtime-im]

    Original design for realtime im / im preview.  I wound up doing "hybrid im
    and MIDI", and using OSC for the protocol to talk to play_cache.
    [ And now I'm no longer using OSC, but a a simple custom message format. ]

    . Get the Sampler.Patch, run its convert function, then send it to
      play_cache.  play_cache only needs use filename, ratio, vol.
      . I can use OSC: /start/path/to/sample <ratio> <vol>
        Abort a sample on space: /stop
      . The advantage is that I can use liblo for C++, and some haskell
        library for karya.
      Implement with ignoring ratio, just stream the sample.
        . I should be able to reuse Streamer.
        . Start a separate thread, which listens on an OSC socket, creates
          a Streamer, and has it start streaming.
        . The main process() also mixes in any output from the realtime
          Streamer.
        . Since it's realtime, I don't want to keep track of debt in read(),
          I just want to get samples as soon as they arrive.
        . Also I play a file, not a directory, and I need to apply volume and
          ratio.
        * plug into PlayCache
      * I need a "MIDI" thru which takes Input.Note, runs the patch convert,
        and sends OSC.
      * Add support for ratio.  libsamplerate with a cheap quality should be
        able to do realtime.
        . I need to do the resampling in the streamer thread.
        . Rename Sample to Audio or Stream
        . I think I can change Streamer to take an abstract Audio in start(),
          then make Mix into an Audio.  Then I can get rid of synchronized.
        . Mix becomes Tracks, since it mixes tracks, and deals with the
          directory structure.
        . Now make a Resample : Audio, and
          . Osc: Resample(ratio, SampleFile(fname))
          . PlayCache: Tracks(dir, mutes), Track hardcodes SampleDirectory
        . But I still need to call Streamer::start with no allocation.  How it
          does that currently is dir and mutes are statically allocated and
          just assigned on start().  The the actual initialization is delayed
          until streamLoop(), which is non-realtime.  The pattern is pass
          constructor and arguments, and call it in streamLoop.
        . I could emulate that with a separate initialiaze method.  I can't
          think of how to get it to work though.
        . Alternately, use Streamer subclasses.
    overview:
      . Im instruments should have a midi thru-like preview sound.
      . This doesn't have to be performance-level of low latency,
        probably < 100ms will do.  Still, the lower the better.
      ? Experiment with how various latencies feel, so I know my target.
      . If I do hybrid im and MIDI: below then I need to make play_cache into a
        little sampler, which just means streaming through libsamplerate.
    possible designs:
      parallel MIDI:
        . Assign a parallel MIDI instrument for thru.
        . All I get is the right pitch and possibly similar instrument sound.
        . Probably in practice I'd just have a single MIDI instrument with
          a neutral sound.
      hybrid im and MIDI:
        . A compromise would be a general mechanism to render samples, and
          then feed that into a realtime sampler.  Of course commercial
          samplers can do that but would insert a lot of manual work into
          the process.  So maybe I can write a minimal MIDI VST sampler.
        . All it needs to do is play a sample with a given pitch (pitch bend
          and pitch) and velocity.
        . For this, I need to prepare each instrument by rendering a set of
          example samples.
        . I don't want to have to encode notes as MIDI, so just have
          play_cache open a socket, which takes a filename and pitch and
          immediately plays whatever it gets.  All I have to do is teach
          play_cache to resample and maybe scale volume.
        . Maybe I just run the convert function, and then take only the
          Sample.filename, and send it to play_cache.
      pure im:
        . I have to give a single Note to the im synthesizer, which then has
          to stream to the VST, which has a socket to immediately play
          samples.
        . I might have to run im synthesizers persistently to avoid startup
          overhead.
        . Presumably I also have to ensure they use a sufficiently small
          block size to get that first block out quickly.  Since I'm not
          doing realtime control, I don't actually care about the rest of
          the blocks.
        . This is more complicated, but is accurate for the instrument:
          I get the right sounds so I can e.g. see how sampler-im picks
          samples in realtime-ish.  However, if I go this far, it might not
          be that much more work to go for the full realtime interface.
      realtime interface:
        . This is basically try to turn im into a realtime interface.
        . The difference with the previous is that I'd also support realtime
          control signals.
        . The reason to want this is that I could explore instruments
          interactively.
        . I would probably have to take MIDI or OSC, run the im synth
          persistently, and either stream to the VST or directly to audio
          out.
        . I'd have to plan out a general purpose interface.  Synthesizers
          that use knowledge of the future would have to be adapted to not
          having that.
-}