Pyo文档(一)

2018-12-28 周五 by Olivier Bélanger. 黄复雄.

作者是Olivier Bélanger,自ajaxsoundstudio.com; 黄复雄编译,有意译和少量省略。

本文对应于原文档的Parts of the documentation部分,但不包括其中的API documentation部分;API documentation将以专文处理。

关于Pyo

Pyo是用C写成的Python模块,用于生成数字信号处理(DSP)脚本。它提供一整套的类,用于构建音频软件,编写算法音乐,或通过一种简单、成熟、强大的编程语言随意探索音频处理。

它包含众多的声音信号处理类。用户可以用它在Python脚本、工程中直接加入信号处理链,并可通过解释器实时操控处理链。Pyo模块中的工具提供最基本的东西,如音频信号的数学运算,基本的信号处理(滤波器、延迟器、合称发生器,等等),也提供复杂算法以生成音频颗粒(sound granulation),以及别的生成音频的操作。Pyo支持开放声音控制协议(OSC, Open Sound Control),以便软件间的通信;支持MIDI协议,以生成声音事件,控制参数的处理。Pyo能够利用一种成熟、广泛使用的通用处理语言的诸多优势,生成复杂的信号处理链条。

Pyo的开发者是Olivier Bélanger <belangeo@gmail.com>。

要提问与评论,请订阅pyo-discuss的邮件列表。

报告错误或请求新特性,请使用github库的issues tracker

源文件和二进制文件可在这里下载:http://ajaxsoundstudio.com/software/pyo/

下载安装文件

安装文件可用于Windows(XP/Vista/7/8/10)和MacOS(from 10.6 to 10.12)。

要下载最新的Pyo编译版,请到Pyo的页面

在Debian和Fedora发布版中,你可以从包管理器中得到Pyo。Pyo库名是python-pyopython2-pyo,对应Python 2.7;或是python3-pyo,对应Python 3.5+。

如果你用的是Arch linux,包名是python2-pyo,还没有对应Python 3.5+的包。

安装文件的内容

安装文件将向系统安装两个不同的软件。其一是安装Pyo模块(包含单精度/32位和双精度/64位两套编译),及其在当前Python发布版下依赖。其二是安装E-Pyo,一个简单的文本编辑器,做了特意调校,以便于编辑、运行音频Python脚本。

Pyo是一个Python模块...

这意味着系统里先要有Python(2.7, 3.5或3.6(推荐))。如果没有安装Python,可以到这里下载python.org

Pyo也提供一些GUI工具,以控制音频处理,或使其视觉化。如果想使用所有Pyo的GUI特性,你必须安装WxPython 3.0 (classic版对应python 2.7,phoenix版对应python 3.5+),这里能得到:wxpython.org

库的结构

下图表示库的内部结构。

结构

起步

启动服务器,输出声音

生成并引导Pyo服务器(可配置采样率、声道数量等)。启动Pyo服务器,即开启音频和MIDI接口,以备接收其他Pyo对象生成的音频和MIDI。

>>> from pyo import *
>>> s = Server().boot()
>>> s.start()

生成并输出音频。下例是生成正弦振荡器对象,并输出到服务器。这时你就能听到声音了。

>>> a = Sine(mul=0.01).out()

停止服务器:

>>> s.stop()

服务器的图形界面

如果不是在交互解释器中执行上述代码,而是非交互地执行脚本文件,那么还需要加上图形控制界面,以免脚本瞬间执行完毕,导致你听不到声音;使用交互解释器时也可以生成图形控制界面。

s.gui(locals())

也可以通过Python的time模块控制服务器关闭时间:

time.sleep(1)
s.stop()

调整对象参数

振荡器对象可以随时调整参数。或通过set方法:

a.setFreq(1000)

或直接赋值:

a.freq = 1000

对象的链条与封包

一个振荡器实例可以作其他实例的输入,从而构成对象实例链条。下例是把前一个实例的频率传给后一个实例。

from pyo import *
s = Server().boot()
mod = Sine(freq=6, mul=50)
a = Sine(freq=mod + 440, mul=0.1).out()
s.gui(locals())

也可以为振荡器生成一个封包:

from pyo import *
s = Server().boot()
f = Adsr(attack=.01, decay=.2, sustain=.5, release=.1, dur=5, mul=.5)
a = Sine(mul=f).out()
f.play()
s.gui(locals())

所有类都有示例

Pyo中的所有类都带着一个示例,以展示类的用法。比如Harmonizer类,可以这样执行他的示例:

>>> from pyo import *
>>> example(Harmonizer)

配置音频输出(特别是在Windows系统中)

以下提示有助于你在Windows上配置音频输入/输出。其中一些处理程序也适用于其他操作系统。

在Windows中检测、选择音频宿主API

在Windows中选择合适的音频API是件头疼的事。你可以使用Pyo官方的检测脚本检测你的系统,以确定:

  • Pyo是否可以双工模式运行,即输入、输出都可用。
  • Pyo是否能连接到Windows中可用的不同宿主API。

调校Windows的WASAPI驱动

Windows的音频会话API(WASAPI, Audio Session API)是微软最现代的与音频设备对话的方法,自Vista以后可用。Pyo默认宿主是DIRECTSOUND,但你可以换成WASAPI:更换服务器对象的winhost参数。如果上述检测脚本告诉你:

Host: wasapi ==> Failed...

那么在任务栏上的扬声器图标上点右键,点选“播放设备”。然后选择你的设备,点击“属性”按钮;在“高级”书签页,确认其采样率与Pyo的采样率(默认为44100Hz)相同。如果需要,还可以勾选“独占模式”中的选项,这样可以忽视系统的混音器、缺省设置、尤其是音频驱动提供的任何效果。

假如获得如下信息,又想让Pyo以使用双工模式运行的话,那么在“录音设备”选项中也做同样的操作:

No input available. Duplex mode should be turned off.
you’ll have to make sure first that there is an available input device in that tab.

如果你用的是比较便宜的声卡(特别是,内置声卡都很不好!),你可能需要加大Pyo服务器的缓冲区规模,以避免音频流故障。

服务器初始化样例

# 采样率是44100Hz,缓冲区大小为256,声道数是2,双工模式全开,宿主是DIRECTSOUND
s = Server()

# 采样率是48000Hz,缓冲区大小为1024,声道数是2,双工模式全开,宿主是DIRECTSOUND
s = Server(sr=48000, buffersize=1024)

# 采样率是48000Hz,缓冲区大小为512,声道数是2,双工模式全开,宿主是WASAPI
s = Server(sr=48000, buffersize=512, winhost="wasapi")

# 采样率是48000Hz,缓冲区大小为512,声道数是2,双工模式半开(仅用输出),宿主是ASIO
s = Server(sr=48000, buffersize=512, duplex=0, winhost="asio")

# 采样率是96000Hz,缓冲区大小为128,声道数是1,双工模式全开,宿主是ASIO
s = Server(sr=96000, buffersize=128, nchnls=1, duplex=1, winhost="asio")

选择指定设备

一个宿主API可以指向多个可用的设备。有几个有用的函数可以帮助你选择音频设备:

  • pa_list_host_apis(): 打印音频宿主API列表。
  • pa_list_devices(): 打印音频设备列表。如果有设备索引号,那么在第一列。
  • pa_get_default_input(): 返回默认输入设备索引号。
  • pa_get_default_output(): 返回默认输出设备索引号。
  • pa_get_default_devices_from_host(host): 为给定音频宿主返回默认输入、输出设备。

运行下面的代码,看看你的音频配置的当前状态:

from pyo import *
print("音频宿主API:")
pa_list_host_apis()
pa_list_devices()
print("默认输入设备:%i" % pa_get_default_input())
print("默认输出设备:%i" % pa_get_default_output())

如果意向宿主的默认设备不是你想要的,你可以用setInputDevice(x)setOutputDevice(x)两个方法,通知服务器哪个设备是你想要的。这两个方法接收意向设备的索引号,必须在服务器引导(Server.boot())前调用。比如:

from pyo import *
s = Server(duplex=0)
s.setOutputDevice(0)
s.boot()

提高Pyo程序的性能

这里列出多个提示,帮助你提供Pyo程序的性能。

Python提示

大部分计算都是在Pyo的C语言部分进行的,所以Python层面能做的不多。不过仍有两个技巧可以考虑:

调整解释器的检查间隔

sys.setcheckinterval(interval)调整解释器的检查频率。默认值是100,即每100条Python虚拟指令执行一次检查。加大此值可能会提高使用线程的程序的性能。

使用subprocess(子进程)或multiprocessing(多进程)模块

使用subprocessmultiprocessing模块能在多处理器上批量处理。下面的小例子使用了multiprocessing模块,以便在多处理器上批量进行正弦(Sine)波计算。

#!/usr/bin/env python
# encoding: utf-8
"""
在多线程中批量生成大量的正弦(sine)波。
在命令行中运行脚本,加上-i旗标。

调用quit()以停止工方(worker,负责处理子任务),并退出程序。
"""
import time
import multiprocessing
from random import uniform
from pyo import Server, SineLoop

class Group(multiprocessing.Process):
    def __init__(self, num_of_sines):
        super(Group, self).__init__()
        self.daemon = True
        self._terminated = False
        self.num_of_sines = num_of_sines

    def run(self):
        # 每核都要运行的代码必须写在run()方法中
        self.server = Server()
        self.server.deactivateMidi()
        self.server.boot().start()

        freqs = [uniform(400,800) for i in range(self.num_of_sines)]
        self.oscs = SineLoop(freq=freqs, feedback=0.1, mul=.005).out()

        # 保持进程活动...
        while not self._terminated:
            time.sleep(0.001)

        self.server.stop()

    def stop(self):
        self._terminated = True

if __name__ == '__main__':
    # 开动四个进程,每个播放500个振荡器。
    jobs = [Group(500) for i in range(4)]
    # 下行源码如此,在我的机器上运行本代码没有任何结果。如改作`job.run()`,则有声音。但作者说用户不应该调用`run`方法。
    # 他不确定涉及多核的实例能不能在Windows上运行(可以在Linux上运行,MacOS上大概也能)。
    # 可以肯定,使用SharedTable对象的将无法运行,此对象WIndows上不支持。作者有意要测试其他对象。
    [job.start() for job in jobs]

    def quit():
        "停止工方并退出程序。"
        [job.stop() for job in jobs]
        exit()

避免在初始化之后进行内存分配

动态内存分配(malloc/calloc/realloc)容易引起不确定性;分配内存的时间无法预见,使其不适合于实时系统。为了保证音频回调函数何时候都能平稳运行,最好在程序初始化时生成所有音频对象,然后在需要时调用它们的'stop()``play()``out()方法。

注意,一个简单的算数符号与音频对象关联后即可生成一个傀儡对象(以持有变动后的符号),于是会分配内存给它的音频流并且增加一个处理任务到CPU中。运行下面的简单代码,观察其进程的CPU如何增长:

from pyo import *
import random

s = Server().boot()

env = Fader(0.005, 0.09, 0.1, mul=0.2)
jit = Randi(min=1.0, max=1.02, freq=3)
sig = RCOsc(freq=[100,100], mul=env).out()

def change():
    freq = midiToHz(random.randrange(60, 72, 2))
    # 因为`jit`是Pyo对象,`freq+jit`和`freq-jit`会
    # 生成一个`傀儡/Dummy`对象,为了它要生成一个引用并
    # 保存在`sig`对象中。其后果是内存兼CPU
    # 增长,直至出事!
    sig.freq = [freq+jit, freq-jit]
    env.play()

pat = Pattern(change, time=0.125).play()

s.gui(locals())

这个程序的高效版本应该像这样:

from pyo import *
import random

s = Server().boot()

env = Fader(0.005, 0.09, 0.1, mul=0.2)
jit = Randi(min=1.0, max=1.02, freq=3)
# 生成一个`Sig`对象以持有频率值。
frq = Sig(100)
# 仅在初始化时一次性地生成`傀儡/Dummy`对象
sig = RCOsc(freq=[frq+jit, frq-jit], mul=env).out()

def change():
    freq = midiToHz(random.randrange(60, 72, 2))
    # 仅仅改变Sig对象的`value`属性
    frq.value = freq
    env.play()

pat = Pattern(change, time=0.125).play()

s.gui(locals())

别做任何会激发垃圾回收的事

Python的垃圾回收也是一个不确定的进程。你要避免任何会激发它的事。所以,不要删除音频对象——这最终会删除许多音频流对象,而应该只是调用它的stop()方法以便把它从服务器的处理循环中移除。

Pyo提示

在应用效果前先混合

使用Pyo很容易使CPU超饱和,尤其是使用多声道扩展特征时。如果你的最终输出使用的声道少于对象中的音频流数量,别忘了先混合(调用它的mix()方法),然后再给信号总体应用效果。

看看下面的片段,它生成50个振荡器构成的的一个合奏,再对最终声音应用一个相位效果。

src = SineLoop(freq=[random.uniform(190,210) for i in range(50)],
                feedback=0.1, mul=0.01)
lfo = Sine(.25).range(200, 400)
phs = Phaser(src, freq=lfo, q=20, feedback=0.95).out()

这个版本在Pyo作者的i5 3320M @ 2.6GHz的ThinkPad T430上试用约47%的CPU。问题在于50个振荡器给Phaser对象作为输入,生成50个一样的Phaser对象实例,每个对象实例一个振荡器。这将大大地浪费CPU。下面的版本混合所有振荡器为一个立体声流,然后再应用效果,这样CPU消耗降到约7%!

src = SineLoop(freq=[random.uniform(190,210) for i in range(50)],
                feedback=0.1, mul=0.01)
lfo = Sine(.25).range(200, 400)
phs = Phaser(src.mix(2), freq=lfo, q=20, feedback=0.95).out()

当用到高消耗效果时,这会剧烈影响CPU的消耗。

停止不用的音频对象

当你不用一个音频对象(但还想留着备用)时,调用stop()方法。这会通知服务器把它移出计算循环。设置音量为0不能节省CPU(全都计算完后乘以0),而stop()方法可以。Pyo作者本人的合成类常常近似如下:

class Glitchy:
    def __init__(self):
        self.feed = Lorenz(0.002, 0.8, True, 0.49, 0.5)
        self.amp = Sine(0.2).range(0.01, 0.3)
        self.src = SineLoop(1, self.feed, mul=self.amp)
        self.filt = ButLP(self.src, 10000)

    def play(self, chnl=0):
        self.feed.play()
        self.amp.play()
        self.src.play()
        self.filt.out(chnl)
        return self

    def stop(self):
        self.feed.stop()
        self.amp.stop()
        self.src.stop()
        self.filt.stop()
        return self

控制属性当用数字而不是Pyo对象

当纯数字用作属性时,对象的内部处理函数会进行优化。除非你真地需要通过某些参数控制音频,否则不要浪费CPU的时钟周期,而是把固定数值给到所有属性,这样它们就不需要随时间变更。看下面的对比:

n = Noise(.2)

# 约5%的CPU消耗
p1 = Phaser(n, freq=[100,105], spread=1.2, q=10,
            feedback=0.9, num=48).out()

# 约14%的CPU消耗
p2 = Phaser(n, freq=[100,105], spread=Sig(1.2), q=10,
            feedback=0.9, num=48).out()

生成p2spread属性时用一个音频信号,导致48个槽口(notch)的频率在每次采样时都要重新计算,这将是消耗很高的进程。

检查非常态数/低常态数/非规格化浮点数(denormal/subnormal number)

据维基百科:

在计算机科学的浮点数算法中,denormal数或denormalized数(现常作subnormal数)会填充0后面的潜流(underflow)1空位。任何非零的数,数值小于最小常态(normal)数时,即为subnormal数。

问题在于,某些处理器计算非常态数值时很慢,这会很快地增加CPU消耗。解决办法是用一个Denorm(非常态)对象掩护(wrap)将受制于非常态数的对象(任何内部带有递归延迟行的对象,比如过滤/filter, 延迟/delay, 混响/reverb, 泛音/harmonizer,等等)。 Denorm添加一点点噪音到自己的输入中, 仅是略大于最小常态数的量。当然,你可以把相同的噪音用于多重的非常态化:

n = Noise(1e-24) # 用于非常态对象的低水平噪音

src = SfPlayer(SNDS_PATH+"/transparent.aif")
dly = Delay(src+n, delay=.1, feedback=0.8, mul=0.2).out()
rev = WGVerb(src+n).out()

使用Pyo对象(PyoObject),只要可用

如果Pyo对象能实现你的意图,优先找来用,它一定比你从新写的更高效。下面的结构尽管在教学上有价值,但效率(即CPU与内存消耗)永远比不上用C写的Pyo对象(Phaser)。

    a = BrownNoise(.02).mix(2).out()

    lfo = Sine(.25).range(.75, 1.25)
    filters = []
    for i in range(24):
        freq = rescale(i, xmin=0, xmax=24, ymin=100, ymax=10000)
        filter = Allpass2(a, freq=lfo*freq, bw=freq/2, mul=0.2).out()
        filters.append(filter)

使用Biquadx(stages=4)也会比用相同参数串联四个Biquad对象更高效。

避免三角函数计算

避免音频频率的三角函数计算(Sin, Cos, Tan, Atan2, 等等), 而用简单的近似对象代替。比如,你可以用基于Sqrt的更廉价的对象代替纯Sin/Cos声像函数:

# 沉重
pan = Linseg([(0,0), (2, 1)]).play()
left = Cos(pan * math.pi * 0.5, mul=0.5)
right = Sin(pan * math.pi * 0.5, mul=0.5)
a = Noise([left, right]).out()

# 廉价
pan2 = Linseg([(0,0), (2, 1)]).play()
left2 = Sqrt(1 - pan2, mul=0.5)
right2 = Sqrt(pan2, mul=0.5)
a2 = Noise([left2, right2]).out()

使用近似对象,如果绝对精确没有必要

绝对精确不是确实重要时,你可以通过近似对象节省宝贵的CPU时钟周期。FastSinesin函数的近似对象,它几乎比查表(Sine)廉价一半。Pyo作者计划将来添加更多的近似对象。

重用你的生成器

有时有可能把相同的信号用于近似目的。琢磨下面的过程:

# 单个白噪音
noise = Noise()

# 非常态信号
denorm = noise * 1e-24
# 1上下的小震动,用于调制频率
jitter = noise * 0.0007 + 1.0
# 波导的激励信号
source = noise * 0.7

env = Fader(fadein=0.001, fadeout=0.01, dur=0.015).play()
src = ButLP(source, freq=1000, mul=env)
wg = Waveguide(src+denorm, freq=100*jitter, dur=30).out()

在这里,同一个白噪音同时用于三个目标。首先,用于生成非常态信号。然后,用于生成小震动以备波导用作频率(给线性声音添加一个小嗡嗡声)。最后,用作波导的激励。这无疑比生成没有明显听觉差异的三个不同白噪音要廉价。

任由muladd属性保持默认值,只要可能

mul=1add=0时,有一个内在条件会绕开对象的“后置处理”(post-processing)函数。好的实践是在同一处使用多重控制,而不是乱糟糟地在每个对象中使用mul属性。

    # 错误
    n = Noise(mul=0.7)
    bp1 = ButBP(n, freq=500, q=10, mul=0.5)
    bp2 = ButBP(n, freq=1500, q=10, mul=0.5)
    bp3 = ButBP(n, freq=2500, q=10, mul=0.5)
    rev = Freeverb(bp1+bp2+bp3, size=0.9, bal=0.3, mul=0.7).out()

    # 棒
    n = Noise(mul=0.25)
    bp1 = ButBP(n, freq=500, q=10)
    bp2 = ButBP(n, freq=1500, q=10)
    bp3 = ButBP(n, freq=2500, q=10)
    rev = Freeverb(bp1+bp2+bp3, size=0.9, bal=0.3).out()

避免图形更新

即使运行在不同线程中,用不同的属性,音频回调函数与Python程序图形界面都是同一个进程,分享相同的CPU。如果你不需要看表或使用滑块,那么不要使用服务器的GUI。你可以从命令行启动脚本,带上旗标-i以便解释器保持活动。

>>> python -i myscript.py

CPU高消耗对象

这是库中的大部分、但并非完全的CPU高消耗对象的列表。

  • Analysis
  • Yin
  • Centroid
  • Spectrum
  • Scope
  • Arithmetic
  • Sin
  • Cos
  • Tan
  • Tanh
  • Atan2
  • Dynamic
  • Compress
  • Gate
  • Special Effects
  • Convolve
  • Prefix Expression Evaluator
  • Expr
  • Filters
  • Phaser
  • Vocoder
  • IRWinSinc
  • IRAverage
  • IRPulse
  • IRFM
  • Fast Fourier Transform
  • CvlVerb
  • Phase Vocoder
  • Almost every objects!
  • Signal Generators
  • LFO
  • Matrix Processing
  • MatrixMorph
  • Table Processing
  • Granulator
  • Granule
  • Particule
  • OscBank
  • Utilities
  • Resample

  1. 译注:因精度限制/指数不够用而在小数点后加0表示,如0.000123e-126。这样一来,后面的有实际意义的数值即成为潜流(underflow)被丢弃,从而影响结果;如果重复此类计算则可能导致严重错误。 


Fork me on GitHub