【问题标题】:Audio synthesis in Haskell using reactive-banana在 Haskell 中使用响应式香蕉进行音频合成
【发布时间】:2014-02-15 11:28:30
【问题描述】:

我正在尝试开始使用响应式香蕉,并想创建一个简单的合成器。有很多 GUI 示例,但我无法将它们应用于音频。由于音频 API 有回调说“给我 n 个音频样本”,我想我应该触发一个事件,每个回调(使用 newAddHandler 返回的 snd 部分)包含要生成的样本数量,应该写入的指针, 和时间信息来协调 MIDI 事件。传递给 reactimate 的 IO 操作会将样本写入指针。 MIDI 事件将类似地从另一个回调中触发,并且还包含时间信息。

然而,这就是我卡住的地方。我猜音频信号应该是一种行为,但是我如何在适当的时间内“运行”一种行为来获取样本?正确的数量当然取决于两个音频回调之间可能发生的 MIDI 事件。

【问题讨论】:

标签: haskell audio midi reactive-banana


【解决方案1】:

假设意图是实时做某事,我认为为每个回调触发一个事件将是极其有限的。大多数音频 API 期望这些回调将很快返回(例如,通常您永远不会调用 malloc 或在一个中执行阻塞 IO)。触发 FRP 事件可能适用于非常简单的处理,但我认为如果您尝试执行更复杂的操作,您会在音频流中丢失。

我希望一种更可行的方法是自己触发事件(通过时钟,或响应 GUI 事件等)并生成音频缓冲区,并从该缓冲区读取回调 API。我知道一些音频 API(例如 portaudio)具有缓冲模式,可以自动处理其中的一些。虽然如果你只有一个回调 API,那么在上面添加一个缓冲区并不难。

【讨论】:

  • 谢谢,可能回调中触发的事件应该意味着“填充下一个缓冲区”而不是当前缓冲区,然后可以将上一个回调填充的缓冲区交给API以避免停滞(响应系统时钟触发事件可能会导致漂移并最终退出)。无论事件在哪里触发,最大的问题是我的问题第二段中描述的反应香蕉部分。
  • @absence 我怀疑你会想要一个像Behavior Builder 这样的类型(使用例如来自最新bytestring 的Builder)。您可以累积事件来创建该行为,添加音频输入并刷新它的输出回调。
  • 使用 Builder 可以让我使用反应香蕉来计算样本吗?我想要包络、振荡器、滤波器等的行为(它们基本上都是 Time -> Double),但我仍然不明白如何从中制作样本。
  • 您需要一个时钟来驱动网络(即Event Time 类型的东西)。您设置采样率(例如 44100),然后您可以计算采样之间的时间,这是您的时钟事件需要触发的频率(使用 yield 循环或类似的方法进行安排)。然后使用oscil <@> clockE 获取输出。一个复杂的问题是,对于实时音频,如果您的时钟在每个样本上运行,您将无法进行大量处理。通常每个时钟事件都会触发一个音频数据块,64 或 128 个样本,所以Behavior (Time -> Vector Double)
【解决方案2】:

要解决此类问题,我发现采取语义观点很有用:什么是音频信号?我可以用什么类型来表示它?

本质上,音频信号是一个随时间变化的幅度

Audio = Time -> Double

这表明表示是一种行为

type Audio = Behavior Double

然后,我们可以使用<@> 组合子查询特定时刻的振幅,即每当事件发生时。

但是,出于效率的原因,音频数据通常存储在 64 字节(或 128、256)的中。毕竟,处理需要快速,并且使用紧密的内部循环很重要。这建议将音频数据建模为一种行为

type Audio = Behavior (Vector Double)

它的值是 64 字节的音频数据块,每当对应于 64 字节的时间段结束时,它就会发生变化。

只有在语义模型明确后才能连接到其他 API。在这种情况下,将行为中的音频数据写入缓冲区似乎是个好主意,每当外部 API 调用您的回调时,缓冲区的内容就会显示出来。


顺便说一句,我不知道 reactive-banana-0.8 是否足够快,还不能用于样本级音频处理。应该不会太糟糕,但您可能必须选择一个相当大的块大小。

【讨论】:

  • 忽略性能并假设 Behavior Double,您和 John L 所说的是时钟机制为每个样本触发一个事件(例如每秒 44100 次)?
  • 拥有一个每秒触发 X 次的时钟事件是构建 Behaviors 每秒每 X 次变化的好方法。
  • 太棒了!我开始看到这些部分是如何组合在一起的,但我似乎仍然缺少一些重要的东西。如何制作时钟事件?查看我得到的最接近的可用函数是使用复制来获取一个 [()],我可以在“生成 n 个样本”事件上溢出、应用、收集和最后 fmap,但我猜一个缓冲区中的所有事件都会共享相同的时间戳,即“生成 n 个样本”事件发生时的测量时间,而不是 t/sampleRate,其中 t = [0..]。
  • 需要导入外部时钟机制。大多数 UI 编程框架(如 SDL、wxWidgets 等)都提供计时器对象。 (您也可以使用forkIOthreadDelay 滚动您的。)使用fromAddHandler 将其转换为Event
  • 是的。关键是reactive-banana 没有提供默认计时器实现,因为不同的用例需要不同的精度等。您必须自己创建一个计时器,这意味着通过fromAddHandler 创建一个Event Time。这可以比实时更快,甚至,没有什么能阻止实现一个计算逻辑时间的“时钟”,但它会尽可能快地触发。
猜你喜欢
  • 1970-01-01
  • 2015-01-28
  • 1970-01-01
  • 1970-01-01
  • 2011-09-25
  • 1970-01-01
  • 2017-11-22
  • 1970-01-01
  • 2011-10-12
相关资源
最近更新 更多