說明: 本文是根據 七週七語言(卷2) 中的一個
Lua
示例項目略加修改而來.html
這個項目經過 Lua
調用一個用 C++
實現的 MIDI
接口庫 RtMidi
來控制一個 MIDI合成器
播放自定義格式的曲譜, 來演示 Lua
跟 C
之間的代碼交互.node
首先用 C++
做爲宿主程序, 把 Lua
解釋器嵌入其中, 接着用 C++
封裝了一個可供 Lua
腳本調用的 C++
函數 midi_send
, 這個函數經過調用 RtMidi
庫中的 API
向 MIDI合成器
發送控制命令來播放音樂, 而音樂的來源則是咱們用 Lua
自定義格式的曲譜, 由 Lua
將其解析轉換爲 MIDI 合成器
可以識別的格式.shell
這個項目是跨平臺的, 能夠同時支持 Windows/macOS/Linux
平臺, 本文只提供 macOS
上的實現, 其餘兩個平臺也很簡單, 其中 Lua
部分的代碼不須要改變.swift
須要安裝如下環境數組
brew
;XCode
或 gcc
;C sound
項目的源碼跟 RtMidi
;Lua
和 CMake
;macOS
下的 MIDI
合成器: SimpleSynth
個人環境上只缺 C sound
項目, RtMidi
以及 SimpleSynth
, 前兩個用 brew
安裝, 命令以下:ruby
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, 下載回來後把它運行起來, 用它來充當 MIDI 合成器
.app
環境準備 OK, 接下來就正式開始項目了.maven
咱們這個項目很簡單, 就是 3
部分:tcp
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
庫, 對 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
消息:
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 合成器
對消息格式的要求, 能夠寫出以下的 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)
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記譜法
音程(音樂術語)