用 Lua 控制 MIDI 合成器來播放自定義格式樂譜
用 Lua 控制 MIDI 合成器來播放自定義格式樂譜
- 作者: FreeBlues
- 最新: https://www.cnblogs.com/freeblues/p/9936844.html
說明: 本文是根據ofollow,noindex" target="_blank">七週七語言(卷2)
中的一個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 [email protected]@10shellzum [email protected] ==> Updated Formulae bdw-gc ✔dartsimhebcalmitiesec c-ares ✔ ...... ==> Deleted Formulae [email protected]@[email protected]@2.2taylortcptrack 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 :beer:/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
,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
接下來就可以靜靜欣賞多聲部卡農了.
參考
How can I build a C program that embeds Lua?
將 Mac OS X 系統的 C、C++ 編譯器從預設的 Clang 切換到 GCC
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++程式
音程(音樂術語)