14

我正在尝试在 Haskell 中编写一个交互式的实时音频合成器,我迫切需要“惰性数字”来表示时间。

事情是这样的:我的程序基于“信号”的概念,这些信号由“信号处理器”转换。但与 Faust 或 ChuckK 等其他类似项目不同,我希望使用严格的纯函数,但又能明确访问时间。

这个想法是可以在 Haksell 中表达纯粹的“惰性流处理器”,并且由于惰性评估,这将以交互式、实时的方式工作。

例如,我可以将“midi 信号”表示为音符变化事件流:

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

这一切在非交互模式下运行得很好,但是当我想实时玩它时,我遇到了一个很大的障碍:在任何一个时间点,输出信号都取决于下一个输入的时间事件。所以我的合成引擎实际上会停止,直到下一个事件。

让我解释一下:当我的声卡要求我的输出信号样本时,惰性求值器遍历我的信号处理器的依赖关系图并最终要求输入(midi)信号的一部分。但是假设输入信号在本地看起来像这样:

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

当我需要在时间 1.5 计算输出(音频)信号时,我需要这样的东西:

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

...当我评估“notesAt input 1.5”时,它必须在返回之前计算“2 > 1.5”。但是事件(2,NoteOff 42)不会再发生 0.5 秒!所以我的输出取决于将来会发生并因此停止的输入事件。

我称这种效应为“矛盾的因果关系”。

我已经考虑如何处理这个问题已经有一段时间了,我得出的结论是,我需要某种形式的数字,让我能够懒惰地评估“a > b”。比方说:

bar :: LazyNumber
bar = 1 + bar

foo :: Bool
foo = bar > 100

...然后我希望“foo”评估为 True。

请注意,您可以为此使用 Peano 数字,它确实有效。

但为了提高效率,我想将我的数字表示为:

data LazyNumber = MoreThan Double | Exactly Double

...并且这需要可变才能工作,即使 LazyNumbers 上的每个函数(例如“>”)都是纯的...

在这一点上,我有点迷路了。所以问题是:是否有可能在交互式实时应用程序中实现有效的惰性数字来表示时间?

编辑

有人指出,我正在做的事情有一个名字:Functional Reactive Programming。Edward Amsden 的论文“A Survey of Functional Reactive Programming”是一个很好的介绍。这是一个摘录:

大多数 FRP 实现,包括迄今为止的所有信号函数实现,都屈服于对事件不发生的持续重新评估,这是由于系统不断重新采样 FRP 表达式以进行输出的“基于拉动”的实现。Reactive 的工作(第 3.1 和 4.4 节)旨在解决 Classic FRP 的这个问题,但尚未探索将这项工作扩展到信号函数,并且发生时间比较的简单操作依赖于程序员检查并且可以说难以证明身份以保持参考透明度。

这似乎是问题的核心:我的“虚拟事件”方法和 DarkOtter 的提议属于“对事件不发生的持续重新评估”类别。

作为一个天真的程序员,我说“让我们使用惰性数字,让 foo/bar 示例工作”。/我挥手。同时,我会看看YampaSynth。

此外,在我看来,像我试图做的那样,使数字相对于线性时间变得“懒惰”与让(实数)数字在精度方面“懒惰”密切相关(参见Exact Real Arithmetic)。我的意思是,我们希望在严格的纯上下文中使用可变对象(事件时间的下限与实数的间隔),满足某些法律以确保我们“保持引用透明度”。更多的挥手,对不起。

4

2 回答 2

5

你可以做一些这样的事情来实现(大致)最大延迟,我认为它可能已经在一些 FRP 程序中完成。我认为这个想法与您的建议类似,类型如下:

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

您可以为此定义各种方便的实例,例如comonad,但您所说的关键是您必须有一些方法,当任何IO进程正在等待下一个midi事件发生时,它立即产生你的一对,与时间和事件的懒惰承诺。该事件仍将仅在实际事件发生时才可用,但应该捏造时间,以便在某个最大延迟后它的一部分始终可用。即,它等待 100 毫秒,然后如果事件发生,则惰性 thunk 变为 (Greater 100ms (thunk)),其中下一个 thunk 以相同的方式运行。这使您可以根据需要懒惰地交错。

我已经看到在旧版本的 FRP 库中使用 MVars 和 unsafeDupablePerformIO 的组合完成了类似的操作。这个想法是,您有一个 MVar,您的 IO monad 等待线程将其推入以发出该值的信号,并且您放入的 thunk 使用 unsafeDupablePerformIO 从 MVar 读取(这应该是线程安全的,并且是幂等的,所以它应该是安全的做我认为)。

然后,如果等待线程认为它太长,您只需为下一位创建另一个 MVar 和伴随的 thunk,然后将您的 (Greater (100ms) (thunk)) 值推入旧的,这允许在惰性中进行评估部分继续。

这并不完美,但这应该意味着你只需要等待,比如未来 100 毫秒而不是 500 毫秒。

如果您不想弄乱时间表示,我想您总是可以将midi事件流设为(时间,Maybe事件)流,然后确保生成的任何东西至少每x插入一次事件小姐。

编辑:

我在这里做了一个简单的例子:https ://gist.github.com/4359477

于 2012-12-22T14:46:00.827 回答
3

使用pipes,唯一可以让您对更多输入的请求进行参数化的流媒体库。您将惰性笔记流构建为服务器:

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

好了,你完成了!要了解有关此技巧的更多信息,请阅读Control.Proxy.Tutorialpipes中的教程。

加分项:你不需要使用unsafePerformIO,但你仍然保持组合编程。例如,如果您想记前 10 个音符,那么您只需:

takeB_ 10 <-< notes

如果您想在给定的截止日期之前记下所有笔记,您只需执行以下操作:

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

通常当人们说他们想要纯度时,他们真正的意思是他们想要组合性。

于 2012-12-23T03:16:03.013 回答