-- 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 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 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 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 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) forall a b. (a -> b) -> a -> b $ \Handle hdl -> Handle -> ByteString -> IO () ByteString.hPut Handle hdl 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) = forall b a. Monoid b => (a -> b) -> [a] -> b mconcatMap (forall a. Semigroup a => a -> a -> a <>ByteString "\0") (forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b] concatMap Play -> [ByteString] serialize1 [Play] plays) forall a. Semigroup a => a -> a -> a <> ByteString "\n" where serialize1 :: Play -> [ByteString] serialize1 (Play String sample Int offset Double ratio Double volume) = forall a b. (a -> b) -> [a] -> [b] map String -> ByteString Char8.pack [String sample, forall a. Show a => a -> String show Int offset, forall a. Show a => a -> String show Double ratio, 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. -}