14

I am trying to write an interactive, realtime audio-synthesis thing in Haskell, and I'm in dire need of "lazy numbers" to represent time.

Here's the thing: my program is based on a notion of "signals" and these signals are transformed by "signal processors". But unlike other similar projects like Faust or ChucK, I would like to work with strictly pure functions and yet have explicit access to time.

The idea is that it's possible to express pure "lazy stream processors" in Haksell and because of lazy evaluation, that will work in an interactive, real-time fashion.

For example, I could represent a "midi signal" as a stream of note-changing events:

type Signal = [ (Time, Notes->Notes) ]

It all works very well in non-interactive mode, but when I want to actually play with it in real-time, I hit a big roadblock: at any one point in time, the output signal is dependent on the time of the next input event. So my synthesis engine actually stops until the next event.

Let me explain: when my soundcard asks for a sample of my output signal, the lazy evaluator walks the dependency graph of my signal processors and eventually asks for a piece of the input (midi) signal. But let's say the input signal looks locally like this:

input :: Signal
input = [ ..., (1, noteOn 42), (2, noteOff 42), ... ]

When I need to compute the output (audio) signal at time 1.5, I will need something like this:

notesAt :: Signal -> Time -> Notes
notesAt = notesAt' noNotes where
    notesAt' n ((st,sf):ss) t
            | st > t = n
            | otherwise = notesAt' (sf n) ss t

... and when I evaluate "notesAt input 1.5", it will have to compute "2 > 1.5" before returning. But the event (2, NoteOff 42) won't happen for another 0.5 seconds! So my output is dependent on an input event that will happen in the future and thus stops.

I call this effect "paradoxical causality".

I have thought about how to handle this for quite some time, and I have come to the conclusion that what I need is some form of numbers that will allow me to evaluate "a > b" lazily. Let's say:

bar :: LazyNumber
bar = 1 + bar

foo :: Bool
foo = bar > 100

... then I would like "foo" to evaluate to True.

Note that you can use Peano numbers for that and it actually works.

But in order to be efficient, I would like to represent my numbers like:

data LazyNumber = MoreThan Double | Exactly Double

... and this needs to be mutable to work, even though every function on the LazyNumbers (e.g. ">") will be pure...

At this point, I'm kind of lost. So the question is: Is it possible to implement efficient lazy numbers to represent time in interactive real-time applications?

EDIT

It has been pointed out that what I'm doing has a name: Functional Reactive Programming. The paper "A Survey of Functional Reactive Programming" by Edward Amsden is a good introduction. Here is an extract:

Most FRP implementations, including all signal function implementations to date, succumb to continuous re-evaluation of event non-occurrences due to a "pull-based" implementation where a system continuously resamples the FRP expression for output. The work on Reactive (Sections 3.1 and 4.4) purports to solve this problem for Classic FRP, but extending this work to signal functions has not yet been explored, and the simple operation of occurrence time comparison relies on a programmer-checked and arguably difficult to prove identity to retain referential transparency.

It seems this is the heart of the problem: my "dummy events" approach and DarkOtter's proposal fall in the "continuous re-evaluation of event non-occurrences" category.

Being a naïve programmer, I say "let's use lazy numbers, let's make the foo/bar example work". /me waves hands. Meanwhile, I'll take a look at YampaSynth.

Also, it seems to me that making numbers "lazy" with respect to linear time like I'm trying to do is closely related to making (real) numbers "lazy" with respect to precision (c.f. Exact Real Arithmetic). I mean, we want to use mutable objects (a lower bound for event-time vs. an interval for reals) from a strictly pure context, given certain laws to be satisfied to make sure that we "retain referential transparency". More handwaving, sorry.

  • 5
    Are you aware of [functional reactive programming](http://www.haskell.org/haskellwiki/Functional_Reactive_Programming)? (Great question, regardless of that – such lazy numbers could be useful in plenty of applications) – leftaroundabout Dec 22 '12 at 13:21
  • I tried to do something like this, including possibly delaying signals, as an Arrow. It didn't work very well because signals could overtake each other in wierd ways. This is where I got to: http://hackage.haskell.org/package/Dflow – Paul Johnson Dec 22 '12 at 20:53
  • No, I am not familiar with functional reactive programming‌​. I'll have a look at it tomorrow (as well as DarkOtter's and Paul Johnson's code) and see if that sheds some light on my problem. – Mikael Bouillot Dec 23 '12 at 01:47
  • What if you added another lazy list, one representing all the times "when my soundcard asks for a sample of my output signal." Then rather than having an IO action that simply returned a `Signal` representing the users inputs, how about one that took the soundcard request times, `ts :: [Time]` and returned `sigs :: [Signal]`, chunking the signal so that `all id $ zipWith (\t -> all (( – rampion Dec 23 '12 at 04:02
  • This would make the model quite complex. One reason I'm doing this in Haskell and not C is that I want the model to be as simple as possible: no explicit handling of "signal chunks" should be necessary. As I commented on DarkOtter's answer, I have made it work by inserting lots of regularly sampled dummy events in my input stream, but I would like not to have to do that and *still* get the right interactive behaviour. – Mikael Bouillot Dec 23 '12 at 09:09
  • If you're inserting dummy events already, why not make them represent the soundcard sample points? `data Event = Request | Update (Notes -> Notes) ; type Signal = [(Time, Event)]`. Then you get good time resolution without a lot of dummy events. – rampion Dec 23 '12 at 15:21
  • Related: https://stackoverflow.com/questions/7267760 . – atravers Jan 13 '22 at 01:59

2 Answers2

5

You could do something a bit like this to achieve (roughly) a maximum latency, and I think it may have already been done in some FRP programs. I think this idea would be similar to what you suggested, having a type like:

data Improving n = Greater n (Improving n) | Exact n

You can define all sorts of handy instances for this, like comonad, but the key bit for what you're saying is that you would have to have some method whereby, while whatever IO process is waiting for the next midi event to occur, it immediately yields your pair, with the lazy promises of the time and event. The event will still only become available when the actual event occurs, but the time should be fudged so that a part of it will always become available after some maximum latency. I.e., it waits for say 100ms, and then if the event has occured, the lazy thunk becomes (Greater 100ms (thunk)) where the next thunk then operates in the same fashion. This allows you to lazily interleave things as you wanted.

I have seen something similar done in an older version of an FRP library, using a combination of MVars and unsafeDupablePerformIO. The idea is, that you have an MVar which your IO monad waiting thread pushes into to signal the value, and the thunk you put in uses unsafeDupablePerformIO to read from the MVar (which should be thread safe, and idempotent, so it should be safe to do I think).

Then, if the waiting thread thinks it's too long, you simply create another MVar and accompanying thunk for the next bit, and then push into the old one your (Greater (100ms) (thunk)) value, which allows the evaluation in the lazy part to continue.

It's not perfect, but it should mean that you would only have to wait, say 100ms into the future rather than 500ms.

If you don't want to mess around with time representations, I suppose you could always just make the stream of midi events a stream of (time, Maybe event), and then just ensure that whatever is generating inserts events at least once every x ms.

Edit:

I made a simple example of this approach here: https://gist.github.com/4359477

  • I didn't check other laws, but the `Functor` instance in your example doesn't satisfy `fmap id = id`. – Vitus Dec 22 '12 at 16:48
  • Depends how strong you're trying to make it. It doesn't preserve the extra bounds, but it does preserve the value contained. I wasn't too worried because it satisfies the property that replacing fmap id with id will still give an answer containing the same value, just with better estimates, and there didn't seem to be any other way to have a functor instance other than discarding the estimated bounds. So for an example, I wasn't too worried. For a proper library, you could either try and find a way to change it, or just discard those instances. –  Dec 22 '12 at 23:07
  • I had tried something similar (I think) to what you're proposing: In my "lazy midi stream" IO function, instead of generating one time-stamped stream element by midi event, I would generate fixed-rate (i.e. every 3 ms) "dummy" events. In effect, I would replace `[(1,noteOn 42),(2,noteOff 42)]` by `[(1,noteOn 42),(1.003,id),...,(1.997,id),(2,noteOff 42)]`. That worked but then, considering that I wanted to work with one input signal per note (i.e. 128 parallel input signals, mostly "empty" of any events at all), that approach seemed incredibly wastefull to me... – Mikael Bouillot Dec 23 '12 at 01:31
  • Well, you'll have the same issue with the approximating time values described in this answer - you're effectively building up a similar list of estimates in the time for the next event instead of the list of dummy events. I would be inclined to say the latter is better, as if you are consuming these streams then it is probably easier to garbage collect the dummy items from the stream, rather than the structure of the increasing value. I think there will have to be some element of extra garbage like this though, as we really shouldn't/can't have a pure value change at any point to remove it. –  Dec 23 '12 at 01:50
  • Alright, I won't pretend I understand your code (comonads and GADTs are beyond me at the moment) but since you kindly provided a library, I tried it out with an example and could not get it to work. In [this example code](http://www.corbac.com/Data/Sources/Haskell/lazy_numbers_1.hs), I expected `foo (bar1::Increasing Integer)` to evaluate to True, but it doesn't. What am I doing wrong? – Mikael Bouillot Dec 26 '12 at 11:08
  • Woah, woah, woah - I didn't provide a library. That was just example code, I would strongly suggest _not_ using it in any production code, or anything else. The reason that it doesn't work in this case is that it can't provide any bounds. When you call 1 + bar1, the plus function doesn't know if bar1 >= 1, so it can't lower bound the value of 1 + bar1. This means that bar1 has no lower bound, and so can't be used lazily. Use bar1 = 1 + (positive bar1), where positive n = Bounded 0 n. This should work. You also need to test 100 < bar1, as it is the left hand side that is evaluated first. –  Dec 26 '12 at 11:53
  • I made a version with simpler data structures, and more complex evaluation model. bar1 = 1 + abs bar1 should work now, under both 100 < bar1 and bar1 > 100. Again though, please don't trust my code for anything important, I am really just a Haskell novice still, and I don't know much about the best way to do things. –  Dec 26 '12 at 12:36
3

Use pipes, the only streaming library that lets you parametrize requests for more input. You structure your lazy stream of notes as a server:

notes :: (Proxy p) => MaxTime -> Server p MaxTime (Maybe Note) IO r
notes = runIdentityK $ foreverK $ \maxTime -> do
    -- time out an operation waiting for the next note
    -- deadline is the maxTime parameter we just received
    t <- lift $ getCPUTime
    note <- lift $ timeout (maxTime - t) $ getNote
    respond note

There, you're done! To learn more about this trick, read the pipes tutorial at Control.Proxy.Tutorial.

Bonus points: You don't need to use unsafePerformIO, yet you still keep compositional programming. For example, if you want to take the first 10 note, then you just do:

takeB_ 10 <-< notes

If you want to then take all notes up to a given deadline, you would just do:

query deadline = takeWhileD isJust <-< mapU (\() -> deadline) <-< notes

Usually when people say they want purity, what they really mean is they want compositionality.

Gabriella Gonzalez
  • 34,863
  • 3
  • 77
  • 135
  • Well I don't know about "people", but when *I* say purity, I mean "nice, friendly, naive-looking functional code". For example, implementing the Unix "grep" command as a function `String -> String` because it's simple and (thanks to Haskell's "magic") it works. As of now, my model based on pure lazy signals feels very simple **and** it works very well apart from that realtime dependency problem. It feels to me that going the way you suggest would amount to losing most of that simplicity. In any case, I know *I* would have much less trouble doing the thing in C. – Mikael Bouillot Dec 23 '12 at 09:45
  • @MikaelBouillot Alright. Even though I don't recommend it, then the solution to your problem is a lazy list of impure functions of type `notes :: [MaxTime -> Maybe Note]`, where each those functions uses `unsafePerformIO` to compare the given `MaxTime` to the current time and waits for a note with a timeout. Then if you wanted to query the list up to a given time, you would just do `query deadline = catMaybes . takeWhile isJust . map ($ deadline) $ notes`. However, the equivalent pipe in my answer is just as expressive. – Gabriella Gonzalez Dec 23 '12 at 16:20
  • I'm inclined to recommend using pipes or something similar here as well, as I think itm ight deal with the monadic code better in the long run, rather than hiding it. Plus I suspect that this might be better performing than creating massive lazy structures as in my answer. –  Dec 26 '12 at 11:56