用Ruby制作音乐
立即解锁
发布时间: 2025-08-21 02:25:25 阅读量: 1 订阅数: 2 


实用Ruby项目:探索编程的无限可能
### 用 Ruby 制作音乐
#### 1. 项目与兴趣
在探索编程项目时,最好的项目往往源于个人兴趣。即便一些概念既实用又令人兴奋,但由自身兴趣驱动的项目才更具吸引力和探索价值。大家可以在 Ruby 社区分享自己的项目成果,无论是写博客、发布作品还是进行演讲,都能让更多人看到你的努力。
#### 2. 音乐编程的开端
早期在麻省理工学院,Peter Samson 努力让 TX0 计算机播放音乐,其成果让特定人群感到惊叹。对于作者而言,最初吸引自己的并非游戏,也不是编程语言,而是从 PC 扬声器中传出的 12 首生硬编码歌曲的声音。
现代计算机音乐系统常被设计成完整的应用程序、编程语言或环境,这让程序员在使用自己喜欢的编程语言制作音乐时,缺乏合适的库。不过,有一些强大的专业环境值得关注,如 SuperCollider、Impromptu、ChucK 和 cmusic :
- SuperCollider:[http://supercollider.sourceforge.net/](http://supercollider.sourceforge.net/)
- Impromptu:[http://impromptu.moso.com.au/](http://impromptu.moso.com.au/)
- ChucK:[http://chuck.cs.princeton.edu/](http://chuck.cs.princeton.edu/)
- cmusic:[www.crca.ucsd.edu/cmusic/](www.crca.ucsd.edu/cmusic/)
接下来,我们将用 Ruby 从头构建一个音乐系统,目标是使用 Ruby、易于上手且便于用户扩展。虽然音乐理论不在本次讨论范围内,但这个系统足以让你开启音乐编程之旅,后续再深入学习其他知识也不迟。
#### 3. MIDI:音乐的词汇表
音乐本质上是声波,计算机音乐也不例外。然而,用声音的词汇来描述或创作音乐并非最佳选择。直接控制声波合成虽能解锁音乐的所有可能性,但多数时候会让人感到无从下手。
幸运的是,计算机音乐发展出了一种与传统音乐记谱法相近的标准——乐器数字接口(Musical Instrument Digital Interface,简称 MIDI)。MIDI 涵盖设备规格、有线协议和抽象软件 API,我们主要关注其抽象 API 的一小部分。
MIDI 有三个基本操作:
- **音符开启(Note On)**:开始播放音符。
- **音符关闭(Note Off)**:停止播放音符。
- **程序改变(Program Change)**:切换乐器。
这些操作需要通道号(区分乐器)、音符编号和速度。16 个 MIDI 通道各对应一个乐器,速度表示按键或释放的力度,范围在 0 到 127 之间。音符编号用于标识特定音符,范围同样是 0 到 127。与传统音乐记谱法(A、B、C、D、E、F、G)相比,音符编号起初可能令人困惑,但对程序员来说更方便。例如,中央 C 的音符编号是 60,每增加 1 表示音阶上升半音,增加 12 表示升高一个八度。
要以最大力度在第一个乐器上演奏中央 C,可按以下步骤操作:
1. 向通道 0 发送音符开启消息,音符编号为 60,速度为 127。
2. 短时间后,向通道 0 发送音符关闭消息,音符编号为 60,速度为 127。
需要注意的是,音符关闭的速度可以与开启速度不同,部分合成器会忽略音符关闭速度,而使用该速度的合成器会将其表示为音符释放的快慢。
程序改变操作允许在众多乐器中切换。由于 MIDI 仅支持 16 个通道,并非所有乐器都能同时使用。该命令通过乐器预设编号和通道号将乐器绑定到指定通道。
不过,不要局限于 MIDI,还有一些强大的声音合成系统能让音乐创作超越简单的音符开启和关闭指令。
#### 4. 与 C 语言交互并发声
由于 MIDI 非常方便,我们可以用它来发声。各大操作系统都提供了 MIDI 支持,但需要用 Ruby 与这些系统库进行交互。这些库通常用 C 语言编写,让 Ruby 与它们通信可能会有挑战。
传统方法是编写一个 C 文件,将其与 MIDI 库接口并链接,再使用 Ruby C API 将功能以对象形式暴露给 Ruby。这种方法很灵活,但需要处理编写和编译 C 代码的麻烦,并且分发也更困难,因为用户可能需要自行编译绑定代码。
幸运的是,Ruby 提供了动态链接库 Ruby DL,可直接从 Ruby 与 C 库交互。需要注意的是,Ruby DL 有新版本正在开发中,版本 2 将修复一些旧版本的问题,而本文代码基于版本 1(与 Ruby 1.8 系列捆绑的版本)。
接下来,我们将为三大主流操作系统(Windows 的多媒体 API、OS X 的 CoreMIDI 和 Linux 的高级 Linux 声音架构 [ALSA])构建 Ruby MIDI 绑定。所有代码应先放在名为 `music.rb` 的文件中,最后再添加一个额外的文件。
#### 5. 共享代码
不同平台的 MIDI 接口会共享一些代码。除了各自的设置和清理代码外,每个操作系统特定的接口都需要实现 `message` 方法,该方法最多接受三个整数并将其转换为有效的 MIDI 消息。
MIDI 消息有两种字节:
- **状态字节**:最高位为 1,范围是 128 - 255,本质上是命令。
- **数据字节**:最高位为 0,范围是 0 - 127。
每个状态字节需要相应数量的数据字节作为参数。为节省带宽,MIDI 设计者采用了一个技巧:部分状态字节用于编码命令影响的 MIDI 通道。例如,`1001****` 中,前四位编码状态类型(音符开启),后四位编码受影响的通道。状态字节 144 表示“通道 0 音符开启”,145 表示“通道 1 音符开启”,128 表示“通道 0 音符关闭”。
需要注意的是,MIDI 面向音乐家,其术语通常从 1 开始计数,通道和乐器编号为 1 - 16 和 1 - 128,而我们作为程序员,使用更自然的 0 - 15 和 0 - 127 编号。如果声音效果异常,要检查是否因编号转换导致错误。
音符开启和关闭消息后跟随两个数据字节,第一个表示要演奏的音符(0 - 127),第二个表示速度。速度为 0 的音符开启消息有时也表示音符关闭。程序改变操作只需一个数据字节,用于指定要映射到指定通道的 128 种乐器中的一种。
以下是所有 MIDI 接口共享的代码:
```ruby
require 'dl/import'
class LiveMIDI
ON = 0x90
OFF = 0x80
PC = 0xC0
def initialize
open
end
def note_on(channel, note, velocity=64)
message(ON | channel, note, velocity)
end
def note_off(channel, note, velocity=64)
message(OFF | channel, note, velocity)
end
def program_change(channel, preset)
message(PC | channel, preset)
end
end
```
代码中,`0x90`、`0x80` 和 `0xC0` 是十六进制数,分别表示通道 0 的音符开启、音符关闭和程序改变的魔法值。通过按位或运算符将它们与通道号组合,生成完整的状态字节。音符和速度直接传递给 `message` 方法,默认速度为 64。
不同操作系统需要实现 `open` 和 `message` 方法(最好也有 `close` 方法)。由于不同平台的代码不兼容,我们将根据操作系统使用开放类直接将代码加载到 `LiveMIDI` 类中:
```ruby
if RUBY_PLATFORM.include?('mswin')
class LiveMIDI
# Windows code here
end
elsif RUBY_PLATFORM.include?('darwin')
class LiveMIDI
# Mac code here
end
elsif RUBY_PLATFORM.include?('linux')
class LiveMIDI
# Linux code here
end
else
raise "Couldn't find a LiveMIDI implementation for your platform"
end
```
如果找不到匹配的实现,代码将抛出异常。
#### 6. 与 Windows 多媒体接口交互
我们先从 Windows 系统开始。代码如下:
```ruby
class LiveMIDI
module C
extend DL::Importable
dlload 'winmm'
end
end
```
这里使用 Ruby 的开放类重新打开 `LiveMIDI` 类,并在其中定义了一个名为 `C` 的模块。通过 `extend DL::Importable`,我们可以方便地访问 `DL::Importable` 的类方法。
`extend` 和 `include` 是 Ruby 中两个特殊的关键字,`extend` 将模块的类方法注入目标,`include` 将模块的实例方法注入目标。
接下来,我们使用 `extern` 方法声明 C 函数:
```ruby
class LiveMIDI
module C
extend DL::Importable
dlload 'winmm'
extern "int midiOutOpen(HMIDIOUT*, int, int, int, int)"
extern "int midiOutClose(int)"
extern "int midiOutShortMsg(int, int)"
end
end
```
每个 `extern` 调用接受一个包含 C 函数签名的字符串。对于有 C 编程经验的人来说,这些签名应该很熟悉。签名中,第一个单词是函数的返回类型,第二个单词是函数名,括号内是参数类型。
虽然实际 API 中使用了自定义的 C 类型,但我们可以忽略大部分参数,将它们视为 `int` 类型。对于有指针概念的 C 程序员,Ruby DL 只关心是否为指针,不关心具体指针类型。
以下是 `open` 方法的实现:
```ruby
class LiveMIDI
def open
@device = DL.malloc(DL.sizeof('I'))
C.midiOutOpen(@device, -1, 0, 0, 0)
end
end
```
`DL.malloc` 用于分配内存,返回一个指针。`-1` 参数指示系统选择默认 MIDI 设备,其他参数可忽略并传入 0 值。为简洁起见,这里未检查 `C.midiOutOpen` 的返回码。
`close` 方法用于结束与 MIDI 子系统的连接:
```ruby
class LiveMIDI
def close
C.midiOutClose(@device.ptr.to_i)
end
end
```
当 Ruby 对象被垃圾回收时,分配的内存会自动释放,无需手动调用 `free` 函数。
最后是 `message` 方法:
```ruby
class LiveMIDI
def message(one, two=0, three=0)
message = one + (two << 8) + (three << 16)
C.midiOutShortMsg(@device.ptr.to_i, message)
end
end
```
该方法将最多三个参数组合成一个无符号四字节整数,作为 MIDI 消息发送。
以下是一个简单的测试代码:
```ruby
midi = LiveMIDI.new
midi.note_on(0, 60, 100)
sleep(1)
midi.note_off(0, 60)
sleep(1)
midi.program_change(1, 40)
midi.note_on(1, 60, 100)
sleep(1)
midi.note_off(1, 60)
```
运行这段代码,你应该能听到钢琴演奏的中央 C,然后在程序改变后听到小提琴演奏的中央 C。需要注意的是,虽然没有严格规定乐器编号与乐器的对应关系,但有一个名为 General MIDI 的标准,遵循该标准时,钢琴编号为 0,小提琴编号为 40。
### 流程图
```mermaid
graph TD;
A[开始] --> B[初始化 LiveMIDI 对象];
B --> C[发送音符开启消息];
C --> D[等待 1 秒];
D --> E[发送音符关闭消息];
E --> F[等待 1 秒];
F --> G[发送程序改变消息];
G --> H[发送音符开启消息];
H --> I[等待 1 秒];
I --> J[发送音符关闭消息];
J --> K[结束];
```
### 表格
| 操作 | 描述 | 参数 |
| --- | --- | --- |
| 音符开启(Note On) | 开始播放音符 | 通道号、音符编号、速度 |
| 音符关闭(Note Off) | 停止播放音符 | 通道号、音符编号、速度 |
| 程序改变(Program Change) | 切换乐器 | 通道号、乐器预设编号 |
#### 7. 与 OS X 的 CoreMIDI 接口交互
在 OS X 系统上,我们同样要实现与 MIDI 系统的交互。虽然原文未给出具体代码,但我们可以按照之前的思路,使用 Ruby DL 来完成这个任务。
首先,我们需要加载 CoreMIDI 相关的库。假设我们有对应的 C 函数可以调用,以下是一个简单的示例框架:
```ruby
class LiveMIDI
module C
extend DL::Importable
dlload 'CoreMIDI' # 假设库名为 CoreMIDI
extern "int midiOpenFunction(...)" # 假设的打开函数
extern "int midiCloseFunction(...)" # 假设的关闭函数
extern "int midiSendMessageFunction(...)" # 假设的发送消息函数
end
def open
# 调用打开函数
C.midiOpenFunction(...)
end
def close
# 调用关闭函数
C.midiCloseFunction(...)
end
def message(one, two=0, three=0)
# 处理消息并调用发送函数
message = one + (two << 8) + (three << 16)
C.midiSendMessageFunction(message)
end
end
```
在实际使用中,需要根据 CoreMIDI 的具体 API 来替换上述代码中的函数名和参数。
#### 8. 与 Linux 的 ALSA 接口交互
对于 Linux 系统,我们使用高级 Linux 声音架构(ALSA)。同样,我们使用 Ruby DL 来与 ALSA 的 C 库进行交互。
以下是一个简单的示例代码框架:
```ruby
class LiveMIDI
module C
extend DL::Importable
dlload 'alsa' # 假设库名为 alsa
extern "int alsaOpenFunction(...)" # 假设的打开函数
extern "int alsaCloseFunction(...)" # 假设的关闭函数
extern "int alsaSendMessageFunction(...)" # 假设的发送消息函数
end
def open
# 调用打开函数
C.alsaOpenFunction(...)
end
def close
# 调用关闭函数
C.alsaCloseFunction(...)
end
def message(one, two=0, three=0)
# 处理消息并调用发送函数
message = one + (two << 8) + (three << 16)
C.alsaSendMessageFunction(message)
end
end
```
实际使用时,要根据 ALSA 的真实 API 来调整函数名和参数。
#### 9. 不同平台代码的整合与使用
我们已经分别为 Windows、OS X 和 Linux 系统实现了 MIDI 接口。现在,我们可以将这些代码整合到之前的 `LiveMIDI` 类中。
```ruby
require 'dl/import'
class LiveMIDI
ON = 0x90
OFF = 0x80
PC = 0xC0
def initialize
open
end
def note_on(channel, note, velocity=64)
message(ON | channel, note, velocity)
end
def note_off(channel, note, velocity=64)
message(OFF | channel, note, velocity)
end
def program_change(channel, preset)
message(PC | channel, preset)
end
if RUBY_PLATFORM.include?('mswin')
module C
extend DL::Importable
dlload 'winmm'
extern "int midiOutOpen(HMIDIOUT*, int, int, int, int)"
extern "int midiOutClose(int)"
extern "int midiOutShortMsg(int, int)"
end
def open
@device = DL.malloc(DL.sizeof('I'))
C.midiOutOpen(@device, -1, 0, 0, 0)
end
def close
C.midiOutClose(@device.ptr.to_i)
end
def message(one, two=0, three=0)
message = one + (two << 8) + (three << 16)
C.midiOutShortMsg(@device.ptr.to_i, message)
end
elsif RUBY_PLATFORM.include?('darwin')
# OS X 代码
module C
extend DL::Importable
dlload 'CoreMIDI'
# 具体 extern 函数
end
def open
# 具体打开实现
end
def close
# 具体关闭实现
end
def message(one, two=0, three=0)
# 具体消息处理
end
elsif RUBY_PLATFORM.include?('linux')
# Linux 代码
module C
extend DL::Importable
dlload 'alsa'
# 具体 extern 函数
end
def open
# 具体打开实现
end
def close
# 具体关闭实现
end
def message(one, two=0, three=0)
# 具体消息处理
end
else
raise "Couldn't find a LiveMIDI implementation for your platform"
end
end
```
这样,我们就可以根据不同的操作系统自动选择合适的 MIDI 接口。
#### 10. 总结与拓展
通过以上步骤,我们成功地使用 Ruby 构建了一个跨平台的 MIDI 音乐系统。这个系统可以让我们在不同的操作系统上使用 Ruby 来控制 MIDI 设备,实现音乐的播放和乐器的切换。
不过,MIDI 只是计算机音乐的一种方式,还有许多其他强大的声音合成系统可以让我们创造出更复杂、更丰富的音乐。例如,之前提到的 SuperCollider、Impromptu、ChucK 和 cmusic 等系统,它们提供了更高级的功能和更灵活的编程接口。
如果你对音乐编程感兴趣,可以进一步探索这些系统,学习更多的音乐理论和编程技巧,创造出属于自己的独特音乐作品。
### 流程图
```mermaid
graph TD;
A[选择操作系统] --> B{Windows};
B -- 是 --> C[使用 Windows 代码];
B -- 否 --> D{OS X};
D -- 是 --> E[使用 OS X 代码];
D -- 否 --> F{Linux};
F -- 是 --> G[使用 Linux 代码];
F -- 否 --> H[抛出异常];
C --> I[初始化 LiveMIDI 对象];
E --> I;
G --> I;
I --> J[进行 MIDI 操作];
J --> K[结束];
```
### 表格
| 操作系统 | 库名 | 主要函数 |
| --- | --- | --- |
| Windows | winmm | midiOutOpen, midiOutClose, midiOutShortMsg |
| OS X | CoreMIDI | 待确定 |
| Linux | alsa | 待确定 |
0
0
复制全文
相关推荐









