加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 大数据 > 正文

用 Lua 控制 MIDI 合成器来播放自定义格式乐谱

发布时间:2020-12-14 21:51:33 所属栏目:大数据 来源:网络整理
导读:用 Lua 控制 MIDI 合成器来播放自定义格式乐谱 作者: FreeBlues 最新: https://www.cnblogs.com/freeblues/p/9936844.html 说明: 本文是根据 七周七语言(卷2) 中的一个 Lua 示例项目略加修改而来. 目录 项目介绍 环境准备 项目结构和代码 从单个音符到乐曲

用 Lua 控制 MIDI 合成器来播放自定义格式乐谱

  • 作者: FreeBlues
  • 最新: https://www.cnblogs.com/freeblues/p/9936844.html

说明: 本文是根据 七周七语言(卷2) 中的一个 Lua 示例项目略加修改而来.

目录

  • 项目介绍
  • 环境准备
  • 项目结构和代码
  • 从单个音符到乐曲
  • 多声道乐曲播放

项目介绍

这个项目通过 Lua 调用一个用 C++ 实现的 MIDI 接口库 RtMidi 来控制一个 MIDI合成器 播放自定义格式的乐谱,来演示 LuaC 之间的代码交互.

首先用 C++ 作为宿主程序,把 Lua 解释器嵌入其中,接着用 C++ 封装了一个可供 Lua 脚本调用的 C++ 函数 midi_send,这个函数通过调用 RtMidi 库中的 APIMIDI合成器 发送控制命令来播放音乐,而音乐的来源则是我们用 Lua 自定义格式的乐谱,由 Lua 将其解析转换为 MIDI 合成器 能够识别的格式.

环境准备

这个项目是跨平台的,可以同时支持 Windows/macOS/Linux 平台,本文只提供 macOS 上的实现,其他两个平台也很简单,其中 Lua 部分的代码不需要改变.

需要安装以下环境

  • 包管理器 brew;
  • 编译工具 XCodegcc;
  • C sound 项目的源码跟 RtMidi;
  • LuaCMake;
  • 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             [email?protected]                     [email?protected]                       shellz                        um
fluxctl                       i386-elf-gcc                  mesa                          [email?protected]                      sourcedocs
==> Updated Formulae
bdw-gc ?                            dartsim                             hebcal                              mitie                               sec
c-ares ? 
......
==> Deleted Formulae
corebird              [email?protected]            [email?protected]             [email?protected]             nethack4              [email?protected]              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,下载回来后把它运行起来,用它来充当 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,963 个数字会被入栈,然后开始执行 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.cppmain 函数的前面.

  • 代码分析

我们知道 Lua 通过一个简单的栈模型来实现跟 C/C++ 代码的交互,所以下面这 3 行代码就是把我们提供的 3MIDI合成器 要用到的参数入栈:

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,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‘
}

这是一个 Luatable,它代表一首歌曲的乐谱,使用一种类似于 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,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,"([A-Gs]+)(%d+)(%a+)")

使用 Luastring.match 函数进行模式匹配和捕获,遇到 D4q 这样的字符串,首先它会进行如下匹配:

  • D 匹配到模式 ([A-Gs]+);
  • 4 匹配到 (%d+);
  • q 匹配到 (%a+),

接着它会返回匹配成功的子串,也就是返回 D,4,将其分别赋给局部变量 letter,octave,value,最后再用 letteroctave 构造 MIDI音符,用 value 构造MIDI速率,也就是这段返回代码:

return {
    note = note(letter,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,将其中的每个字符串解析转换为 noteduration,然后传递给函数 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,VELOCITY)
    scheduler.wait(duration)
    midi_send(NOTE_UP,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,duration = duration(value)
    }
end

local function play(note,}

完整的 good_morning_to_all.lua 代码如下:

-- good_morning_to_all.lua

scheduler = require ‘scheduler‘
notation = require ‘notation‘

notes = {
    ‘D4q‘,‘Fs4h‘
}

function play_song()
    for i = 1,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,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,B2s,D3s,B3s,Fs2s,G2s,G3s,D2s,}

song.part{
    Fs4ed,Fs5s,G5s,E5s,D5ed,D5s,Cs5s,D5q,C5s,B4s,}

song.go()

因为我们写的 C++宿主程序 缺少对 Lua 脚本的错误处理代码,所以在最开始调试的时候遇到不少问题,其中一个就是因为把乐谱中的大写音符写成小写结果导致 C stack overflow,所以一定要确保你的输入没有任何错误.

最后执行:

./play canon.lua

接下来就可以静静欣赏多声部卡农了.

参考

七周七语言(卷2)
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 OSX 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记谱法
音程(音乐术语)

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读