用 Lua 控制 MIDI 合成器來播放自定義格式曲譜

用 Lua 控制 MIDI 合成器來播放自定義格式曲譜

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

說明: 本文是根據 七週七語言(卷2) 中的一個 Lua 示例項目略加修改而來.html

目錄

項目介紹

這個項目經過 Lua 調用一個用 C++ 實現的 MIDI 接口庫 RtMidi 來控制一個 MIDI合成器 播放自定義格式的曲譜, 來演示 LuaC 之間的代碼交互.node

首先用 C++ 做爲宿主程序, 把 Lua 解釋器嵌入其中, 接着用 C++ 封裝了一個可供 Lua 腳本調用的 C++ 函數 midi_send, 這個函數經過調用 RtMidi 庫中的 APIMIDI合成器 發送控制命令來播放音樂, 而音樂的來源則是咱們用 Lua 自定義格式的曲譜, 由 Lua 將其解析轉換爲 MIDI 合成器 可以識別的格式.shell

環境準備

這個項目是跨平臺的, 能夠同時支持 Windows/macOS/Linux 平臺, 本文只提供 macOS 上的實現, 其餘兩個平臺也很簡單, 其中 Lua 部分的代碼不須要改變.swift

須要安裝如下環境數組

  • 包管理器 brew;
  • 編譯工具 XCodegcc;
  • C sound 項目的源碼跟 RtMidi;
  • LuaCMake;
  • 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 庫

接着就要引入 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, 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'
}

這是一個 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, 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+)")

使用 Luastring.match 函數進行模式匹配和捕獲, 遇到 D4q 這樣的字符串, 首先它會進行以下匹配:

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

接着它會返回匹配成功的子串, 也就是返回 D, 4, q, 將其分別賦給局部變量 letter, octave, value, 最後再用 letteroctave 構造 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, 將其中的每一個字符串解析轉換爲 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, 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記譜法
音程(音樂術語)

相關文章
相關標籤/搜索