Midido User wiki

Midido ♫¶

一个用于读写MIDI文件,且具有十分友好的API的Lua扩库。它提供的MIDI数据是贴近文件底层的,因此并不需要用户担心那些诸如增量时间(Delta Time)和音符信号(NoteOn/NoteOff)这样的技术问题。它的方法是直观且具体的,同时对象数据也具有良好的可读性。 值得一提的是——这个扩展库不需要 任何 依赖。
编写和翻译它的初衷是为了让大家都能尝试以一种更加方便的方法写出自己的乐谱,人人都是作曲家,都能享受音乐的魅力。

这里是Midido的API介绍wiki,你可以在这里知晓Midido绝大部分API的用途、原理,以便在编写乐谱脚本时能更快更准确的达到自己想要的效果。废话就不多说了,让我们快点开始吧~

需要注意的是,本文主要针对那些有一定 Lua 编写基础的脚本作者,同时我也十分希望这些作者在阅读完本教程后能够尝试着写写自己的 midi 乐谱,并为乐谱库作出一些贡献。

第一章

1. 开始,安装和调用

你可以在下面的两个地址里下载到Midido扩展库:

  1. 论坛的Midido帖子:https://forum.kokona.tech/d/1255
  2. 原帖:https://jyunko.cn/2022/10/03/wiki-of-Midido/ 密码:wiki-of-Midido

那么在使用前,你需要将脚本全部解压至 DiceQQ\plugin\ 目录下,或者 Diceki\lua\ 也可以,但这边特别推荐放在 DiceQQ\plugin\ 目录下,因为这涉及到生成文件的路径问题,同时本教程的示例代码也是根据 DiceQQ\plugin\ 目录来编写的。

调用:

local Midido = require ('Midido')

2. 编写一个简单的(单音轨)脚本

那么现在来编写一个简单的生成midi的脚本吧~
新建 test1.lua文件,将下面的代码复制粘贴进去,保存,随后使用 .system load命令重载。
在重载时 test1.lua会被执行,将在 DiceQQ\plugin\Midido\project\ 文件夹生成 test1.mid 文件。

local Midido = require ('Midido')
local Track = Midido.Track
local NoteEvent = Midido.NoteEvent
local Writer = Midido.Writer

-- 创建通道音轨(Track)实例
local track = Track.new()

-- 将音符存为notes表中的键值(必须是指定的八度音阶音符)
local notes = {'C3', 'D3', 'E3', 'F3', 'G3', 'A3', 'B3', 'C4'}

-- 将音符添加到音轨
track:add_events(NoteEvent.new({pitch = notes, sequential = true}))

-- 遍历音轨
local writer = Writer.new(track)

-- 在`Midido\project\`内生成一个名为《test1》的MIDI文件
writer:save_MIDI('test1',getDiceDir().."\\plugin\\Midido\\project")

由于代码中已经有一些注释,因此不需要对其再进行解释说明。 这是 MIDI 文件构建的基本步骤。

3. 剖析、分解示例脚本

通过运行上述脚本,我们已经知晓了MIDI 文件构建的最基本步骤:

1. 创建通道音轨.
2. 设置音轨音符.
3. 写入通道音轨.
4. 写文件并导出.

为了方便理解,我们对 test1.lua内的API逐行说明:

3.1 调用主库

local Midido = require ('Midido')

Midido.lua
通过这个主库可以调用其它所有的库。写一个MIDI序列,无非是读取并写入的问题,因此将其它支持库均整合起来,它仅提供两个非常有用的函数:

  1. Midido.get_MIDI_tracks(path)

读取MIDI序列的所有轨道,
并将它们转换为Midido的轨道实例.
@string path:MIDI文件的路径.
@return->由轨道组成的数组.

  1. Midido.add_tracks_to_MIDI(input, tracks, output)

向MIDI序列写入轨道.
@string input:原MIDI文件的路径.
@param tracks:一个轨道实例或由轨道组成的表.
@string[opt=input]:输出写好后的MIDI序列.
@return:返回布尔值

3.2 主库调用的内容

local Track = Midido.Track
local NoteEvent = Midido.NoteEvent
local Writer = Midido.Writer

Midido.lua 调用了所有库,因此在 require('Midido') 以后可以直接赋值使用。

Midido.Util = require 'Midido.Util'
Midido.Chunk = require 'Midido.Chunk'
Midido.Track = require 'Midido.Track'
Midido.Writer = require 'Midido.Writer'
Midido.Constants = require 'Midido.Constants'
Midido.MetaEvent = require 'Midido.MetaEvent'
Midido.NoteEvent = require 'Midido.NoteEvent'
Midido.NoteOnEvent = require 'Midido.NoteOnEvent'
Midido.NoteOffEvent = require 'Midido.NoteOffEvent'
Midido.ArbitraryEvent = require 'Midido.ArbitraryEvent'
Midido.ProgramChangeEvent = require 'Midido.ProgramChangeEvent'

3.3 创建通道音轨(Track)

local track = Track.new()

Track.new(name)Track.lua 内的第一个函数,其作用是创建一条通道音轨,@string [opt=name]是可选参数,用于自定义音轨名称。

通常来说,如果没有什么特殊需求(比如设置调号拍号等),只需要使用 Track.new() 以及 Track:add_events()(此方法会在下文说明) 即可。
如果想知道更多内容,你可以查看 Midido\Track.lua ,里面有对各个函数的详细介绍。

3.4 添加音符到音轨

local notes = {'C3', 'D3', 'E3', 'F3', 'G3', 'A3', 'B3', 'C4'}

将这些音的指定音名存入一个表。

Q1: 可以填入那些音名?

A1: 在 Constans.lua 第36-49行定义了可以使用的音名写法:

local table_notes = {
   {'C','B#'},
   {'C#','Db'},
   {'D'},
   {'D#','Eb'},
   {'E','Fb'},
   {'F','E#'},
   {'F#','Gb'},
   {'G'},
   {'G#','Ab'},
   {'A'},
   {'A#','Bb'},
   {'B','Cb'},
}

Q2: 为什么存入表内?

A2: 目的当然是为了方便以及偷懒,但从lua编写角度来说,这主要是因为 NoteEvent.new() 可以将表作为第一个参数使用。下面我将会介绍 NoteEvent.new()

NoteEvent.new(fields)

这是 NoteEvent.lua 内的第一个函数。参数 fields 是一个包含7个可选参数(比如pitch、sequential)和1个默认参数(type='note')的表。pitch 即音高,放在 Track 内即编曲家都会描述的通道音轨,它可以是一个表。sequential(数据信号流/时序) 是一个布尔值,为 true 时代表让 MIDI 数据按照每个 MIDI 设备插入音轨的顺序流经它们。
NoteEvent.lua 用于并向处理 MIDI 的 NoteOn 和 NoteOff 事件,对音符的操作十分全面,如果你对此感兴趣也可以去看看源码,里面同样有十分详细的介绍、

track:add_events(NoteEvent.new({pitch = notes, sequential = true}))

这是 Track:add_events() 方法,可以将事件列表(或单个事件)添加到通道音轨. 这些事件可以是 MetaEventsNoteEventsProgramChangeEvents

3.5 使用编辑器(Writer)遍历音轨

Writer.new(tracks)

这是 Writer.lua 内的第一个函数,用于向 MIDI 文件写入通道音轨,tracks参数可以说单个通道音轨对象或包含多个通道音轨的表.

3.6 使用编辑器(Writer)生成文件

Writer:save_MIDI(title, directory)

这是 Writer.lua 内的最后一个方法,用于写 MIDI 文件,其中 title 是文件的名字,directory 是可选参数,表述保存的路径。

好了,看到这里本教程的第一部分算是结束了,快写出你的第一个 MIDI 文件吧~

4. 附录

一些可能有用的信息

4.1 文件结构

DiceQQ
└───plugin
       │  │Midido.lua
       │
       └───Midido
              │   │Writer.lua
              │   │Util.lua
              │   │utf8.lua
              │   │Track.lua
              │   │ProgramChangeEvent.lua
              │   │NoteOnEvent.lua
              │   │NoteOffEvent.lua
              │   │NoteEvent.lua
              │   │MetaEvent.lua
              │   │Constants.lua
              │   │Chunk.lua
              │   │ArbitraryEvent.lua
              │
              └───bit
              │     │numberlua.lua
              │     │native_bitwise.lua
              │
              └───project

4.2 参考

5. 提及的脚本

test1.lua 直接解压至plugin\文件夹下再使用.system load命令重载即可,
生成的midi文件路径:DiceQQ\plugin\Midido\project\

test1.zip
569B

第一章结束

to be…


Lastest Edit:2023/01/31 13:55 by 简律纯

25 天 后
6 天 后

虽然打算去自力更生啃NoteEvent.lua文件,但考虑小白很可能看不懂还是来问问大佬,
休止符和四分音符、八分音符那些怎么表示呢

    Lazzyie

    Uh-Uh

    不好意思,在那以后太忙了,没来得及继续写用户wiki,原谅我的疏忽。
    NoteEvent.lua最好是看一看,有许多可选参数,比如pitch用来规范音名,duration用来规范时值,rest用来规范休止。

    这里有一个完整的示例:

    local Midido = require ('Midido')
    local Track = Midido.Track
    local NoteEvent = Midido.NoteEvent
    local Writer = Midido.Writer
    
    local track = Track.new("Intro")
    track:set_copyright("(C) Hsiang Nianian")
    track:set_instrument_name("Acoustic Guitar")
    
    local function note(pitch, duration)
       return NoteEvent.new({pitch = pitch, duration = tostring(duration)})
       -- NoteEvent.note里的duration参数用于调整音符长度,数字越小越长
    end
    
    local function rest(rest)
        return NoteEvent.new({pitch = "C5", rest=tostring(rest)})
        --这里的pitch可以随便填,ABCDEFG都可以,但不能省略
     end
    
    local A3 = note('A3')
    local C4 = note('C4')
    local E4 = note('E4')
    local A4 = note('A4')
    local Ab3_B4 = note({'G#3', 'B4'})
    local B4 = note('B4')
    local G3_C5 = note({'G3', 'C5'})
    local C5 = note('C5')
    local Gb3_Gb4 = note({'F#3', 'F#4'})
    local D4 = note('D4')
    local Gb4 = note('F#4')
    local F3_E4 = note({'F3', 'E4'})
    local long_C4 = note('C4', 2) 
    
    local Am = {'A2', 'E3', 'A3', 'C4'}
    
    local chord_GB = note({'B2', 'D3', 'G3', 'B3'})
    local chord_Am = note(Am)
    local long_chord_Am = note(Am, 1)
    
    track:add_events({
       A3,Gb3_Gb4,rest(2),long_chord_Am
    })
    
    local writer = Writer.new(track)
    writer:save_MIDI('stairway_to_heaven',getDiceDir().."\\plugin\\Midido\\project")

    你可以改变

    track:add_events({
       A3,Gb3_Gb4,rest(2),long_chord_Am
    })

    里的rest()参数来调整休止长度。(这是一个自定义休止函数,最终效果如下):

    题外话

    关于note的更多内容请看下表,但想要灵活运用还需自行探索。
    我很真诚的邀请你在这里做出贡献,替大家写第二章用户wiki,我很期待你的pr。

    若不太懂如何编写wiki那么可以搜索QQ群971050440,我在那里等你的申请

      简律纯 非常感谢!!
      菜鸡弄了一天四处缝合终于搞了个凑合能用的脚本,虽然感觉写得超烂但要不还是发上来吧

      test.bin
      10kB

      (下载之后格式改成lua)
      还是有很多东西不太懂,还是太菜了

        Lazzyie

        阿b快告诉我怎么用啊啊啊啊啊啊啊啊——

        我太忙懒得一遍一遍看过去了x

          简律纯 啊是我写的bug百出的脚本吗…..
          如果是的话,忘记说了,输入.music_long的格式最后结尾要有/END/
          (实在不会正则匹配所以就这样了)
          其他的话,可能改一下文件路径,在plugin下新建data文件夹,其中新建Notes.txt,但这些大佬们肯定都知道


          然后应该就没问题了吧,我这边运行貌似挺正常的x

          plus:又检查了一下,delete函数里的string.match后面应该改成.music_delete,我本来写了个查找记录的函数,但因为没什么用就删了x,delete的查找是直接从那儿移来的
          以及如果觉得有必要的话,那个函数是这样的

          function show_notes(msg)
              local letter = read_file(notes_path) -- 读取
              j = json.decode(letter)
              local length = #j.notes["list"]
              local check_name = string.match(msg.fromMsg,"^[%s]*(.-)[%s]*$",#".music_show"+1)  --标题索引
              if(check_name=="")then
                  return "请回复.music_show标题/序号\n目前存储乐谱数:"..length
              else
                  local tag = j.notes["list"]
                  local logs = #tag
                  local flag = 0
                  local num={}
          
                  for i=1, logs do
                      local name = j.notes["list"][i]["name"]
                      if(check_name==name)then --找到记录
                          flag = flag + 1 --可能存在多条重名记录,全部显示
                          num[flag] = i --num:记录编号。可能存在多条,所以数组存储
                      end
                  end
          
                  if(flag==0)then --没有记录或非乐谱名输入
                      num[1] = tonumber(string.match(msg.fromMsg,"%d*",#".music_show"+1))
                      if(num[1]==nil or num[1]>length)then --找到有效记录
                          return "无此记录" --无对应输入
                      end
                  end
          
                  local size = #num
                  for i=1, size do
                      local number = num[i] --一条条来
          
                      text = ""
                      name = j.notes["list"][number]["name"]
                      info = j.notes["list"][number]["data"]
                      amount = #info
              
                      for i=1, amount do	--table换成string输出。又一层嵌套。太累赘了
                          tbl = info[i]["note"] --这是个table,就像info
                          mess = #tbl
                          txt = "("
                          for i=1, mess do
                              txt = txt..tbl[i]
                              if(i==mess)then
                                  txt = txt..")"
                              else txt = txt..", "
                              end
                          end
          
                          text = text..txt
                          if(i<amount)then
                              text = text..", "
                          end
                      end
                      return "本条记录为“"..name.."”,存储的音符:"..text.."\n"
          
                  end
              end
          end
          msg_order[".music_show"]="show_notes"
          ````
          
          ~~别的不说,这个脚本是真麻烦,很难用,我也觉得~~

            Lazzyie 阿b,我看了一下,也试了,很不错。
            但提几个建议:

            1. plugin\data\文件夹的缺失会导致报错,因此建议在.music初始化的时候先判断一下该文件夹是否存在,再进行之后的一系列操作。
            2. 铁子,.music_save.music_off我觉得可以放在一起?结束的时候保存,提出这个建议的原因是我不理解保存midi结束输入的顺序是怎样的,以及,这二者究竟有什么区别。
            3. 帮助指令看不懂可能是我比较笨

              简律纯 .music_off中是包含了保存的(如果看代码的话,是直接把.music_save当函数调用了),.music_save是个生成mid文件的小函数,分开是因为有时候写到一半想看看已经输入的部分出的效果,然后save了之后还能继续写,off就不行了
              不是您的问题,是因为我真的写得很粗糙

              3 个月 后
              说点什么吧...