Live Coding

Dead Reviewing

确实, 在复习的紧要关头开始搞这种东西看起来就是在找死, 而我对明天的考试 (热统) 毫无信心.

(注: 本文大部分内容都是在考完后写的, 虽然后面还有两门考试, 但是总得让我缓缓是吧. )

Live Coding

首先, 什么是 Live Coding?

Live coding, sometimes referred to as on-the-fly programming, just in time programming and conversational programming, makes programming an integral part of the running program.

from Wikipedia

好像确实没有问题. 尽管如果你在网络上搜索, 很有可能会搜索得到一个 “LiveCode” – 貌似是一个用来创建应用的一个软件. 或者可能会搜索到一个叫做 Live Coding 的概念, 即在编辑代码的时候可以在程序运行的时候更新代码并实时载入. (比如 UE 中的 Live Coding, 介绍内容就是在运行时重新编译并修正你的游戏二进制文件.)

好吧, 我可能一开始理解错了, 我一开始认为这个 Live 是指 “现场” (Live House, Music Live Performance) 之类的. 因为一开始我接触到这个玩意的时候, 是这个 视频 (bilibili) (不一定就是一模一样的那个视频, 大概是类似的. 视频中使用的是 Sonic Pi, 详细会在之后介绍. )

算了, 那么就不卖关子了. 用农民的话说, 就是通过计算机编程生成音乐.

那么, 为什么玩这个?

很简单, 因为看起来很有意思.

并且我也想学一些音乐知识. 玩合成器没钱也没有精力, 玩乐器手指太笨拙, 但是写代码, 私以为应该还算是勉勉强强. 恰好最近有学习 Lisp 的一个想法, 所以在论坛划水的时候偶然间发现了这样的一个有趣的玩具: Extempore.

(注: 为什么不用其他的 Live Coding 的语言呢? 比如 Sonic Pi. 实际上我之前也用过, 但是不知道是不是因为系统更新了还是我把环境给搞坏了, 最近不能开启了, 所以就只好换用了新的. 反正没有沉没成本不是. )

不过相比 Sonic Pi, 我觉得 Extempore 的 文档 实在是少得可怜… 并且这些文档很少有一些比较农民的解释和说明, 也缺少一些技术细节的文档, 所以开始玩的时候还是挺麻烦的. 不过, 有一个比较好的 视频 (可惜比较糊) 可以用来学习入门. 或者是参考 Xinyu's Extempore tutorials. (下文主要就是这篇文章的一个转述)

Extempore Installation

软件的仓库 地址 里面提供了基本的安装方法. 对于 Emacs 用户, 只需要通过 MELPA 再安装一个 extempore-mode 即可. 我的配置文件如下:

(use-package extempore-mode
  :config
  (setq extempore-path "/path/to/extempore/"))

然后在终端进入 /path/to/extempore 目录后, 运行 ./extempore. (这样做是为了防止出现找不到 init.xtm 的报错. 不过好像也可以直接在 Emacs 里面通过 switch-to-extempore 来运行, 默认的按键绑定是 C-c C-z. )

Extempore 的简单介绍

这部分主要参考的是 Xinyu's 那个教程, 尽管文中开头的说明是这个文档之后会合并到官方的文档里面, 但是我并没有找到对应的文档 (可能是没仔细看吧).

不过因为我没有太多的相关知识, 所以在看本文的时候, 还请就当作是一个自大的学生写的装模作样的笔记吧. 不一定对就是了.

Lisp (Scheme) 的简单介绍

Extempore 用的是 Lisp 的一个方言 Scheme 魔改得到的一个语言. 其中很大一部分和 Lisp 语言有着差不多的特性. 尽管 Lisp 是一个 “该死的括号语言”. 但是它非常的好学, 并且非常的强大.

基本上没有语法, 甚至可以自己创造语法. 并且非常好懂, 所以基本不必特意去学. (指使用的时候, 除非是为了了解其中的精妙之处. 不过用过之后基本也能理解为什么妙了. )

推荐参考资料:

  • Little Scheme
  • Land of Lisp
  • 等等, 这里就不偏题介绍了. 会在之后用到的地方介绍具体的操作.

如果你了解的话, 可以跳过这一节的部分, 直接看下一部分.

观察下面的代码:

(println (+ 1 2 (* 3 4))) ;; => 15

会发现, Lisp 的语法非常简单, 农民地认为它就是把函数提前, 然后在把函数的参数跟在后面, 整体用一个括号包裹即可. (上面的代码就是 println 打印输出 1 + 2 + 3 * 4 的结果. 输出可以在 extempore 的窗口中看到. )

而在 Lisp 里面, 函数, 或者说过程, 也是一种数据, 比如说我们可以用 lambda 关键词来生成一个函数:

(println
   ((lambda (x) (+ x 1)) 15)) ;; => 16

在形式上, 本来应该是函数的位置上现在是一个表达式. (注: 在 Lisp 里面, 每个表达式都应该会有其返回值. ) 那么这个表达式的返回值就应该是一个函数了. 这个函数接受的 (形式) 参数列表为 (x), 函数的表达式为 (+ x 1).

并且我们还能够将函数作为参数传入:

(println
 ((lambda (f1 f2 x) (+ (f1 x) (f2 x))) ;; f1(x) + f2(x)
  (lambda (x) (cos x))                 ;; f1: cos(x)
  (lambda (x) (sin x))                 ;; f2: sin(x)
  (/ 3.1415926 2)))                    ;; => 1.000000

于是, 你就基本上了解完了大部分的 Lisp 编程技术了. (注: 还有一个比较重要但是没有介绍的是宏. )

Extempore 项目的基本组成

  1. 初始化 DSP
  2. 载入 乐器采样器
  3. 编写音乐循环, 比如 和弦

DSP

什么是 DSP (Digital Signal Processor)? 以及它是干什么用的?

A digital signal processor (DSP) is a specialized microprocessor chip, with its architecture optimized for the operational needs of digital signal processing.

The goal of a DSP is usually to measure, filter or compress continuous real-world analog signals.

from Wikipedia

emm… 有点抽象? 不妨来看看下面这个示意图: (图片仍然来源于 Wikipedia)

https://upload.wikimedia.org/wikipedia/commons/b/bc/DSP_block_diagram.svg

那么一个农民的理解: 有点像是电吉他的效果器的感觉. (注: 我不了解电吉他, 也从来没有用过效果器, 但是没吃过猪肉, 总见过猪跑嘛不是. THE FART PEDAL (bilibili) 慎入)

(注: 实际上这个不应该叫做效果器, 也许可以叫做合成器. 不过我也说不好)

不妨就拿一个效果器来学习和理解吧: ( 图片来源 Wikipedia)

https://upload.wikimedia.org/wikipedia/commons/d/df/Pedalboard_%28995939579%29-2.jpg

可以看到, 大部分的效果器的外观都十分类似: 一个根输入线, 一根输出线, 一些控制按钮.

  • 输入的音频信号, 比如从拾音器上读到的音频信号, 或者是从上一级效果器中传来的输出信号.
  • 输出的信号线, 指定输出到什么通道里面.
  • 控制按钮, 用来控制效果器是怎么工作的

那么仔细看 (营销号口吻), 下面这段声明是一个 DSP 的声明:

(bind-func dsp:DSP
  (lambda (in time chan dat)
    0.0))

于其将其看成是一个函数, 我们不妨将其看成是一个 “效果器”. 如果你了解一些 Lisp 的编程知识的话, 那么不难知道, 在这里我们定义了一个叫做 dsp 的函数, 接受四个参数:

  • in:SAMPLE 就像是我们的输入的信号 (sample from input device)
  • time:i64 就是我们输入的信号的编号 (sample number)

    这有点像是我们将信号量化后, 按照时间顺序存放, 然后放入一个数组, 这个数组中的编号就是这个 time, 而我们可以用 (/ (convert time) SRf) 来转换当前的输入的序号和时间.

    其中, SRf 就是当前输入 (sample) 的频率. 显然, $\mathrm{d}t = \frac{1}{\mathrm{SRf}}$.

  • chan:i64 就是我们输出的信号通道 (audio channel)
  • dat:SAMPLE 就是用户用来控制的数据 (user data)

并且这个函数的返回值是 <return>=SAMPLE. 在这里, 补充一个约定, 用 <var-name>:TYPE 来表示 TYPE 类型的, 叫做 <var-name> 的变量. 其中, SAMPLE 类型的字面值在 -1.01.0 之间.

当然, 如果你只是运行那段代码的话, 并不会有任何事情发生. 毕竟你的输出始终是 0.0, 也就是没有声音.

那么先来试试看, 让这个 DSP 自己输出一个噪音:

(bind-func dsp:DSP
  (lambda (in time chan data)
    (* 0.2 (random))))

运行 (Emacs 中, 光标移动到该函数上, 执行 extempore-send-dwim, 默认按键绑定是 C-M-x) 完后, 你会在 extempore 的窗口看到如下输出: Compiled: dsp >>> [float,float,i64,i64,float*]*.

(注: 为了更快地上手, 所以更复杂的说明会留到之后再介绍. )

但是你可能 (一定听不到) 并没有听到任何的声音. 这是因为我们现在只是定义了有那么个东西, 并没有让其进行工作. 于是使用命令将其设置为我们当前使用的 DSP:

(dsp:set! dsp)

于是你应该可以听到 “悦耳的” 噪声了. 你可以中二地认为, 你将自己刚刚定义的这个函数插上了电 (plugged in), 然后它就开始输出了.

现在来干一些更加有趣的事情: 让我们来输出一个振幅随时间正弦变化的一个声音吧:

 (bind-func dsp:DSP
   (lambda (in time chan dat)
     (let ((amplitude 0.2)
	    (frequency 490.0)
	    (amplitude-frequency 2.0)
	    (two-pi (* 2.0 3.1415926)))
	(* (* amplitude
	      (sin (/ (* amplitude-frequency
			 two-pi
			 (convert time)) SRf)))
	   (sin (/ (* frequency two-pi (convert time)) SRf))))))

嗯, 你可以自己改变其中的变量来调整自己的喜好, 不过因为我没有审美, 所以就只能随便乱选了.

不过, 这么搞实在是太麻烦了, 所以我们不可能像 C 语言一样, 从零开始写所有的东西. 所以接下来, 我们要载入一些写好的乐器库.

载入乐器

(sys:load "libs/core/instruments.xtm")

现在我们载入了 Extempore 中自带的一个音乐库. 你应该可以在其输出中看到类似这样的结果:

Loading xtmrational library... done in 1.032445 seconds
Loading xtmaudiobuffer library... done in 0.929176 seconds
Loading xtmaudio_dsp library... done in 2.779191 seconds
Loading xtminstruments library... done in 10.362588 seconds
sys:load notification instruments already loaded

你可以这样中二地想: 现在我们打开了一个叫做 instruments 的一个装满了乐器的库房. 然后我们从中拿出一个 fmsynth FM 合成器. 并叫其 fmsynth (或者你不妨叫做 my-fm-synth).

(make-instrument my-fm-synth fmsynth)
;; SetValue:  my-fm-synth >>> [float,float,i64,i64,float*]*
;; New instrument bound as my-fm-synth in both scheme and xtlang

然后我们再将这个乐器和我们的 DSP 相连接. 不过为了简单, 我们的 DSP 是一个非常简单的程序: (即作为一个乐器来干活).

(bind-func dsp:DSP
  (lambda (in time chan dat)
    (my-fm-synth in time chan dat)))
;; Compiled:  dsp >>> [float,float,i64,i64,float*]*

现在让我们按下一个音符 (note):

(play-note (now)          ;; time
	     my-fm-synth    ;; inst
	     (random 60 80) ;; pitch
	     80             ;; volume
	     *second*       ;; dur
	     )

我们会发现, 在 play-note 这个操作中, 传入了如下的参数:

  • time 时刻 (也就是 (now) 现在), 即按下这个音符的时间.
  • inst 乐器, 这里传入的就是我们定义的 my-fm-synth.
  • pitch 音高 (这里是一个随机的量), 其对应的标准为 MIDI 的音符标记.
  • volume 音量 (这里是 80, 总共是 0100)
  • dur 为持续时间 (这里是 *second*, 是一个全局变量, 表示 1 秒钟. 又: 在 Lisp 里面, 常常约定用 * 来包围全局变量. )

当然, 我们还能够做一个循环来实现播放:

(define my-loop
  (lambda (time)
    (play-note time my-fm-synth (random 60 80) 80 *second*)
    (callback (+ time *second*) 'my-loop (+ time *second*))))

(my-loop (now))

于是你就拥有了一个比较 “阴沉” 的背景音乐了. 当然, 你也可以 pitch 调高一些. 比如 (random 80 120), 现在就是一些极其尖锐的背景音乐了. 有种惊悚片中的背景配乐的感觉了.

代码解释:

  • define 是 Scheme 中定义函数的一个关键词, 那么和之前的 bind-func 的区别则是: 后者是 Extempore 自己添加的一个叫做 Extemporelang 的东西, 通过静态的编译的方式来实现的.

    emmm… 感觉这样子讲有些不太严谨和准确, 你可以这样理解: 在执行 bind-func 这些 Extemporelang 部分的代码的时候, Extempore 会将代码编译后等待调用. 而 Scheme 的代码则是动态地被执行的.

    或者, 你可以中二地认为, Extemporelang 部分的代码得到的结果是一些 “硬件”, 而 Scheme 部分的代码, 得到的结果是一些 “软件”, 用来控制该怎么操控硬件.

    我们可以反复调用这个函数, 就好像是多个人在反复执行这个操作.

  • callback 传入的参数如下:
    • time, 这里是 (+ time *second*), 即在 +1 秒后调用 my-loop 这个函数.
    • closure, 即调用的函数的名字. 这里是 ='my-loop=. 其中 ='sym= 是 (quote sym) 的一个缩写. 可以理解为, 在环境中去找叫做 sym 的这个符号的函数.
    • args*, 这里是 (+ time *second*), 也就是调用函数需要的参数.
    • Note: 为什么不是直接通过调用的方式来实现这件事情呢? 反正通过尾递归 TOC 进行优化后应该差不多.

      不过这个我也不清楚就是了.

比如我们想要更加丰富一些的演奏方式, 比如可以按顺序弹奏一组音符:

 ;; Shift a list of notes
 ;; for example:
 ;; (shift-notes '(1 2 3)) => (3 2 1)
 (define shift-notes
   (lambda (notes)
     (flatten (list (cdr notes)
		     (car notes)))))

 (define my-loop
   (lambda (time notes)
     (let ((note-length (random '(1.0 1.5 2.0)))
	    (wait-time (random '(0.0 0.5 1.5 2.0))))
	(play-note time my-fm-synth
		   (car notes) ;; the first element of notes
		   80
		   (* *second* note-length))
	(callback (+ time (* note-length wait-time *second*))
		  'my-loop
		  (+ time (* note-length wait-time *second*))
		  (shift-notes notes)))))

 (my-loop (now) '(60 80 63 70 68 73))

代码说明: (shift-notes)

  • Lisp 语言, 一开始可以叫做 “LISt Processor” (Wikipedia), 其中的一个基本的元素就是 List, 即 (list exp1 exp2 ...), 或者 ='(sym1 sym2)=, 前者和后者的区别在于, 后者为 quote 语法糖, 被括起来的全部都会被作为符号储存, 而前者则会将表达式执行后以值的形式储存.
  • 对于 List, 可以通过 Linked List (Geeksforgeeks) 的方式来实现. 在历史上, 因为一开始的计算机实现了一个叫做高低位的储存方式, 即高位 car 一个数据 (指向), 低位 cdr 指向另外一个数据. 这样的两个数据通过 cons 和在一起: (cons 'car 'cdr).

    而 Linked List 则通过如下图所示的方式联系在一起: (图片来源 Wikipedia)

    https://upload.wikimedia.org/wikipedia/commons/6/6d/Singly-linked-list.svg

    于是一个 List 就和 (cons item1 (cons item2 ...)) 差不多. 所以我们对 List 做 carcdr 则会分别得到第一个元素和剩余元素的列表.

或者还能够再来一些奇怪的操作. 比如再加一个声音比较低的循环: 比如继续执行下面的这个代码:

(my-loop (now) '(30 40 43 48 33 20))

(注: 上面的声音都是我乱写的, 所以不敢保证好听. 不过和我的五子棋一样, 有一种黑猩猩的智慧. )

那么最后, 一个简单的问题, 该怎么让它停下来? 很简单, 我们只需要在上面的函数上做一些修改, 即可:

 (define *playing* #f)

 (define my-loop
   (lambda (time notes)
     (let (...)
	(play-note ...)
	(if *playing*
	    (callback ...)))))

和弦 Chords

那么来点 chords (和弦) 吧. 在农民的眼中, 和弦把几个按键一起按:

(define play-chord
  (lambda (chord)
    (map (lambda (pitch)
	     (play-note (now) my-fm-synth pitch 80 *second*))
	   chord)))

(play-chord '(72 76 69))

代码解释:

  • map 函数就是把一个 list 中的每个元素都拿出来, 然后过一边 func 函数, 得到的新的列表: (map func list).

    这个概念来自于数学中的 $A → B, a \mapsto b$ 这样的一个映射 (map).

    不过在这里, 你可以理解为对 chord 中的每个音符都执行一遍 play-note.

  • 不过也能够用类似 Ruby 中的 each 方法来实现:
       (define play-chord
         (lambda (time chord)
    	(for-each (lambda (pitch)
    		    (play-note time my-fm-synth pitch 80 *second*))
    		  chord)))
    

于是我们就可以来弹一些简单好玩的东西了:

 ;; Do `chords-loop-play` for `to-loop` times
 (define (iter-chords-loop time chords to-loop)
   (if (> to-loop 0)
	(begin (chords-loop-play time chords)
	       (callback (+ time (* (length chords) *second*))
			 'iter-chords-loop (+ time (* (length chords) *second*))
			 chords (- to-loop 1)))))

 ;; Play chords in row
 (define (chords-loop-play time chords)
   (if (not (eq? chords '()))
	(begin (play-chord time (car chords))
	       (callback (+ time *second*)
			 'chords-loop-play (+ time *second*) (cdr chords)))))

 ;; Play the chords in row for four times
 (define (chords-4-loop time chords)
   (iter-chords-loop time chords 4))

 (chords-4-loop (now) '((72 76 79)(69 72 76)(65 69 72)(67 71 74)))

代码解释:

  • (begin <exp1> <exp2> ...) 命令的作用就是将其参数的表达式依次执行.

    (注: 其实好像用 list 感觉也没有什么问题, 只是后者更加注重返回值罢了… )

  • (not (eq? chords '())) 的意思是, 若 chords 不为空列表.
  • 代码写得不怎么样, 没能体现 Lisp 的一个简洁和优美. 并且现在还是有点没有理解 callback 和直接调用的区别.

    之后会去看一下具体的实现 (大概), 估计可以理解更好一些. 目前来看, callback 更像是 JS 里面的 setTimeout(). 不是立刻调用而是延时调用.

  • Lisp 里面应该是有 DocString 一说的, =(define (two-times x) (* x 1))= 应该就是一个例子. 不过可能是我把 Common Lisp 和 Scheme 的风格搞错了吧…

采样 Sampler

那么如果我们想要使用一些自己录制的音频, 玩玩 Sample (比如 像广告一样呐 (bilibili), 好吧, 这个可能不算. 那么比如这个 纯人工声音采样器 (bilibili), 以及 OP-1 (bilibili). OP-1 那个合成器真的超级帅. )

这个思路就是, 将录制好的音频素材播放出来. (或者是, 经过一些处理过后导出. )

那么首先就是要将素材导入到 Extempore 中. 使用 Extempore 自带的 Sampler:

;; (sys:load "libs/core/instruments.xtm") ;; if you are not loaded
(sys:load "libs/external/instruments_ext.xtm")

接下来载入一些素材 (这里使用的是 Salamander Drum Kit, 即鼓的素材, 为音乐提供一个节奏. )

(define drum-path "/Users/liyiyang/Downloads/salamanderDrumkit/OH")

(define drum-path "/your/path/to/salamanderdrumkit/OH")

(make-instrument drums sampler)

(sampler-populate-slot drums (string-append drums-path "kick_OH_F_9.wav") *gm-kick* 0 0 0 1)
(sampler-populate-slot drums (string-append drums-path "snareStick_OH_F_9.wav") *gm-side-stick* 0 0 0 1)
(sampler-populate-slot drums (string-append drums-path "snare_OH_FF_9.wav") *gm-snare* 0 0 0 1)
(sampler-populate-slot drums (string-append drums-path "hihatClosed_OH_F_20.wav") *gm-closed-hi-hat* 0 0 0 1)
(sampler-populate-slot drums (string-append drums-path "hihatFoot_OH_MP_12.wav") *gm-pedal-hi-hat* 0 0 0 1)
(sampler-populate-slot drums (string-append drums-path "hihatOpen_OH_FF_6.wav") *gm-open-hi-hat* 0 0 0 1)
(sampler-populate-slot drums (string-append drums-path "loTom_OH_FF_8.wav") *gm-low-floor-tom* 0 0 0 1)
(sampler-populate-slot drums (string-append drums-path "hiTom_OH_FF_9.wav") *gm-hi-floor-tom* 0 0 0 1)
(sampler-populate-slot drums (string-append drums-path "crash1_OH_FF_6.wav") *gm-crash* 0 0 0 1)
(sampler-populate-slot drums (string-append drums-path "ride1_OH_FF_4.wav") *gm-ride* 0 0 0 1)
(sampler-populate-slot drums (string-append drums-path "china1_OH_FF_8.wav") *gm-chinese* 0 0 0 1)
(sampler-populate-slot drums (string-append drums-path "cowbell_FF_9.wav") *gm-cowbell* 0 0 0 1)
(sampler-populate-slot drums (string-append drums-path "bellchime_F_3.wav") *gm-open-triangle* 0 0 0 1)
(sampler-populate-slot drums (string-append drums-path "ride1Bell_OH_F_6.wav") *gm-ride-bell* 0 0 0 1)

于是一个 drums 便初始化完了. (注: 原文中使用的方法 set-sampler-index 应该是被弃用了. 参考 官方的文档, 现在使用的是 sampler-populate-slot. 其调用的参数: (sampler-populate-slot inst filename index offset lgth bank print?), 因为我们在这里将 print? 设为了 1, 所以在 Extempore 中应该可以看见导入的输出信息. )

A sampler is an instrument which stores chunks of audio which can be triggered—played back.

You can think of a sampler as a series of ‘slots’, each of which contains a sound file.

可以这样理解: 将 sampler 看作是对乐器按照音阶进行索引 index. 于是之后在 play-note 的时候, 就会去找相应的 index. (如果找不到的话, 就会去找最近的索引. )

于是就可以进行一个鼓的敲:

(play-note (now) drums *gm-open-triangle* 80 44100)

不过你肯定听不到声音. 因为我们并没有将鼓连接到我们的 DSP 输出上. 所以这个时候, 我们不妨直接做一个加法合成器 (Addictive Synthesis, 不过并不是真的就是了):

(注: 这样的代码并不够好, 只是能用的水平, 之后会介绍如何让其变得更加友好. )

(bind-func dsp:DSP
  (lambda (in time chan dat)
    (+ (my-fm-synth in time chan dat)
	 (drums in time chan dat))))

然后你应该就可以尝试替换 *gm-open-triangle* 来听听看各种声音了.

不过不要乱敲, 来点节拍: (metronome 节拍器)

(define *metro1* (make-metro 120))

(define drum-loop
  (lambda (time duration drum)
    (play-note (*metro1* time) drums drum 80 (*metro1* 'dur duration))
    (if #t
	  (callback (*metro1* (+ time duration)) 'drum-loop (+ time duration)
		    duration drum))))

(drum-loop (*metro1* 'get-beat) 1 *gm-hi-floor-tom*)

代码解释:

  • make-metro 函数返回了一个 BPM (Beats Per Min) 的一个节拍器. 这样的一个节拍器的功能就是为了方便我们对齐时间来进行打拍子.

    其中通过 (<metro> time) 的方式, 可以将当前 time 对齐到拍上. (<metro> 'dur duration) 可以将 duration 转换为持续时间.

于是可以来一段节奏:

;; patterns for example:
;; `((,*gm-kick* (x o x o x o x x x x))
;;   (,*gm-side-stick* (x o x o x x x)))
(define (syn-beat time patterns duration)
  (let ((shifted (map
		    (lambda (pattern) (shift-beat-pattern time pattern duration))
		    patterns)))
    (if #t
	  (callback (*metro1* (+ time duration))
		    'syn-beat (+ time duration) shifted duration))))

;; return the shifted pattern
(define (shift-beat-pattern time pattern duration)
  (let ((note (car pattern))
	  (sequences (shift-notes (cadr pattern))))
    (if (eq? 'x (car sequences))
	  (play-note time drums note 80 (*metro1* 'dur duration)))
    (list note sequences)))

;; let dance...
(syn-beat
 (*metro1* 'get-beat)
 `((,*gm-kick*          (x o o o o o o x o o x o o o o x))
   (,*gm-side-stick*    (o o o o x o o o o o o o x o o o))
   (,*gm-open-hi-hat*   (o o x o o o o o o o o o o o o o))
   (,*gm-closed-hi-hat* (x x o o x o x o x o x o x o x o)))
 .25)

代码解释:

  • Lisp 中有一个叫做 Backquote 的操作, 有点类似于 Ruby 中的 formatted string: =”Hello #{name}”. 通过 ~`(sym sym ,<exp> sym)~ 的形式来将 =<exp> 的值插入到列表中.

    这里通过 Backquote 的方式来得到变量的值而不是变量的名字.

于是可以通过配合鼓和合成器来创造节奏韵律了. 不过这部分, 我做得太难听了, 所以就不把代码放上来了.

接下来?

我觉得应该分为几种方式:

之后有想法再更新.