说明: 本文是根据 七周七语言 (卷 2) https://book.douban.com/subject/26921107/ 中的一个 Lua 示例项目略加修改而来.
目录
项目介绍
环境准备
项目结构和代码
从单个音符到乐曲
多声道乐曲播放
项目介绍
这个项目通过 Lua 调用一个用 C++ 实现的 MIDI 接口库 RtMidi 来控制一个 MIDI 合成器 播放自定义格式的乐谱, 来演示 Lua 跟 C 之间的代码交互.
首先用 C++ 作为宿主程序, 把 Lua 解释器嵌入其中, 接着用 C++ 封装了一个可供 Lua 脚本调用的 C++ 函数 midi_send, 这个函数通过调用 RtMidi 库中的 API 向 MIDI 合成器 发送控制命令来播放音乐, 而音乐的来源则是我们用 Lua 自定义格式的乐谱, 由 Lua 将其解析转换为 MIDI 合成器 能够识别的格式.
环境准备
这个项目是跨平台的, 可以同时支持 Windows/macOS/Linux 平台, 本文只提供 macOS 上的实现, 其他两个平台也很简单, 其中 Lua 部分的代码不需要改变.
需要安装以下环境
包管理器 brew;
编译工具 XCode 或 gcc;
C sound 项目的源码跟 RtMidi;
Lua 和 CMake;
macOS 下的 MIDI 合成器: SimpleSynth
我的环境上只缺 C sound 项目, RtMidi 以及 SimpleSynth, 前两个用 brew 安装, 命令如下:
添加 C sound 项目的源代码
- Air:midi admin$ brew tap kunstmusik/csound
- Updating Homebrew...
- ==> Auto-updated Homebrew!
- Updated 2 taps (homebrew/core and homebrew/cask).
- ==> New Formulae
- azure-storage-cpp i386-elf-binutils maven@3.5 node@10 shellz um
- fluxctl i386-elf-gcc mesa Ruby@2.4 sourcedocs
- ==> Updated Formulae
- bdw-gc dartsim hebcal mitie sec
- c-ares
- ......
- ==> Deleted Formulae
- corebird kibana@4.4 maven@3.0 maven@3.1 nethack4 Ruby@2.2 taylor tcptrack
- Error: Failed to import: /usr/local/Homebrew/Library/Taps/benswift/homebrew-extempore/extempore-llvm341.rb
- extempore-llvm341: undefined method `sha1' for #<Class:0x000000011189d728>
- ==> Tapping kunstmusik/csound
- Cloning into '/usr/local/Homebrew/Library/Taps/kunstmusik/homebrew-csound'...
- remote: Enumerating objects: 7, done.
- remote: Counting objects: 100% (7/7), done.
- remote: Compressing objects: 100% (7/7), done.
- remote: Total 7 (delta 0), reused 3 (delta 0), pack-reused 0
- Unpacking objects: 100% (7/7), done.
- Tapped 3 formulae (34 files, 28.1KB).
- Air:midi admin$
安装 RtMidi
- Air:midi admin$ brew install rtmidi
- ==> Downloading https://homebrew.bintray.com/bottles/rtmidi-3.0.0.high_sierra.bottle.tar.gz
- ######################################################################## 100.0%
- ==> Pouring rtmidi-3.0.0.high_sierra.bottle.tar.gz
- /usr/local/Cellar/rtmidi/3.0.0: 8 files, 196.6KB
- Air:midi admin$
而 SimpleSynth 可以直接到它的官网去下载: SimpleSynth http://notahat.com/simplesynth , 下载回来后把它运行起来, 用它来充当 MIDI 合成器.
环境准备 OK, 接下来就正式开始项目了.
项目结构
我们这个项目很简单, 就是 3 部分:
C++ 宿主程序 play.cpp, 创建 Lua 解释器并执行自定义格式的乐谱;
用 Lua 写的模块, 负责对解析乐谱, 跟 MIDI 合成器交互;
用 Lua 写的自定义格式的乐谱;
首先为项目创建一个目录 midi, 把所有的项目代码都放在这里.
C++ 宿主程序 play.cpp
在 midi 目录下创建一个 C++ 文件 play.cpp, 内容如下:
- extern "C"
- {
- #include "lua.h"
- #include "lauxlib.h"
- #include "lualib.h"
- }
- int main(int argc, const char* argv[])
- {
- lua_State* L = luaL_newstate();
- luaL_openlibs(L);
- luaL_dostring(L, "print('Hello world!')");
- lua_close(L);
- return 0;
- }
代码分析
基础函数库: 其中 #include "lua.h" 引入 Lua 的基础函数库, 它提供如下基础函数:
创建新 Lua 环境的函数;
调用 Lua 函数的函数;
读写环境中的全局变量的函数;
注册供 Lua 语言调用的新函数的函数;
...
辅助函数库: #include "lauxlib.h" 引入辅助函数库, 它使用 lua.h 提供的基础 API 来提供更高层次的抽象, 特别是对标准库用到的相关机制进行抽象.
标准函数库: #include "lualib.h" 引入标准函数库, 所有的标准库都被组织成不同的包.
用
lua_State* L = luaL_newstate();
创建一个 Lua 解释器, 然后用
luaL_openlibs(L);
打开标准库, 之后就可以用
luaL_dostring(L, "print('Hello world!')");
给 Lua 解释器发送一些 Lua 代码让它去执行.
首次编译
接着我们就可以用 CMake 来构建项目了, 在 midi 目录下创建一个名为 CMakeLists.txt 的文件, 内容如下:
- cmake_minimum_required (VERSION 2.8)
- project (play)
- add_executable (play play.cpp)
- target_link_libraries (play lua)
- include_directories (/usr/local)
- link_directories ("/usr/local")
然后执行 cmake
- Air:midi admin$ cmake .
- -- Configuring done
- -- Generating done
- -- Build files have been written to: /Users/admin/code-staff/lua+c/midi
- Air:midi admin$
接着执行 make , 提示找不到 lua.h
- Air:midi admin$ make
- [ 50%] Linking CXX executable play
- ld: library not found for -llua
- clang: error: Linker command failed with exit code 1(use -v to see invocation)
- make[2]: *** [play] Error 1
- make[1]: *** [CMakeFiles/play.dir/all] Error 2
- make: *** [all] Error 2
- Air:midi admin$
既然找不到 lua 库的路径, 那么看看它在哪里:
- Air:midi admin$ find /usr/local -name "liblua*"
- /usr/local/lib/liblua5.3.4.dylib
- /usr/local/lib/liblua.a
- /usr/local/Cellar/lua/5.2.4_3/lib/liblua.5.2.dylib
- /usr/local/Cellar/lua/5.2.4_3/lib/liblua.5.2.4.dylib
- /usr/local/Cellar/lua/5.2.4_3/lib/liblua.dylib
- /usr/local/Cellar/lua/5.3.4_3/lib/liblua.5.3.dylib
- /usr/local/Cellar/lua/5.3.4_3/lib/liblua.5.3.4.dylib
- /usr/local/Cellar/lua/5.3.4_3/lib/liblua.dylib
- /usr/local/Cellar/lua/5.3.4_3/lib/liblua.a
- Air:midi admin$
在 CMakeList.txt 中增加路径说明:
- cmake_minimum_required (VERSION 2.8)
- project (play)
- add_executable (play play.cpp)
- target_link_libraries (play lua)
- include_directories (/usr/local/Cellar/lua/5.3.4_3/)
- link_directories ("/usr/local/Cellar/lua/5.3.4_3/")
再次执行 make, 结果还是同样的错误, 因为对 CMake 不太熟悉, 于是查了很多资料, 试验了很多方法, 结果还是不行, 后来一想, 算了, 不用 CMake 了, 反正这个项目也很简单, 就这么一个 C++ 文件, 直接用命令行编译吧, 命令行如下:
- Air:midi admin$ g++ play.cpp -o play -I/usr/local -L/usr/local -llua
- Air:midi admin$
- Air:midi admin$ ./play
- Hello world!
- Air:midi admin$
结果顺利通过, OK, 终于可以进行下一步了
引入 RtMidi 库
接着就要引入 RtMidi 库, 对 MIDI 合成器 进行操作了, 首先修改 play.cpp 代码如下:
- extern "C"
- {
- #include "lua.h"
- #include "lauxlib.h"
- #include "lualib.h"
- }
- #include "RtMidi.h"
- static RtMidiOut midi;
- int main(int argc, const char* argv[])
- {
- if (argc <1 ) {return -1;}
- unsigned int ports = midi.getPortCount();
- if (ports < 1 ) {return -1;}
- midi.openPort(0);
- lua_State* L = luaL_newstate();
- luaL_openlibs(L);
- lua_pushcfunction(L, midi_send);
- lua_setglobal(L, "midi_send");
- //luaL_dostring(L, "print('Hello world!')");
- luaL_dofile(L, argv[1]);
- lua_close(L);
- return 0;
- }
代码分析
这两行代码引入 RtMidi 库, 其中 RtMidiOut 对象就是我们后续的程序中用来跟 MIDI 合成器进行交互的接口, 将其放入一个全局变量 midi 中, 后面就可以通过这个全局变量 midi 来引用 RtMidi 库的函数:
- #include "RtMidi.h"
- static RtMidiOut midi;
接着通过命令行输入的参数个数 argc 来判断用户是否输入正确, 若否则直接退出.
下面就是对 RtMidi 库的函数来对 MIDI 合成器进行操作, 使用了两个函数:
- midi.getPortCount()
- midi.openPort()
关于这两个函数的详细定义可以在 RtMidi 官网教程 RtMidiOut Class Reference 查到.
它们具体的工作就是寻找正在运行中的 MIDI 合成器 (也就是我们之前运行起来的 SimpleSynth).
然后是这两行代码:
- lua_pushcfunction(L, midi_send);
- lua_setglobal(L, "midi_send");
首先用 lua_pushcfunction 注册一个用来播放音乐的 C++ 函数 midi_send, 函数 lua_pushcfunction 会获取一个指向函数 midi_send 的指针 (也就是 L), 然后在 Lua 中创建一个 function 类型, 代表待注册的函数 midi_send. 一旦把这个函数类型的值压入 Lua 栈中完成注册, 这个 C++ 函数 midi_send 就可以像其他 Lua 函数一样被调用了.
然后再用 lua_setglobal 把这个函数类型的值赋给全局变量 midi_send, 完成这两步, 我们就可以在 Lua 脚本中使用新函数 midi_send 了.
注意: 第一个 midi_send 是在 C++ 中定义的函数, 第二个 midi_send 是提供给 Lua 使用的函数名, 这两个名字可以不一样.
最后我们把代码行:
luaL_dostring(L, "print('Hello world!')");
换成了:
luaL_dofile(L, argv[1]);
因为函数 luaL_dofile 可以从文件中加载 Lua 代码, 我们从命令行获取用户输入的 Lua 文件名, 例如:
play song.lua
这样就可以灵活地把乐曲放在 song.lua 中, 而不需要每次改写 Lua 乐曲时都去重新编译 C++ 代码了.
MIDI 相关知识
要想在 MIDI 合成器 中播放一个音符, 需要给它发送两个 MIDI 消息:
Note On 消息
Note Off 消息
MIDI 标准给每个消息编了号, 并规定每个消息接受 2 个参数:
音符
速率
这样我们的 midi_send 函数就需要使用 3 个参数:
消息编号
音符
速率
例如如下 Lua 代码就代表一个 Note On 消息, 音符为 60, 速率为 96:
midi_send(144, 60, 96)
执行这行代码后, 144, 60, 96 这 3 个数字会被入栈, 然后开始执行 C++ 函数. 按照 Lua 编写 C API 的约定, 我们可以根据这些参数在栈内的位置来获取它们. Lua 栈顶的索引是 -1, 对应着最后入栈的数字 96.
编写 midi_send 函数
前面我们虽然注册了 midi_send 函数, 但是还没有编写具体的代码, 根据 MIDI 合成器对消息格式的要求, 可以写出如下的 midi_send 函数定义代码:
- int midi_send(lua_State* L)
- {
- double status = lua_tonumber(L, -3);
- double data1 = lua_tonumber(L, -2);
- double data2 = lua_tonumber(L, -1);
- std::vector<unsigned char> message(3);
- message[0] = static_cast<unsigned char>(status);
- message[1] = static_cast<unsigned char>(data1);
- message[2] = static_cast<unsigned char>(data2);
- midi.sendMessage(&message);
- return 0;
- }
记得将其放在 play.cpp 中 main 函数的前面.
代码分析
我们知道 Lua 通过一个简单的栈模型来实现跟 C/C++ 代码的交互, 所以下面这 3 行代码就是把我们提供的 3 个 MIDI 合成器 要用到的参数入栈:
- double status = lua_tonumber(L, -3);
- double data1 = lua_tonumber(L, -2);
- double data2 = lua_tonumber(L, -1);
然后要把刚才入栈的数字转换成 RtMidi 能够读取的格式, 并用 midi.sendMessage 函数把它们传递给 MIDI 合成器, 下面这几行代码就是做这些工作的:
- std::vector<unsigned char> message(3);
- message[0] = static_cast<unsigned char>(status);
- message[1] = static_cast<unsigned char>(data1);
- message[2] = static_cast<unsigned char>(data2);
- midi.sendMessage(&message);
说明: 这是 C++ 形式的写法, 实际上对于 midi.sendMessage 函数, RtMidi 还提供了一个 C 形式的原型, 我们也可以按照 C 的形式去写这段代码.
因为我们在代码中引入了 RtMidi 库, 所以需要在 CMakeLists.txt 文件中增加相关说明 以便链接器能够正确把 RtMidi 库链接进去, 如下:
target_link_libraries (play lua RtMidi)
不过对我来说, 需要修改的就是在编译命令行上增加 lRtMidi 再重新执行, 如下:
g++ play.cpp -o play -I/usr/local -L/usr/local -llua -lRtMidi
一切顺利, 编译通过.
自定义格式乐谱
前面说了, 我们第一次只打算播放一个音符, 我们把这个简单的乐谱放在 Lua 文件 one_note_song.lua 中, 其代码如下:
- NOTE_DOWN = 0x90
- NOTE_UP = 0x80
- VELOCITY = 0x7f
- function play(note)
- midi_send(NOTE_DOWN, note, VELOCITY)
- while os.clock() <2 do end
- midi_send(NOTE_UP, note, VELOCITY)
- end
- play(60)
代码分析
首先, 定义消息编号跟速率, 接着写一个用来播放的函数 play, 在其中调用我们事先写好的 C++ 函数 midi_send 来播放, 中间的这行代码:
while os.clock() < 2 do end
用来控制播放时间, 我们这里选择了 2 秒.
首次播放
确保 SimpleSynth 正在运行, 然后执行如下命令:
- Air:midi admin$ ./play one_note_song.lua
- Air:midi admin$
就会听到中音 C 持续播放 2 秒钟.
从单个音符到乐曲
前面说过, 我们的项目分 3 部分, 不过我们只实现了其中的 1(C++ 宿主程序), 接下来我们就把剩下的两部分完成.
自定义格式的乐谱
首先, 我们用 Lua 来定义一种乐谱格式, 创建一个新文件 good_morning_to_all.lua, 内容如下:
- notes = {
- 'D4q',
- 'E4q',
- 'D4q',
- 'G4q',
- 'Fs4h'
- }
这是一个 Lua 的 table, 它代表一首歌曲的乐谱, 使用一种类似于 ABC 记谱法 的格式来标识乐谱, 具体来说就是用 C,D,E,F,G,A,B 来表示 1,2,3,4,5,6,7, 再加上一些额外的符号, 可以完整地表示一段乐谱.
我们的自定义格式乐谱中每个字符串表示 3 个部分, 以 D4q 为例:
音名: D, 可以有 C,Cs,D,Ds,E,F,Fs,G,Gs,A,As,B;
音度:
4
, 又叫音程, 确定乐曲基准音, 可以有 0~
12
;
音长: q, 可以有 h, q, ed, e, s.
而 Fs4h 中的 Fs 表示 升 F.
我们需要有一个乐谱解析函数, 来把我们乐谱中的这些字符串解析转换成 MIDI 的音符编号跟长度, 也就是 midi_send(144, 60, 96) 函数中的 音符 和 速率 参数, 我们新建一个文件 notation.lua, 内容如下:
- local function note(letter, octave)
- local notes = {
- C = 0, Cs = 1, D = 2, Ds = 3, E = 4,
- F = 5, Fs = 6, G = 7, Gs = 8, A = 9,
- As = 10, B = 11,
- }
- local notes_per_octave = 12
- return (octave + 1) * notes_per_octave + notes[letter]
- end
- local tempo = 100
- local function duration(value)
- local quarter = 60 / tempo
- local durations = {
- h = 2.0, q = 1.0, ed = 0.75, e = 0.5, s = 0.25,
- }
- return durations[value] * quarter
- end
- local function parse_note(s)
- local letter, octave, value = string.match(s, "([A-Gs]+)(%d )(%a )")
- if not (letter and octave and value) then return nil end
- return {
- note = note(letter, octave),
- duration = duration(value)
- }
- end
代码分析
首先分析函数 parse_note(s), 它用来实现从乐谱到 MIDI 数据的解析转换.
代码行:
local letter, octave, value = string.match(s, "([A-Gs]+)(%d )(%a )")
使用 Lua 的 string.match 函数进行模式匹配和捕获, 遇到 D4q 这样的字符串, 首先它会进行如下匹配:
将 D 匹配到模式 ([A-Gs]+);
将
4
匹配到 (%d );
将 q 匹配到 (%a ),
接着它会返回匹配成功的子串, 也就是返回 D, 4, q, 将其分别赋给局部变量 letter, octave, value, 最后再用 letter 和 octave 构造 MIDI 音符, 用 value 构造 MIDI 速率, 也就是这段返回代码:
- return {
- note = note(letter, octave),
- duration = duration(value)
- }
在这段代码中用到两个新函数 note(letter, octave) 和 duration(value), 我们继续分析这两个函数.
函数 note(letter, octave) 首先定义了一个音阶表 notes, 里面根据每个音名跟 MIDI 音符 的对应关系设置一个数值, 再定义一个 notes_per_octave, 最后根据公式来计算实际的 MIDI 音符 数值:
return (octave + 1) * notes_per_octave + notes[letter]
这样我们就可以根据 音名 和 音度 得到 MIDI 音符.
最后是函数 duration(value), 它根据音长来计算 MIDI 速率, 同样定义了一个表 durations, 里面用不同的字符表示不同的音长设置, 还定义默认节拍 tempo, 作为计算基准, 最终根据公式:
return durations[value] * quarter
计算得到用秒表示的 MIDI 速率.
这样, MIDI 合成器需要的参数就都准备好了, 接下来就是播放相关的代码, 需要修改 good_morning_to_all.lua, 遍历其中乐谱表 notes 的每个音符, 新增代码如下:
- scheduler = require 'scheduler'
- notation = require 'notation'
- function play_song()
- for i = 1, #notes do
- local symbol = notation.parse_note(notes[i])
- print("note:", symbol.note, "duration:", symbol.duration)
- notation.play(symbol.note, symbol.duration)
- end
- end
- scheduler.schedule(0.0, coroutine.create(play_song))
- scheduler.run()
代码分析
函数 play_song() 所做的就是遍历乐谱表 notes, 将其中的每个字符串解析转换为 note 和 duration, 然后传递给函数 notation.play.
这里使用了一个新的调度库 scheduler, 是利用 Lua 的 协程 实现的, 关于 协程 的内容相对来说要复杂一些, 所以这里我们只使用, 不对其做详细讲解, 如果想要了解 协程, 可以参考我以前写过的一篇介绍 协程 的文章 从零开始写一个武侠冒险游戏 - 5 - 使用协程.
而 notation.lua 中的新增代码如下:
增加在开头位置的代码
- local scheduler = require 'scheduler'
- local NOTE_DOWN = 0x90
- local NOTE_UP = 0x80
- local VELOCITY = 0x7f
增加在结尾位置的
- local function play(note, duration)
- midi_send(NOTE_DOWN, note, VELOCITY)
- scheduler.wait(duration)
- midi_send(NOTE_UP, note, VELOCITY)
- end
- return {
- parse_note = parse_note,
- play = play,
- }
留心一下就会发现, 这个版本我们用这行代码:
scheduler.wait(duration)
取代了原来的:
while os.clock() < 2 do end
使用 scheduler 库的好处就是在等待的时候不会阻塞程序的运行.
这里附上调度库 scheduler.lua 的代码:
- -- scheduler.lua
- local pending = {}
- local function sort_by_time(array)
- table.sort(array, function(e1,e2) return e1.time < e2.time end)
- end
- local function remove_first(array)
- result = array[1]
- array[1] = array[#array]
- array[#array] = nil
- return result
- end
- local function schedule(time, action)
- pending[#pending +1] = {
- time = time,
- action = action
- }
- sort_by_time(pending)
- end
- local function wait(seconds)
- coroutine.yield(seconds)
- end
- local function run()
- while #pending> 0 do
- while os.clock() < pending[1].time do end
- local item = remove_first(pending)
- local _, seconds = coroutine.resume(item.action)
- -- print("seconds:",seconds)
- if seconds then
- later = os.clock() + seconds
- schedule(later, item.action)
- end
- end
- end
- return {
- schedule = schedule,
- run = run,
- wait = wait
- }
完整的 notation.lua 的代码如下:
- -- notation.lua
- local scheduler = require 'scheduler'
- local NOTE_DOWN = 0x90
- local NOTE_UP = 0x80
- local VELOCITY = 0x7f
- local function note(letter, octave)
- local notes = {
- C = 0, Cs = 1, D = 2, Ds = 3, E = 4,
- F = 5, Fs = 6, G = 7, Gs = 8, A = 9,
- As = 10, B = 11,
- }
- local notes_per_octave = 12
- return (octave + 1) * notes_per_octave + notes[letter]
- end
- local tempo = 100
- local function duration(value)
- local quarter = 60 / tempo
- local durations = {
- h = 2.0, q = 1.0, ed = 0.75, e = 0.5, s = 0.25,
- }
- return durations[value] * quarter
- end
- local function parse_note(s)
- local letter, octave, value = string.match(s, "([A-Gs]+)(%d )(%a )")
- if not (letter and octave and value) then return nil end
- return {
- note = note(letter, octave),
- duration = duration(value)
- }
- end
- local function play(note, duration)
- midi_send(NOTE_DOWN, note, VELOCITY)
- scheduler.wait(duration)
- midi_send(NOTE_UP, note, VELOCITY)
- end
- return {
- parse_note = parse_note,
- play = play,
- }
完整的 good_morning_to_all.lua 代码如下:
- -- good_morning_to_all.lua
- scheduler = require 'scheduler'
- notation = require 'notation'
- notes = {
- 'D4q',
- 'E4q',
- 'D4q',
- 'G4q',
- 'Fs4h'
- }
- function play_song()
- for i = 1, #notes do
- local symbol = notation.parse_note(notes[i])
- print("note:", symbol.note, "duration:", symbol.duration)
- notation.play(symbol.note, symbol.duration)
- end
- end
- scheduler.schedule(0.0, coroutine.create(play_song))
- scheduler.run()
乐曲播放的代码基本完工, 试试效果:
./play good_morning_to_all.lua
听到了悦耳的乐曲声!
多声道乐曲播放
截至目前为止, 我们的项目从无到有, 已经实现了乐曲播放, 不过似乎还有些不太完美, 比如只支持单声道, 还有就是我们自定义格式的乐谱中的每个音符都要用引号引起来, 写起来比较麻烦, 所以我们接下来希望解决这两个问题.
那么我们希望自定义格式的乐谱写成这个样子:
- song.part{
- D3q, A2q, B2q, Fs2q,
- }
- song.part{
- D5q, Cs5q, B4q, A4q,
- }
- song.go()
多声道播放就是同时播放多个声部, 类似于合唱, 好在我们有调度器 scheduler, 可以很容易实现这一点, 把以下代码放入 notation.lua 中:
- local function part(t)
- local function play_part()
- for i = 1, #t do
- print("note:",t[i].note, "duration:", t[i].duration)
- play(t[i].note, t[i].duration)
- end
- end
- scheduler.schedule(0.0, coroutine.create(play_part))
- end
- local function set_tempo(bpm)
- tempo = bpm
- end
- local function go()
- scheduler.run()
- end
- return {
- parse_note = parse_note,
- play = play,
- part = part,
- set_tempo = set_tempo,
- go = go,
- }
代码分析
函数 part(t) 使用音符数组 t, 在其中定义了一个用于遍历播放 t 的函数 play_part, 我们把它加入调度器 scheduler 中, 只要通过新增的函数 go 来调用 scheduler.run() 就可以播放了, 通过调度器非常简单就实现了多声道播放.
最后是解决乐谱中每个音符都必须使用引号的问题, 其实这个问题有多种解决方法, 不过书中使用了最直接粗暴的一种, 就是使用 Lua 的元表, 将每个音符都设为全局变量, 具体代码如下 (这段代码也要放在 notation.lua 中):
- local mt = {
- __index = function(t, s)
- local result = parse_note(s)
- return result or rawget(t, s)
- end
- }
- setmetatable(_G, mt)
代码分析
以上代码重新定义了对 Lua 全局表 _G 中全局变量查找的方式 __index, 优先从函数 parse_note(s) 表返回的表中查找, 其余不是音符的全局变量则由 rawget(t, s) 提供查找结果.
完整的自定义格式乐谱
最后我们使用一个完整的自定义格式的乐谱, 是一首卡农, 两个声部, 新建文件 canon.lua, 代码如下:
- -- canon.lua
- song = require 'notation'
- song.set_tempo(50)
- song.part{
- D3s, Fs3s, A3s, D4s,
- A2s, Cs3s, E3s, A3s,
- B2s, D3s, Fs3s, B3s,
- Fs2s, A2s, Cs3s, Fs3s,
- G2s, B2s, D3s, G3s,
- D2s, Fs2s, A2s, D3s,
- G2s, B2s, D3s, G3s,
- A2s, Cs3s, E3s, A3s,
- }
- song.part{
- Fs4ed, Fs5s, Fs5s, G5s, Fs5s, E5s, D5ed, D5s, D5s, E5s, D5s, Cs5s,
- B4q, D5q, D5s, C5s, B4s, C5s, A4q,
- }
- song.go()
因为我们写的 C++ 宿主程序 缺少对 Lua 脚本的错误处理代码, 所以在最开始调试的时候遇到不少问题, 其中一个就是因为把乐谱中的大写音符写成小写结果导致 C stack overflow, 所以一定要确保你的输入没有任何错误.
最后执行:
./play canon.lua
接下来就可以静静欣赏多声部卡农了.
参考
七周七语言 (卷 2) https://book.douban.com/subject/26921107/
How can I build a C program that embeds Lua?
cmake 添加头文件目录, 链接动态, 静态库
将 Mac OS X 系统的 C,C++ 编译器从默认的 Clang 切换到 GCC
Lua C Stack Overflow 错误代码汇总
While installing on OS X Sierra via gcc-6, keep having "FATAL:/opt/local/bin/../libexec/as/x86_64/as: I don't understand 'm' flag!" error
Cmake 知识 ---- 编写 CMakeLists.txt 文件编译 C/C++ 程序
as don't understand'm' flag
ABC 记谱法
音程 (音乐术语)
来源: https://www.cnblogs.com/freeblues/p/9936844.html