近被「諾亞財富34億踩雷」的新聞刷屏了,報案、否認、甩鍋,一波三折,如今演繹成了羅生門。承興造假,京東否認,諾亞報案。如何通過技術方案的設計在風險發生時為業務提供強有力的舉證支持是我最近一直在思考的事情,其中電子合同的有效性是我們一直在討論的重點問題。
隨著互聯網金融/金融科技的發展,合同通過電子化簽署逐漸被大家接受。但是受限制于國內《電子簽名法》采用了技術折中的立法模式,法律法規僅從功能性和效果上的角度上提出了要求。
如何界定電子簽名、數據電文及電子合同的有效性涉及較為復雜的技術知識,司法實踐中對于技術路徑審查的相關經驗并不多,存在著對“電子簽名制作數據”、“電子簽名”、“數字簽名”、“用戶密碼”等專業概念的認知偏差,對如何簽出一份合法有效的電子合同則比較模糊。
針對于此,我也與很多的法務同學、電子合同服務商進行過交流,整理了下我在交流中的感受,分享出來拋磚引玉,歡迎砸我。
首先我們得確認電子合同是合法有效的,這是這篇文章的基礎。
對于這一點,在《合同法》以及《電子簽名法》中都有明確規定:
《中華人民共和國合同法》
第十條:當事人訂立合同,有書面形式、口頭形式和其他形式;
第十一條:書面形式是指合同書、信件和數據電文(包括電報、電傳、傳真、電子數據交換和電子郵件)等可以有形地表現所載內容的形式。
簡而言之:電子合同屬于合同的一種。
《中華人民共和國電子簽名法》
第十三條 電子簽名同時符合下列條件的,視為可靠的電子簽名:
- 電子簽名制作數據用于電子簽名時,屬于電子簽名人專有;
- 簽署時電子簽名制作數據僅由電子簽名人控制;
- 簽署后對電子簽名的任何改動能夠被發現;
- 簽署后對數據電文內容和形式的任何改動能夠被發現。當事人也可以選擇使用符合其約定的可靠的條件的電子簽名;
第十四條 可靠的電子簽名與手寫簽名或者蓋章具有同等的法律效力
簡而言之:《電子簽名法》規定了可靠的電子簽名的有效性以及可靠的電子簽名的要素:專有、專控、不可篡改。
(當然,《電子簽名法》中也規定了一些不適用電子簽名的情況:涉及婚姻、收養、繼承等人身關系的;涉及停止供水、供電、供氣等公用事業服務的;法律、行政法規規定的不適用電子文書的其他情形。這些不在我們本次討論范圍之內。)
合同的成立在傳統合同書中一般通過簽字或者蓋章的方式來體現,在電子合同中簽字蓋章的行為被可靠的電子簽名所代替《電子簽名法》第十四條[1]。
本質上電子合同的成立生效沒有跳出傳統合同的要件要求,這些要求我們不在此做討論。
既然是「可靠的電子簽名」與手寫簽名或者蓋章具有同等的法律效力,那么我們只需要能在業務中能夠證明符合相關要素即可。市面上的一些第三方電子合同服務商都有符合相關規定(至少是能經得起推敲)的產品方案(畢竟人家賺的就是這份錢)。
但第三方服務商的標準產品開發需要最大程度的降低其自身的風險,一般情況下會將簽署合同的整個過程至于其可控范圍內,其產品流程并不一定適合我們的業務系統交互需求甚至可能與業務系統相沖突。
因此部分過程需要業務系統自己完成,業務系統在風險發生時需要承擔一定舉證責任(不得不說,某些服務商的方案對于業務系統侵入性太大,很難接受啊)。
我們先對電子合同簽署的生命流程進行拆解:
實名認證步驟,一般分為個人實名和企業實名。(不討論線下核驗的方式)
個人實名的方案很多:生物識別、銀行卡驗證、手機號實名認證……一般目前最保險的是掃臉認證活體檢測,最好將活體的視頻保存下來,以便舉證。
企業實名,目前用的最多的營業執照、銀行賬戶小額打款、法人身份證來驗證。
這里順便我說一句,我只看到一家供應商可以提供法人姓名+身份證號的認證信息,不知道市面上還有沒有其他的服務商可以有此類服務提供(不是根據法人身份證對信息做二要素驗證,而是根據工商注冊信息對提供的法人姓名和身份證號做驗證)。
意愿認證上,方式有很多。目前大部分第三方電子合同服務商都采用云托管數字證書模式,在意愿認證上針對個人一般采用短信驗證碼、人臉識別等來作為簽署人意愿表達的行為;針對企業一般通過向企業授權人發送短信驗證碼、識別授權人人臉等方式或者采用 Ukey 來作為意愿表達。
這里多說一句:如果你采用 ukey 來做鑒權,需要考慮一點就是簽署時使用的數字證書是否為 ukey 里存儲的數字證書。如果不是(極大概率不是)一定要注意證據鏈完整的問題,因為本質上還是云托管數字證書來完成簽署。
至于合同生成和司法舉證,去找第三方服務商吧,如果這些事情都要自己做,那你就是自己在做一個電子合同平臺了。
其實技術上沒有難點,主要在舉證問題上有一些思考:
一般來說,我們做實名都是調用第三方數據接口,我們需要盡量保證調用記錄的完整性及可查性。在出現電子合同有效性問題時可以提供我方已經進行應盡的實名義務,并在力所能及的范圍內做到了對用戶的實名認證。
針對企業實名,要至少核驗包括包括企業名稱、統一社會信用代碼,最好對法人姓名及身份證號進行核驗,同時應對經辦人進行個人實名核驗,以及企業核心隱私數據的核驗,例如對公銀行打款、開具指定金額發票等核驗方式;
也可通過電子認證服務機構頒發的數字證書進行實名核驗(這一點某 CA 的一個服務可以實現通過全國大部分(小)銀行發放的 ukey進行實名認證,不過大行很少)。
這里需要注意的一點是我們在選擇第三方數據服務商的時候一定要主要選擇政府權威部門的數據庫或者取得政府權威部門授權或認可的電子數據庫(比如國政通……國政通麻煩廣告費結下)
在合法合規的前提下,除通過短信驗證碼、人臉識別或 ukey 認證等方式完成用戶意愿認證外,管理系統還應該盡可能的收集用戶在簽署時的IP 地址、操作設備 MAC 地址、操作系統信息等可以佐證是用戶自身操作的信息。
目前大部分系統對接第三方電子合同服務商的時候為了不讓電子合同系統侵入業務系統都采用了各家服務商提供的「自動簽署」方案(這一點我要吐槽下拉,各家差不多都有這樣的接口,但是在使用上并沒有很好的給用戶說明。)
首先,我們要說的是《電子簽名法》第十三條里提到的「簽署時電子簽名制作數據僅由電子簽名人控制」這一項規定是對電子簽名過程中電子簽名制作數據歸誰控制的要求。這里所規定的控制是指一種實質上的控制,即基于電子簽名人的自由意志而對電子簽名制作數據的控制。
在電子簽名人實施電子簽名行為的過程中,無論是電子簽名人自己實施簽名行為,還是委托他人代為實施簽名行為,只要電子簽名人擁有實質上的控制權,則其所實施的簽名行為,滿足本法此項規定的要求。(這段話不是我說的,是全國人大關于《電子簽名法》的釋法[2])
在中國互聯網金融協會《互聯網金融個體網絡借貸電子合同安全規范(征求意見稿)》第 8 司法舉證要求(d)中也提到「電子簽名人委托他人代為實施簽名行為時,從業機構或第三方電子合同訂立系統服務商提供電子簽名制作數據由電子簽名人控制的證據,包括調用電子簽名制作數據的時間和方式、電子簽名人位置、IP地址、授權及認證方式、授權及認證記錄等;」
所以,自動簽署的方案大家還是可以放心用,只需要你能通過其他方式來證明電子簽名人擁有實質上的控制權即可。
關于電子簽名,有一個舉證的坑。
《電子簽名法》第二十八條:電子簽名人或者電子簽名依賴方因依據電子認證服務提供者提供的電子簽名認證服務從事民事活動遭受損失,電子認證服務提供者不能證明自己無過錯的,承擔賠償責任。
也就是說,關于電子簽名,舉證責任倒置。即對方提出的侵權事實,電子認證提供者如果予以否認,則應負舉證責任,證明自己沒有過錯。
在司法實踐上,《袁斌與合肥夢川玖貿易有限公司等小額借款合同糾紛二審案件》【案號:北京市第三中級人民法院(2018)京03民終4903號】[3]中,法院也是這樣認定的。
當然,只要電子認證服務提供者能夠證明自己對于電子簽名人或者電子簽名依賴方所遭受的損失沒有過錯,就不承擔責任。而對于電子認證服務提供者來講,只要能夠證明其所提供的服務完全是嚴格按照本法和符合國家規定并向國務院信息產業主管部門備案的電子認證業務規則實施的,則應能夠證明沒有過錯。(《電子簽名法釋義 法律責任》[4])
電子簽名無效僅僅表示該電子簽名并非當事人真實意愿的表達,并不必然影響當事人之間的部分關系(比如債權債務關系、勞動關系)的成立。
如果能夠從其他方面來證明當事人之間存在相關關系,法院大概率上會要求侵權方承擔民事責任。但一些合同上具體規定可能無法予以認定。
比如上邊提到的《袁斌與合肥夢川玖貿易有限公司等小額借款合同糾紛二審案件》中,法院雖然認定電子簽名無效,但是從實名認證信息、操作記錄等方面認定借款合同有效。
其實這一點上,《最高人民法院關于互聯網法院審理案件若干問題的規定》第十一條[5]已經明確指出:「當事人提交的電子數據,通過電子簽名、可信時間戳、哈希值校驗、區塊鏈等證據收集、固定和防篡改的技術手段或者通過電子取證存證平臺認證,能夠證明其真實性的,互聯網法院應當確認。」
最后多說一句,如果有錢花一點錢找服務商做個證據保全系統或者直接和司法鑒定中心、公證處之類的合作,畢竟法官不是開發小哥,你跟他講技術遠不如公證處或者司法鑒定中心的一個章子管用。
[1]
《電子簽名法》第十四條:可靠的電子簽名與手寫簽名或者蓋章具有同等的法律效力。
[2]
全國人大關于《電子簽名法》的釋法:http://t.cn/AiYEl4bm
[3]
《袁斌與合肥夢川玖貿易有限公司等小額借款合同糾紛二審案件》【案號:北京市第三中級人民法院(2018)京03民終4903號】:http://t.cn/AiYEOrYd
[4]
《電子簽名法釋義 法律責任》:http://t.cn/AiYElv5w
[5]
《最高人民法院關于互聯網法院審理案件若干問題的規定》第十一條:http://www.court.gov.cn/zixun-xiangqing-116981.html
互金業務中經常提到的電子合同,到底是個啥?
張小璋,公眾號:張小璋的碎碎念(ID:SylvainZhang),人人都是產品經理專欄作家。野蠻生長的產品經理,專注于互聯網金融領域。
本文原創發布于人人都是產品經理。未經許可,禁止轉載。
題圖來自 Unsplash,基于 CC0 協議
MIDI 協議即數字音樂接口(Musical Instrument Digital Interface),是電子樂器、合成器等演奏設備之間的一種即時通信協議,用于硬件之間的實時演奏數據傳遞。MIDI 協議誕生之初希望解決的事情是通過統一通信協議讓不同樂器制造商的設備可以互相兼容,比如把 Roland 鍵盤接入 Yamaha 合成器。MIDI 協議的編碼經過拓展后也可以作為一種記錄音樂信息的文件格式,被稱為“標準 MIDI 文件格式”。
在音樂技術研發中除了需要與音頻打交道之外,許多場景中還需要直接處理音符信息。如果說 wav 與 mp3 記錄的是音樂的物理現象,那么 MIDI 協議與 MIDI 文件則記錄的是音樂這門語言的“文字”。本文的目的是讓開發中涉及到音樂“本體”的同學可以了解這一最通用的演奏信息交互和文件存儲格式的編碼規則。同時通過對 MIDI 事件流等概念的認識,能在開發中更好地抽象自己的業務邏輯。
和 HTTP 這類協議不同,MIDI 作為傳輸協議時所有傳遞的信息都需要被實時響應,比如一個觸鍵信息、一個效果器參數的改變都需要立刻被執行,所以其采用數據流的方式進行數據傳輸。MIDI 定義了一個 8 位的二進制數據流,許多時候我們可以使用 ASCII 碼來將其表示為 16 進制的字符用于傳輸和保存。
對于 MIDI 標準文件格式來說,其存儲的內容也是 MIDI 產生的事件流。一段典型的 MIDI 文件長這樣:
4D 54 68 64 00 00 00 06 00 01 00 03 01 E0 4D 54
72 6B 00 00 00 1A 00 FF 03 03 31 32 33 00 FF 51
03 08 7A 23 00 FF 58 04 04 02 18 08 00 FF 2F 00
...
上面這個例子可能會造成一些困惑,因為 MIDI 文件確實對人類閱讀不太友好,但其編碼規則實際上是較易掌握的,下面我們就來逐步認識 MIDI 的編碼規則。
注:在本文中,一個字節的最低有效位為第 0 位,最高有效位是第 7 位。比如在 X000 000Y 中,X 為第 7 位,Y 為第 0 位。
MIDI 最核心的功能是用于傳輸實時的音樂演奏信息,這些信息本質上是一條條包含了音高、力度、效果器參數等信息的指令,我們將這些指令稱之為 MIDI 消息(MIDI message)。一條 MIDI 消息通常由數個字節組成,其中第一個字節被稱為 STATUS byte,其后面有跟有數個 DATA bytes。STATUS byte 第七位為 1,而 DATA byte 第七位為 0。
開頭的 STATUS byte 有兩個作用:一個作用是表示系統或者某個信道狀態的改變,其二個作用是確定當前 MIDI Message 的類型,MIDI 類型會確定后面 DATA byte 的數量和意義。這樣說比較空洞,下面我們舉一個例子:
Status byte : 1100 CCCC
Data byte 1 : 0XXX XXXX
Status byte : 1001 CCCC
Data byte 1 : 0PPP PPPP
Data byte 2 : 0VVV VVVV
第一個 STATUS byte 告訴我們這是一個進行樂器選擇的 MIDI Message(1100 為樂器選擇指令,CCCC 是信道編號)。樂器選擇的 MIDI Message 只有一條 DATA byte,而這條 DATA Byte 的數據表示選擇的樂器編號。第二條 1001 開頭的 STATUS byte 則告訴我們這是一條 Note On 類型 MIDI message,這個類型按照約定有兩個 DATA byte。
除了向整個系統發送的 MIDI 消息, STATUS byte 通常包含了信道編號(即例子中的 CCCC),16 個信道分別從 0000 到 1111。而向整個系統發送的 MIDI 信息則以 1111 開頭,原來的信道編號變成了指令編號(比如播放指令:1111 1010,終止指令:1111 1100)。
需要注意的是,許多時候我們會連續發送許多相同狀態的 MIDI 消息,這個時候可以省略 STATUS byte,合成器會沿用最后一個接收的 STATUS byte,被合成器記錄的狀態稱之為 MIDI RUNNING STATUS。
總結一下:
一條 MIDI message 由 STATUS byte 和 Data byte 構成。
STATUS byte 以 1 開頭,DATA byte 以 0 開頭。
STATUS byte 確定消息的類型。后面的 DATA 字節數取決于消息的類型。
STATUS byte 通常包含信道編號,除了面向系統發送的指令。
連續相同的 STATUS byte 可以省略。
MIDI Message 不需要全部掌握,需要的時候可以直接到 MIDI 標準中查詢,日常開發中只需要了解常用的幾種 MIDI Message 即可。下面筆者介紹最常用的幾種 MIDI Message。
NOTE ON 和 NOTE OFF 是最主要的兩個 MIDI Message。當演奏者敲擊音樂鍵盤的琴鍵時發送 NOTE ON 消息,它包含了音高以及“力度”的參數。當合成器收到此消息時,它會開始以相應的音高和“力度”播放該音符。當收到 NOTE OFF 消息時,合成器會終止該音符。
每個 NOTE ON 消息都需要相應的 NOTE OFF 消息,否則該音符將一直處于播放狀態。但打擊樂器可以只發送 NOTE ON,因為打擊樂音符會自動停止。但最好養成始終發送 NOTE OFF 的習慣,因為不同合成器對這一特性的實現可能不一樣。
下面我們舉例說明 NOTE ON:
Status byte : 1001 CCCC
Data byte 1 : 0PPP PPPP
Data byte 2 : 0VVV VVVV
在這個例子中,1001 可以理解為 NOTE ON 事件的編碼,CCCC 是信道編號。
PPP PPPP 表示音高值,在 General MIDI 協議中(后文會提到),通常使用 69 表示標準音 A4(440 Hz),音高值增減一,就增減一個半音。比如 60 表示 C4(中央 C), 61 表示 C#4。同樣,升高或者降低八度只需要在當前音高上增減 12 即可。
VVV VVVV 表示速率(velocity),這個速率可以理解為敲擊鍵盤的速度,或者管樂器氣流的速度。在最基礎的合成器中,速率僅用于確定彈奏音符的力度,唯一的效果是音符音量變大或變小。總體來說,下面這張表可以作為速率和樂譜中的力度記號的對應關系參考:
但在在一些復雜的仿真建模合成器中,速率也會影響音色。我們以 Galaxy Steinway 采樣器為例:
圖片來源:https://zhuanlan.zhihu.com/p/19964066
左側是小力度敲擊的頻域圖,右側是大力度敲擊的頻域圖。我們可以看到大力度敲擊不僅產生了更多的泛音,也在低頻區產生了一些噪音(木材被撞擊的聲音)。
注:關于 velocity 可以參考附錄的介紹。
NOTE OFF 消息和 NOTE ON 消息基本一樣:
Status byte : 1000 CCCC
Data byte 1 : 0PPP PPPP
Data byte 2 : 0VVV VVVV
其中 CCCC 和 PPPPPPP 含義同上。VVVVVVV 是釋放速率,可以看作是按鍵抬起的速度,這個值很少使用,通常將其設置為零。另外,在實踐中經常使用速率為 0 的 NOTE ON 消息取代 NOTE OFF 消息。
需要額外說明的是 MIDI 協議還提供了一組 All Notes Off 消息,當某個信道接收到 All Note Off 消息之后會關閉所有還在發音的振蕩器,通常來說 All Notes Off 消息用于在演奏、播放結束后用于清理狀態,這里不多贅述。
樂器選擇消息的格式如下:
Status byte : 1100 CCCC
Data byte 1 : 0XXX XXXX
其中唯一的一個 DATA byte 表示樂器編號,支持 128 個不同的樂器。由于不同的軟件上存在的樂器音源并不一致,為了讓 A 設備上創建的標準 MIDI 文件在 B 設備上播放時聽起來相似,樂器廠商邊采用 General MIDI 協議來編排音源。Gerneral MIDI 通常簡寫為 GM ,它提供了一個標準化的音庫,將 128 個樂器排列成 16 個系列,每個系列有 8 個同類型的樂器,并為每個樂器分配一個特定的程序編號。GM 樂器表可以參考:
http://www.harfesoft.de/aixphysik/sound/midi/pages/genmidi.html
在 GM 標準下,信道 10 是保留給打擊樂器的(實際上合成器可以在任何信道上使用鼓),在這個信道上樂器編碼遵循通用 MIDI 鼓樂器列表(General MIDI drum instruments list),具體可以參考:
https://en.wikipedia.org/wiki/General_MIDI#Percussion
由于鼓是總體上是噪音樂器,所以之前的音高參數則被映射為不同的鼓音效。
注:噪音樂器指沒有明確音高的樂器,有明確音高的樂器稱為樂音樂器。
MIDI 設備通常會提供一些控制器用于改變合成器的某個參數,比如混響、增益等。MIDI 協議可以使用控制器消息操作 128 個不同的控制器,控制器消息結構如下:
Status byte : 1011 CCCC
Data byte 1 : 0NNN NNNN
Data byte 2 : 0VVV VVVV
其中 NNN NNNN 是控制器的編號,VVV VVVV 則是控制器的值。
控制器消息一方面可以用于改變合成器的某些參數,比如我們可以用以下指令將某個信道的力度值設置為 100:
Status byte : 1011 CCCC
Data byte 1 : 0000 0111
Data byte 2 : 0110 0100
另一方面,控制器編碼可以通過“組合”的方式實現一些更復雜的指令。如前文所述,選擇樂器可以通過 1000 開頭的 STATUS byte 實現,這個指令可以選擇 128 種樂器。對于同一個樂器來說可以應用不同的音色庫,比如我可以在鋼琴上使用雅馬哈的采樣、施坦威的采樣或者是珠江的采樣,由于樂器廠商認為 128 這個數量對于音色庫太小了,所以采用的 MSB + LSB 的方式表示音色庫,例子如下:
Status byte : 1011 CCCC
Data byte 1 : 0000 0000 // 0 = Sound bank selection (MSB)
Data byte 2 : 0000 0101
Status byte : 1011 CCCC
Data byte 1 : 0010 0000 // 32 = Sound bank selection (LSB)
Data byte 2 : 0000 0001
Status byte : 1100 0000
Data byte 1 : 0000 0010
這段代碼選擇了一個編號為 2,并且音色編號為 MSB = 0, LSB = 32 的樂器。由于 MSB 和 LSB 的范圍都是 2 ^ 7 = 128,所以理論上可以選擇的音色為 (2 ^ 7) ^ 2 = 16384
在 MIDI 中控制器消息和音源與效果器的參數密切相關,不同編號的控制器有一些約定俗稱的含義,在程序中實現控制器時盡量與已有的規范對齊,具體內容可以參考這個表格:MIDI CC List(https://professionalcomposers.com/midi-cc-list/)
注:MSB 指最高有效字節(most significant byte),LSB 指最低有效字節(least significant byte)。一個 14 位的數據 XXX XXXX YYY YYYY 可以用 MSB + LSB 表示為:0XXX XXXX 0YYY YYYY
彎音消息也用到了我們剛才提到的 MSB + LSB 表示法,其消息結構如下:
Status byte : 1110 CCCC
Data byte 1 : 0LLL LLLL
Data byte 2 : 0MMM MMMM
其中 LLL LLLL 表示 LSB,MMM MMMM 表示 MSB,彎音值 0x2000(即 0b10000000000000)為同音高,0x3FFF(即 0b11111111111111)表示上方大二度,0x0000(即 0b00000000000000)表示下方大二度。在實踐中,我們可以通過連續發送遞增或者遞減的彎音消息來表現滑音。
所有系統消息都以 1111 開頭,其中有兩個特殊的消息。一個是 1111 0000 它表示后面的消息是系統獨有的。另外一個 1111 0111 則表示系統獨有消息結束,消息結構如下:
11110000
0iiiiiii
0ddddddd
..
..
0ddddddd
11110111
當合成器監聽到 1111 0000 時,檢查下一個字節 0iii iiii , iii iiii 是一個 7 位的制造商 ID。如果合成器識別出這個代碼則會繼續監聽后面的數據,否則則忽略掉收到的消息,直到結束消息 1111 0111 出現。
許多宿主環境都提供了用于編寫 MIDI 交互程序的 API,在瀏覽器上是 Web MIDI API,在 iOS & Mac 上是 Core MIDI,Android 上則有 AMidi。為了方便讀者進行實際操作,我們以 Web MIDI API 為例展示如何編寫一個最基本的 MIDI 程序:
const button = document.getElementById('console-message')
button.addEventListener('click', () => {
if (navigator.requestMIDIAccess) {
navigator.requestMIDIAccess()
.then(success, failure);
}
})
function success (midiAccess) {
const inputs = midiAccess.inputs.values();
for (let input of inputs) {
input.value.onmidimessage = onMIDIMessage;
}
}
function failure () {
console.error('No access to your midi devices.')
}
function onMIDIMessage (messageEvent) {
console.log(messageEvent)
}
在這里,我們可以通過 requestMIDIAccess 向用戶索要訪問 MIDI 設備的權限,用戶允許后我們會拿到一個 midiAccess 對象,可以通過這個對象拿到所有的輸入和輸出設備。我們可以通過設備對象提供的 onmidimessage 回調監聽 midi message。
MIDI 消息的編碼存儲在 messageEvent 的 data 成員中,通過打印出的信息我們可以發現 Web MIDI API 并不會省略 Status Byte,這是為了便于開發者更容易區分指令屬于哪個狀態,而不必手動保存 MIDI 的運行狀態。
如果想要 MIDI 可以發音,我們可以使用 Web Audio API 提供的振蕩器:
const button = document.getElementById('play-sound')
const oscillators = {};
let context
button.addEventListener('click', () => {
context = new AudioContext()
if (navigator.requestMIDIAccess) {
navigator.requestMIDIAccess()
.then(success, failure);
}
})
function success (midiAccess) {
const inputs = midiAccess.inputs.values();
for (let input of inputs) {
input.onmidimessage = onMIDIMessage;
}
}
function failure () {
console.error('No access to your midi devices.')
}
function onMIDIMessage (message) {
const frequency = midiNoteToFrequency(message.data[1]);
// midi 鍵盤的普通按鍵默認使用通道 0,所以其 note on 事件為 1100 0000
if (message.data[0] === 144) {
playNote(frequency);
}
// note off
if (message.data[0] === 128) {
stopNote(frequency);
}
}
function midiNoteToFrequency (note) {
return Math.pow(2, ((note - 69) / 12)) * 440;
}
function playNote (frequency) {
oscillators[frequency] = context.createOscillator();
oscillators[frequency].frequency.value = frequency;
oscillators[frequency].connect(context.destination);
oscillators[frequency].start(context.currentTime);
}
function stopNote (frequency) {
oscillators[frequency].stop(context.currentTime);
oscillators[frequency].disconnect();
}
我們可以使用這個小程序來回顧與驗證我們之前講到的 MIDI Message 知識。
這里有一個筆者以前做的視唱練耳小工具,可以使用 MIDI 鍵盤進行視唱練耳練習:
演示地址:muse-training(https://muse-training-8gwn0lc039762917-1252681582.tcloudbaseapp.com/)
倉庫地址:https://github.com/lipd/muse-training
MIDI 協議解決的是音樂設備之間的即時通訊問題,它本質上是一個硬件之間的通信協議。而當我們想把 MIDI 演奏保存在磁盤上則需要用到標準 MIDI 文件格式規范(Standard MIDI-File Format Spec)。和 MIDI 通信協議一樣,MIDI 文件也是 8 位字節流,下文將會說明 MIDI 文件一些最基本的格式規范。
Chunk 是構成 MIDI 文件的基本單元。一個 Chunk 由三個部分組成:Chunk 類型 、Chunk 長度以及 Chunk 數據。Chunk 類型是 4 個 ASCII 字符,之后使用 32 位表示 Chunk 數據的長度,最后才是 Chunk 需要存儲的數據。
MIDI 中一共有兩種 Chunk,分別為 Header Chunk 和 Track Chunk。Header Chunk 標記為 MThd,存儲的是整個 MIDI 文件的基本信息,和 PNG 等文件的 Header Chunk 類似。Track Chunk 標記為 MTrk,每個 Track Chunk 都存儲了一個 MIDI 事件流,一個事件流可以包含 16 個 MIDI 信道的消息。一個典型 MIDI 文件的結構如下:
MThd <length>
<MThd data>
MTrk <length>
<MTrk data>
MTrk <length>
<MTrk data>
MIDI 文件的 Header Chunk 包含的信息非常簡單,我們以上面這個文件為例:
4D 54 68 64 // MThd 的 ASCII 碼
00 00 00 06 // MThd 的數據長度,MThd Data 固定為 6 字節
---- DATA 部分 ----
00 01 // MIDI 文件格式,有 0、1、2 三種
00 02 // MIDI 文件的包含的音軌數量,即 Track Chunk 數量
00 DC // MIDI 文件的時間類型
前兩條數據已經介紹過,這里不再贅述。我們來解釋一下 MIDI 文件格式與 MIDI 時間類型:
MIDI 文件格式(MIDI File Formats)
MIDI 文件格式分為三種,格式 0 的 MIDI 文件只有一個 Header Chunk 和一個 Track Chunk。對于只有一個軌道的程序可以采用這種格式。
格式 1 有一個 Header Chunk ,和多個 Track Chunk 。其中第一條 Track Chunk 是特殊的,負責記錄 MIDI 文件的所有 Meta Event(后面會講到),而從第二條 Track Chunk 開始才會記錄 MIDI Event,所以我們上圖中的 MIDI 文件實際上只有一條用于演奏的音軌。目前絕大部分的支持多音軌的程序都采用這種格式,筆者也建議讀者盡量使用這種格式。
格式 2 的 MIDI 文件也有多個 Track Chunk,但不同的是格式 1 所有 Track Chunk 共用一條時間軸,所有 Track 應當被視作同時播放的。而格式 2 中 Track Chunk 都有自己獨立的時間信息,這種格式非常少見,不建議使用。
我們用一張表總結一下:
音軌數量 | 時間軸 | |
格式 0 | 1 個 | 1 條 |
格式 1 | 多個 | 1 條 |
格式 2 | 多個 | 多條 |
MIDI 時間類型
MIDI 時間類型主要有兩種,為了方便介紹讀者可以簡單將其理解為“按音符分割的”和“按幀分割的”:
“按音符分割的”時間類型 15 位為 0,被稱為 TPQN(Ticks Per Quarter-Note),即一個四分音符中包含了多少 Tick。在前文的例子中 00 DC 表示 TPQN 為 220,那么一個八分音符為 110 Ticks,一個二分音符為 440 Ticks。另外 TPQN 也被稱為 Pulses Per Quarter-Note (每四分音符的脈沖數),如果你在代碼中看到 PPQ、PPQN 這樣的簡寫,你知道他們是一個意思即可。
“按幀分割的”時間類型 15 為 1,這種格式單純 MIDI 文件中幾乎不用而且比較復雜,建議讀者跳過。其編碼規則簡單說就是使用了 SMPTE 時間碼的規范。其 14 - 8 位包含了包含 -24、-25、-29 或 -30 四個值之一,對應于四種標準 SMPTE 時間碼格式(-29 對應于 30 個丟幀),并表示每秒的幀數。第 7 到 0 位表示幀內分辨率。我們依然用一張表總結一下:
15 位 | 14-8位 | 7-0位 | |
按音符 | 0 | 四分音符的Tick數 | |
按幀 | 1 | SMPTE格式 | 每幀Tick數 |
Track Chunk 的主要功能是用于存儲實際的演奏數據。它的 Chunk Data 中存儲的是一串事件流,被 Track Chunk 記錄的事件我們稱為 MTrk 事件,其結構如下:
<MTrk event> = <delta time> <event>
在這個結構中,事件可以指代三類事件:midi 事件、系統獨有事件、元事件:
<event> = <midi event> | <sysex event> | <meta event>
delta time
MIDI 通信時所有信息都是即時執行,所以 MIDI 消息并沒有記錄時間,但是 MIDI 文件則需要記錄時間在時間軸上的位置。MIDI 文件采用差量時間來記錄 MIDI 事件,即 Δt。delta time 表示的是當前事件與上一個時間相差的 Tick 數。如果要表示同時發生的數個任務,則記錄一串 delta time 為 0 的事件流即可。比如我們控制器一章中切換樂器的事件流可以表示為:
Delta time : 0000 0000
Status byte : 1011 CCCC
Data byte 1 : 0000 0000 // 0 = Sound bank selection (MSB)
Data byte 2 : 0000 0101
Delta time : 0000 0000
Status byte : 1011 CCCC
Data byte 1 : 0010 0000 // 32 = Sound bank selection (LSB)
Data byte 2 : 0000 0001
Delta time : 0000 0000
Status byte : 1100 0000
Data byte 1 : 0000 0010
sysex event
即系統獨占的消息事件,具體可以參考前文中的系統獨占消息。
meta event
所有元事件以 1111 1111 開頭,這個指令在 MIDI 消息中表示系統復位。這個指令是一個系統實時信息,通常在使用 MIDI 文件的程序并不會用到,所以在這里用于表示元事件。元事件主要用于指定拍號、調號、速度等。
需要注意的是FF 2F 00 是一個特殊的元事件,表示軌道結束。所有 Track Chunk 都以這個元事件結束。下面這張表是標準中已定義的元事件:
意義 | |
FF 00 02 | 序列號 |
FF 01 len text | 文本事件 |
FF 02 len text | 版權聲明 |
FF 03 len text | 軌道名稱 |
FF 04 len text | 軌道中使用的樂器類型 |
FF 05 len text | 歌詞 |
FF 06 len text | 某個點的名稱,比如“第一樂章” |
FF 07 len text | Cue Point 某個舞臺事件描述 |
FF 20 01 cc | MIDI 通道前綴 |
FF 2F 00 | End of Track |
FF 51 03 tttttt | 設置速度 |
FF 54 05 hr mn se fr ff | SMPTE Offset |
FF 58 04 nn dd cc bb | 拍號 |
FF 59 02 sf mi | 調號 |
MIDI 通信協議目前看來主要有兩個較明顯的缺陷。第一個缺陷是許多值可以表示的范圍實在有限,比如 note off 的 velocity 就只有 128 個、樂器也只有 128 個、只有 16 個信道。
另一個問題更為麻煩,MIDI 中控制器、和彎音消息只能發送給某個信道,你根本就沒法將它和某個音聯系在一起。這一局限在以前并沒有引起多少問題,因為傳統樂器很少碰到按音處理控制器的情況。而彎音用得最頻繁的更多是單聲部樂器。
但電子音樂界向來不缺乏整活健將,工程師總是會想方設法突破現有限制。最典型的例子就是 seaboard 鍵盤,這玩意兒可以在每個鍵上提供彎音能力。你可以從下面這段演奏上感受到這一樂器的神奇魅力:
原視頻鏈接:https://www.youtube.com/watch?v=6SCug5kUsBs
為了解決讓控制器消息能按“音”發送,seaboard 的制造商 ROLI 制訂了 MIDI Polyphonic Expression(MPE,MIDI 復音表示法)。其原理基本上可以概括為:讓每個發聲的音符都會在其 Note On 和 Note Off 之間臨時分配一個 MIDI 通道。這樣便把控制器消息和彎音消息與特定音符建立了聯系,并且很好的兼容了 MIDI 協議。
上述問題現在都正在通過新的 MIDI 2.0 得到解決,在 MIDI 2.0 中 volocity 從 0 - 128 擴展到 0 - 65535,信道從 16 個增加到 256 個,同時 MIDI 2.0 也支持 MPE 以及遠程控制。
如果 MIDI 2.0 和 MPE 這類現成的解決方案無法滿足你的需求,那么你可以考慮自己來拓展 MIDI 協議或者 MIDI 標準格式。目前來看,可靠的拓展方式有幾下幾種:
首先我們需要認識到 MIDI 的優點,MIDI 記錄的實際上是事件流,最適合的場景就是在現場演奏時用于硬件之間的通信。作為 MIDI 文件格式作為一種存儲格式,其優點是數據十分緊湊,體積較小。但 MIDI 的缺點是十分明顯的,一方面我們無法快速查詢、訪問其中某個具體內容的值:比如我們沒法快速找到某一個軌道的拍號,或者某個音的音高。
所以我的建議是,盡量避免在現場演奏場景之外使用 MIDI 文件格式,但可以在抽象上對齊 MIDI。在內存中我們盡量把 MIDI 文件轉化為實例對象,便于我們快速訪問。在需要持久化的場景下則可以使用更容易解析的 JSON 或者 MusicXML 格式。只有在用戶需要或者向其他編輯工具導出數據的時候,我們才考慮使用 MIDI 標準文件格式。
絕大部分這類問題可以通過不使用 MIDI 編碼來解決。原則很簡單,只要不涉及現場演奏場景和向其他工具導出數據,就避免使用 MIDI 編碼來做任何事情。只用確保在需要 MIDI 的場景可以導出 MIDI 文件就行。
如果你的開發工作涉及到音樂的“本體”部分,那么我建議多了解一些 MIDI 協議,因為雖然我們可能多數情況下不直接使用 MIDI 協議的編碼,但是 MIDI 的事件流是創作場景和存儲場景會大量用到的,同時 MIDI 中的多數抽象和概念是行業內通用的。
我的建議是用一個文檔維護所有的基礎字段和拓展字段,各項目在定義 Model 時盡量參考這個文檔。如果現有的拓展字段可以解決你的需求,就不要新增拓展字段。
由于單個字節表示的最大范圍為 0 - 256,所以在 MIDI 文件中表示較大數字時會采用可變長度數量。其每一個字節使用第 7 位表示這個字節是否為最后一個字節,1 表示不是最后一個字節,0 表示是最后一個字節, 0 - 6 位則作為有效位。
舉一個例子,數字 127 可以表示為 0111 1111 ,128 則表示為 1000 0001 0000 0000,這樣理論上可以表示的數字可以無限大,不過在實踐中通常不會使用超過 32 位。
總結一下就是:
7 位 | 0-6 位 |
是否為最后一個字節 | 有效位 |
note on 中的 velocity 實際上是按鍵的“觸發速率”,你可以把其視為從鍵盤能感知到下按到下按結束這個過程中的鍵程除以按下時間,note off 則是反向的“釋放速率”。速率的計算方式和更多細節可以參考這篇論文:The Interpretation of MIDI Velocity
萬維網,建立在internet上,全球性的、交互、多平臺、分布的信息資源。
wwwf組成:
雙標簽:<標簽名>內容</標簽名>
單標簽:<標簽名 />
注釋標簽:<!-- 注釋說明 -->
各種標簽,以html文檔結構來做:
<!doctype html> <!--文檔類型標簽-->
<html> --最外層的標簽
<head> <!--頭部標簽,包含編碼、標題等-->
<title>標題</title> <!--標題標簽-->
<meta charset="utf-8"/> <!--編碼格式-->
</head>
<body> <!--主體標簽,里面顯示的內容-->
<h1>哈哈哈哈</h1>
</body>
</html>
h1~h6,h1最大的,h6最小的
屬性:align,對齊方式
屬性值:left-左對齊,center-居中,right-右對齊
示例:
<h1 align="left">標題左對齊</h1>
<p>內容</p>
屬性:
-title:提示信息
-style:行內樣式
-dir:文字方向,默認從左向右,ltl或者rtl
示例:
<p title="這是提示信息" dir="ltl"></p>
屬性:
align:對齊方式,默認center居中對齊
color:顏色
width:寬度,可以取值百分比或者像素
size:高度,只能取值像素
noshade:取消陰影
示例:
<hr color="red" width="50%" size="10"/>
<b></b>和<strong></strong>
屬性:
src:圖片路徑
alt:圖片加載錯誤時提示信息
title:鼠標放在圖片上提示信息
width:寬度
border:邊框
示例:
<img src="1.jpg" alt="圖片加載失敗" title="提示信息"/>
屬性:
href:要跳轉的地址
示例:
<a href="https://www.baidu.com">百度一下</a>
*請認真填寫需求信息,我們會在24小時內與您取得聯系。