Link search Menu Expand Document

粗鲁机械

norns引擎研究1:构建合成器、创建引擎、在norns上使用引擎进行脚本编写

SuperCollider是一个免费开源的音频制作平台,为norns提供合成层支持。许多norns脚本都是SuperCollider(定义合成引擎)和Lua(定义硬件+UI交互)的组合。

本研究的目的是为norns上的引擎开发提供指导,使用SuperCollider。我们假设您已经对SuperCollider有一些熟悉度——如果没有,请务必查看学习SuperCollider获取有用的学习资源,并在一些实验后回到这里。

章节

准备

如果您还没有,请在您的主要非norns计算机上下载SuperCollider。虽然我们最终会在norns上结束,但能够在实验阶段快速执行SuperCollider代码片段将为引擎构建提供必要的基础。

请注意,如果您是SuperCollider新手,您可能会产生一些意外响亮/尖锐的声音。Nathan Ho在他的网站上有一些很棒的建议,特别是关于”电平管理和音量安全”:

快速总结:

  1. 如果您在macOS上,请立即升级到至少SC 3.12,并在启动文件中添加Server.default.options.safetyClipThreshold = 1,这样音频输出会被削波。
  2. 在系统音量控制中工作在低电平,但在SC中工作在高电平。如果您的音频处于舒适电平,SC中峰值在-6 dBFS,那么最响亮的声音只能比这高6 dB,所以合成事故可能会令人吃惊,但不太可能危险。
  3. 作为推论:如果SC产生安静信号,不要使用计算机的音量控制调高音量!相反,在SC中调高它。
  4. 在主总线上使用限制器。
  5. 将增益量输入为例如 * -60.dbamp 而不是 * 0.001。

现在,让我们在深入研究SuperCollider之前做一些norns的整理工作:

  • 在code内创建一个名为engine_study的文件夹
  • 在engine_study内创建一个名为lib的文件夹

第一部分:在SuperCollider中从零开始

我们将从在SuperCollider中构建一个基础合成器定义开始,稍后我们会将其移植到norns上。在您的非norns计算机上打开SuperCollider并创建一个新的SuperCollider文件。

制作声音

在空白SuperCollider文件中输入以下文本:

(
SynthDef("Moonshine", {
	// 让我们构建一个原始声音:
	var pulse = Pulse.ar(freq: 300);
	var saw = Saw.ar(freq: 300);
	var sub = Pulse.ar(freq: 300/2);
	var noise = WhiteNoise.ar(mul: 0.1);
	var mix = Mix.ar([pulse,saw,sub,noise]);

	// 并生成一个包络:
	var envelope = Env.perc(attackTime: 0.3, releaseTime: 2).kr(doneAction: 2);
	
	// 将原始声音通过谐振滤波器
	//  并用包络调制滤波器
	var filter = MoogFF.ar(in: mix, freq: 6000 * envelope, gain: 3);

	// 我们的滤波信号然后乘以我们的包络,居中声像:
	var signal = Pan2.ar(filter*envelope,0);

	// 让我们将信号发送到输出:
	Out.ar(0,signal);
	
	}).add;
)

导航到结束括号并按CMD-RETURN(Mac)/CTRL-ENTER(Win)来评估文本。您还不会听到声音,因为这只是将合成器定义添加到我们当前的SuperCollider会话中。

要触发合成器并发出声音,将其分配给变量并评估:

x = Synth("Moonshine");

您应该听到一个被滤波器扫过的嗡嗡声调,因为我们的SynthDef通过我们的三个波形生成器UGens(以及白噪声UGen)播放300hz音符,并生成一个控制合成器振幅但也调制最终级谐振滤波器截止频率的包络。

构建和修改参数

如果我们要将这个草图作为norns引擎移植并分配一个键来触发合成器,它最终会变得陈旧,因为音高、包络的起音和释音、噪声电平、起始滤波器截止,甚至次振荡器的除法——这些都是静态值。所以,让我们为我们的SynthDef引入一些参数来在每次执行时修改这些元素:

(
SynthDef("Moonshine", {
	arg freq = 330, sub_div = 5, noise_level = 0.1,
	cutoff = 8000, resonance = 3,
	attack = 0.3, release = 0.4,
	amp = 1, pan = 0, out = 0;
	
	var pulse = Pulse.ar(freq: freq);
	var saw = Saw.ar(freq: freq);
	var sub = Pulse.ar(freq: freq/sub_div);
	var noise = WhiteNoise.ar(mul: noise_level);
	var mix = Mix.ar([pulse,saw,sub,noise]);

	// 并生成一个包络:
	var envelope = Env.perc(attackTime: attack, releaseTime: release, level: amp).kr(doneAction: 2);
	
	// 将原始声音通过谐振滤波器
	//  并用包络调制滤波器
	var filter = MoogFF.ar(in: mix, freq: cutoff * envelope, gain: resonance);

	// 我们的滤波信号然后乘以我们的包络,居中声像:
	var signal = Pan2.ar(filter*envelope,pan);

	// 让我们将信号发送到输出:
	Out.ar(out,signal);
	
	}).add;
)

现在,我们可以简单地用默认值执行:

x = Synth("Moonshine");

或者我们可以通过寻址这些参数来修改值:

x = Synth("Moonshine",[\freq,300, \cutoff,2000, \resonance,3.7, \release,3]);

现在,我们有了一个整洁的小合成器+发送命令的方式——让我们将这个SynthDef塑造成一个norns引擎!

第二部分:为norns构建引擎

与其试图诗意地引导您通过发现,似乎首先呈现norns引擎样板更有帮助。

样板

Engine_MySynthName : CroneEngine {

	// ** 在这里添加您的变量 **

	// 当脚本实际加载引擎时调用此函数。
	// 您可以假设它将在Routine中调用,
	//  并且您可以使用.sync和.wait方法。
	alloc { // 为以下内容分配内存:
		
		// ** 在这里添加您的SynthDefs **
				
		// 如果您需要在发送命令之前让SynthDef可用,
		//  通过与服务器同步来**取消注释以下行**:
		// Server.default.sync;
		
		// ** 在这里添加您的命令 **
		
	}

}

Moonshine引擎

按照上述结构,让我们从我们之前在SuperCollider中工作的合成器定义构建一个norns引擎。

最终目标是生成一个名为Engine_Moonshine.sc的SuperCollider类文件,其中包含我们的引擎定义。

我们可以在SuperCollider中执行此操作(确保保存为类文件!)并将其导入到我们的code > engine_study > lib文件夹中,或者我们可以直接在maiden中创建文件(只需使用maiden的文件重命名功能将文件重命名为Engine_Moonshine.sc)。

Engine_Moonshine : CroneEngine {
// 所有norns引擎都遵循上面的'Engine_MySynthName'约定

	var params;

	alloc { // 为以下内容分配内存:

		// 添加SynthDefs
		SynthDef("Moonshine", {
			arg out = 0,
			freq, sub_div, noise_level,
			cutoff, resonance,
			attack, release,
			amp, pan;

			var pulse = Pulse.ar(freq: freq);
			var saw = Saw.ar(freq: freq);
			var sub = Pulse.ar(freq: freq/sub_div);
			var noise = WhiteNoise.ar(mul: noise_level);
			var mix = Mix.ar([pulse,saw,sub,noise]);

			var envelope = Env.perc(attackTime: attack, releaseTime: release, level: amp).kr(doneAction: 2);
			var filter = MoogFF.ar(in: mix, freq: cutoff * envelope, gain: resonance);

			var signal = Pan2.ar(filter*envelope,pan);

			Out.ar(out,signal);

		}).add;

  // 在这个例子中我们不需要与服务器同步,
  //   因为我们实际上没有做任何依赖于SynthDef可用的事情,
  //   所以让我们保持这个注释:
  // Server.default.sync;
		
  // 让我们创建一个Dictionary(无序关联集合)
  //   来存储参数值,初始化为默认值。
		params = Dictionary.newFrom([
			\sub_div, 2,
			\noise_level, 0.1,
			\cutoff, 8000,
			\resonance, 3,
			\attack, 0,
			\release, 0.4,
			\amp, 0.5,
			\pan, 0;
		]);

  // "命令"是Lua解释器控制引擎的方式。
  // 格式字符串类似于OSC消息格式字符串,
  //   'msg'参数包含数据。

  // 我们只是遍历字典的键,
  //   并为每个键添加一个命令,更新相应的值:
		params.keysDo({ arg key;
			this.addCommand(key, "f", { arg msg;
				params[key] = msg[1];
			});
		});

  // 这比(但类似于)单独定义每个命令更快,例如:
		// this.addCommand("amp", "f", { arg msg;
		//	  amp = msg[1];
		// });

  // 然而,"hz"命令需要新语法!
  // ".getPairs"将字典展平为交替的键,值数组
  //   "++"连接它:
		this.addCommand("hz", "f", { arg msg;
			Synth.new("Moonshine", [\freq, msg[1]] ++ params.getPairs)
		});

	}

}

一旦Engine_Moonshine.sc文件位于code > engine_study > lib下,我们将希望通过SYSTEM > RESTART一起重启matron + SuperCollider。

命令

构建命令是真正有趣的地方,因为命令向Lua层暴露动态控制。一旦您确定要从脚本控制什么,命令构建就相当直接了。

我们可以为每个音色参数单独定义命令,例如:

this.addCommand("release", "f", { arg msg;
	release = msg[1];
});

其中:

  • "release"是我们希望Lua用来引用此命令的名称
  • "f"定义我们期望的参数类型("f"表示浮点数,但我们也可以使用"i"表示整数或"s"表示字符串)
    • 命令也可以一次接受许多参数(和参数类型),例如"ifffff"
    • 我们在这个例子中使用单浮点命令,因为这对介绍来说更容易
  • arg msg让SuperCollider知道传入的参数是要解包的消息
  • release = msg[1]将SynthDef的release分配给第一个消息的值
    • 在我们的例子中,我们只有一个消息——一个"f"(浮点数)

但我们有8个音色参数,这意味着我们基本上会一遍又一遍地重复相同的结构,只是改变名称。

与其不必要地重复具有小差异的相似代码行,我们的Moonshine引擎利用SuperCollider的Dictionary对象构建一个关联数组,保存我们的每个参数命令(和起始默认值):

params = Dictionary.newFrom([
	\sub_div, 2,
	\noise_level, 0.1,
	\cutoff, 8000,
	\resonance, 3,
	\attack, 0,
	\release, 0.4,
	\amp, 0.5,
	\pan, 0;
]);

一旦我们将params建立为Dictionary,我们可以使用SuperCollider的内置迭代方法非常容易地迭代它。keysDo允许我们迭代Dictionary并为每个键评估给定函数。由于这些音色命令中的每一个都接受单个浮点数并为我们的SynthDef贡献单个参数,我们可以使用这种方法节省很多行:

params.keysDo({ arg key;
	this.addCommand(key, "f", { arg msg;
		params[key] = msg[1];
	});
});

命令焦点:hz

Moonshine代表一个有用的设计模式——一次性合成器,在播放音符时捆绑当前音色参数状态。我们的hz命令就是它发生的方式:

this.addCommand("hz", "f", { arg msg;
	Synth.new("Moonshine", [\freq, msg[1]] ++ params.getPairs)
});

此命令的动作首先将参数作为我们Moonshine合成器的freq值传递,但然后它使用另一个Dictionary方法(.getPairs)来简洁地捆绑我们的音色参数。通过数组连接方法(++),一行代码可以执行以下操作:

  • 创建新的Synth:Synth.new(
  • 使用Moonshine SynthDef:"Moonshine",
  • 并将传入参数作为合成器的频率传递:[\freq, msg[1]]
  • 但也捆绑该参数: ++
  • 与每个当前音色参数值:params.getPairs)

结果是一个接受频率的单个浮点数并捆绑所有其他变量当前状态来实例化Moonshine声音的命令。

使用命令

现在我们的Moonshine引擎已安装在norns上(并且我们做了适当的SYSTEM > RESTART),让我们使用Lua来执行我们在前一个练习中建立的命令。

在maiden中,导航到code > engine_study文件夹并创建一个新的Lua文件(确保此文件在code > engine_study > lib文件夹外创建!)。将其命名为Moonshine.lua并输入以下文本:

engine.name = 'Moonshine'
-- 注意:单引号或双引号无关紧要,只是不要混合+匹配对!

s = require 'sequins'
-- 查看 https://monome.org/docs/norns/reference/lib/sequins 获取更多信息

function init()
  mults = s{1, 2.25, s{0.25, 1.5, 3.5, 2, 3, 0.75} } -- 创建hz倍数的sequins
  playing = false
  base_hz = 200
  sequence = clock.run(
    function()
      while true do
        clock.sync(1/3)
        if playing then
          engine.hz(base_hz * mults() * math.random(2))
        end
      end
    end
  )
end

function key(n,z)
  if n == 3 and z == 1 then
    playing = not playing
    mults:reset() -- 将'mults'索引重置为1
    redraw()
  end
end

function redraw()
  screen.clear()
  screen.move(64,32)
  screen.text(playing and "K3: 关闭" or "K3: 开启")
  screen.update()
end

现在,如果我们运行这个脚本并按K3,我们的序列将切换开关。

就我们的引擎而言,我们只使用了一个命令——hz。这是在我们的SuperCollider代码中启动声音的方式,但它通过轮询许多其他参数的状态来做到这一点:\pw,pw, \amp,amp, \cutoff,cutoff, \resonance,resonance, \release,release, \pan,pan。如果我们想修改这些方面之一怎么办?

由于我们在SuperCollider代码中将每个都设置为命令,我们可以简单地在Lua中执行对它们的更改(对于这个例子,通过maiden的REPL):

>> engine.attack(0.1)
>> engine.cutoff(500)
>> engine.sub_div(3)

当我们的序列播放时,我们执行这些命令,您会注意到我们合成器的音色发生变化。

第三部分:为我们的引擎构建Lua库文件

norns脚本编写的一个便利是参数系统,它为脚本免费提供MIDI控制+预设。虽然通过REPL的命令行控制我们的引擎很有趣,但对于所有类型的表演来说,这可能不是最有效的手段。但我们不想将参数初始化和引擎维护留给主脚本——如果有人构建了一个很棒的控制方案,只想插入我们的引擎,我们不希望他们必须挖掘SuperCollider代码来弄清楚如何最好地从Lua寻址我们所有不同的命令。

所以,为了干净+可移植地将我们的引擎集成到norns生态系统中,让我们为我们的引擎构建一个配套的Lua文件,它将把它的命令作为脚本中使用的部分捆绑起来!

我们将这个文件命名为moonshine_engine.lua,我们希望它位于我们的code > engine_study > lib文件夹中——所以要么通过maiden在那里创建它,要么将您外部创建的moonshine_engine.lua文件导入到该位置:

local Moonshine = {}
local Formatters = require 'formatters'

-- 首先,我们将所有命令收集到norns友好的范围内
local specs = {
  ["amp"] = controlspec.new(0, 2, "lin", 0, 1, ""),
  ["sub_div"] = controlspec.new(1, 10, "lin", 1, 2, ""),
  ["noise_level"] = controlspec.new(0, 1, "lin", 0, 0.3, ""),
  ["cutoff"] = controlspec.new(0.1, 20000, 'exp', 0, 1300, "Hz"),
  ["resonance"] = controlspec.new(0, 4, "lin", 0, 2, ""),
  ["attack"] = controlspec.new(0.003, 8, "exp", 0, 0, "s"),
  ["release"] = controlspec.new(0.003, 8, "exp", 0, 1, "s"),
  ["pan"] = controlspec.PAN
}

-- 这个表建立参数初始化的顺序:
local param_names = {"amp","sub_div","noise_level","cutoff","resonance","attack","release","pan"}

-- 初始化参数:
function Moonshine.add_params()
  params:add_group("Moonshine",#param_names)

  for i = 1,#param_names do
    local p_name = param_names[i]
    params:add{
      type = "control",
      id = "Moonshine_"..p_name,
      name = p_name,
      controlspec = specs[p_name],
      formatter = p_name == "pan" and Formatters.bipolar_as_pan_widget or nil,
      -- 每次参数更改时,我们都会将其发送到SuperCollider引擎:
      action = function(x) engine[p_name](x) end
    }
  end
  
  params:bang()
end

-- 单一用途触发命令发射音符
function Moonshine.trig(hz)
  if hz ~= nil then
    engine.hz(hz)
  end
end

 -- 我们将这些引擎特定的Lua函数返回给主脚本:
return Moonshine

导入+初始化

现在我们的引擎音色命令都自包含为norns参数,主脚本可以非常容易地导入+初始化Moonshine:

-- SC引擎研究:导入+初始化

engine.name = 'Moonshine' -- 将引擎分配给此脚本的运行

-- 我们引擎的Lua文件被分配给脚本作用域变量:
moonshine = include('engine_study/lib/moonshine_engine')
s = require 'sequins'

function init()
  moonshine.add_params() -- 脚本通过`.add params()`函数添加参数
  mults = s{1, 2.25, s{0.25, 1.5, 3.5, 2, 3, 0.75} }
  playing = false
  sequence = clock.run(
    function()
      while true do
        clock.sync(1/3)
        if playing then
          moonshine.trig(200 * mults() * math.random(2))
        end
      end
    end
  )
end

function key(n,z)
  if n == 3 and z == 1 then
    playing = not playing
    mults:reset()
    redraw()
  end
end

function redraw()
  screen.clear()
  screen.move(64,32)
  screen.text(playing and "K3: 关闭" or "K3: 开启")
  screen.update()
end

脚本作用域变量+路径

每当我们在norns脚本中包含库函数时,我们需要将其分配给脚本作用域变量才能使用它。从上面的代码片段:

moonshine = include('engine_study/lib/moonshine_engine')

记住:在我们的moonshine_engine.lua文件中,我们将所有引擎特定函数包装到一个Moonshine表中,然后我们在末尾返回它。所以当我们使用include时,我们将这些函数分配给脚本作用域变量moonshine,然后它可以访问这些相同的函数。要确认,运行SC引擎研究:导入+初始化代码片段并在命令行上执行以下操作:

>> tab.print(moonshine)

返回:

add_params	function: 0x3f3860
trig	function: 0x4fa410

我们提供给include函数的路径也很重要——它指定编译器应该在哪里查找来完善脚本的功能。为了让任何脚本找到并初始化我们的Moonshine的Lua文件,我们提供确切的路径(例如'engine_study/lib/moonshine_engine')。

准备分发

要向其他人分发我们的Moonshine引擎,我们可以使用以下架构:

moonshine/
  lib/
    Engine_Moonshine.sc
    moonshine_engine.lua
  moonshine.lua

示例脚本

虽然我们通过提供配套的Lua库文件使另一个脚本更容易使用我们的Moonshine引擎,但我们可以通过包含一个简单的示例脚本(在上面的文件树中,这是moonshine.lua文件)来确保成功+可读性,它阐明了用法。

上面的SC引擎研究:导入+初始化片段是一个很好的起点:

  • 它建模了正确的引擎命名
  • 它确认了库文件的路径
  • 它显示了如何将引擎的音色参数添加为脚本初始化的一部分
  • 它显示了如何触发引擎

进一步

如果您觉得准备更深入地探索SuperCollider和Lua(希望您这样做!),这里有一些扩展Moonshine引擎的出发点:

  • 在屏幕上显示参数值
  • 在主脚本UI中创建norns上的参数操作交互
  • 用外部MIDI控制替换sequins音序器
  • 为滤波器截止调制创建单独的包络
  • 添加控制3个声音中每个单独电平的机制

要继续探索+为norns创建新的合成引擎,我们强烈推荐: