ルモーリン
ホーム 更新 Perl Sample サービス 雑談 鉄ゲタ Linux リンク 連絡先

Synthesizer V Pro(琴葉茜・葵)でAIきりたんを真似るスクリプト

2020-08-15

前回のスクリプト作成で当初の目標だった「AIきりたんのように歌う」というのが失敗に終わり、とりあえずピッチをシフトするスクリプトを作りました。
Synthesizer V Proのスクリプトを作った

AIきりたんは音声合成をする際にbapファイル、f0ファイル、mgcファイルも生成します。 f0はピッチ、mgcは声量が含まれているようです。 f0ファイルをSynthesizerV Proのピッチベンド、mgcファイルをラウドネスに設定すればAIきりたんのように歌ってくれるのではないかと考えました。

真似をするスクリプトを作りました。

-- AIきりたんの真似

local plugin_name = "AIきりたんの真似(Lua)"
local timeout = 100

function getClientInfo()
  return {
    name = plugin_name,
    category = "AIきりたん",
    author = "lemorin_jp",
    versionNumber = 1,
    minEditorVersion = 0
  }
end

function main()
  -- ダイアログの内容
  local myForm = {
    title = plugin_name,
    message = "ファイルのフルパス(Windowsは漢字NG)",
    buttons = "OkCancel",
    widgets = {
      {
        name = "f0file",
        type = "TextBox",
        label = "f0",
        default = "sample.f0"
      },
      {
        name = "mgcfile",
        type = "TextBox",
        label = "mgc",
        default = "sample.mgc"
      },
    }
  }

  -- ダイアログ表示(モーダル)
  local is_valid = false
  while not is_valid do
    local result = SV:showCustomDialog(myForm)
    if not result.status then break end

    -- Okならダイアログの設定をグローバル変数へ
    f0file = result.answers.f0file
    mgcfile = result.answers.mgcfile
    is_valid = validate(f0file, mgcfile)
    if is_valid then break end

    myForm.widgets[1].default = f0file
    myForm.widgets[2].default = mgcfile
  end

  if is_valid then
    -- 後でkiritanを開始
    SV:setTimeout(timeout, kiritan)
  else
    -- スクリプト終了
    SV:finish()
  end
end

function validate(f0, mgc)
  if not string.match(f0, "^[:\\%a]+%.f0$") then
    SV:showMessageBox(plugin_name, "f0ファイルではありません\n" .. f0)
    return false
  end

  local fh = io.open(f0, "rb")
  if not fh then
    SV:showMessageBox(plugin_name, "f0ファイルを開けません\n" .. f0)
    return false
  end
  fh:close()

  if not string.match(mgc, "^[:\\%a]+%.mgc$") then
    SV:showMessageBox(plugin_name, "mgcファイルではありません\n" .. mgc)
    return false
  end

  fh = io.open(mgc, "rb")
  if not fh then
    SV:showMessageBox(plugin_name, "mgcファイルを開けません\n" .. mgcfile)
    return false
  end
  fh:close()

  return true
end

function kiritan()
  track = SV:getMainEditor():getCurrentTrack()

  -- トラックを検証
  local notegroup = track:getGroupReference(1):getTarget()
  local numnote = notegroup:getNumNotes()
  if numnote < 2 then
    SV:showMessageBox(plugin_name, "トラックに音符がありません")
    -- スクリプト終了
    SV:finish()
    return
  end

  -- トラックの歌声をリセット
  track:getGroupReference(1):setVoice({
    tF0Left = 0,
    tF0Right = 0,
    dF0Left = 0,
    dF0Right = 0,
    tF0VbrStart = 0,
    tF0VbrLeft = 0,
    tF0VbrRight = 0,
    dF0Vbr = 0,
    fF0Vbr = 0,
    paramLoudness = 0,
    paramTension = 0,
    paramBreathiness = 0,
    paramGender = 0
  })

  -- トラックのノートのパラメータをリセット
  local i
  for i = 1, numnote do
    notegroup:getNote(i):setAttributes({
      tF0Offset = 0,
      tF0Left = 0,
      tF0Right = 0,
      dF0Left = 0,
      dF0Right = 0,
      tF0VbrStart = 0,
      tF0VbrLeft = 0,
      tF0VbrRight = 0,
      dF0Vbr = 0,
      pF0Vbr = 0,
      fF0Vbr = 0,
      tNoteOffset = 0
    })
  end

  piano_roll = SV:getMainEditor():getNavigation()

  -- 後でf0startを開始
  SV:setTimeout(timeout, f0start)
end

function f0start()
  trackAutomation = track:getGroupReference(1):getTarget():getParameter("Pitch Deviation")
  trackAutomation:removeAll()

  projectTimeAxis = SV:getProject():getTimeAxis()

  trackNoteGroup = track:getGroupReference(1):getTarget()
  numNote = trackNoteGroup:getNumNotes()
  curFreq = SV:pitch2Freq(trackNoteGroup:getNote(1):getPitch())

  nextIndex = 2
  nextNote = trackNoteGroup:getNote(nextIndex)
  blickNext = nextNote:getOnset()
  blickLast = nextNote:getEnd()
  nextFreq = SV:pitch2Freq(nextNote:getPitch())

  msPos = 0
  fh = io.open(f0file, "rb")
  dbl = fh:read(8)

  -- 後でf0progressを開始
  SV:setTimeout(timeout, f0process)
end

function f0process()
  -- 処理範囲を表示
  disp_scroll(msPos)

  -- 適当に5ms×100個分を処理
  local i
  for i = 1, 100 do
    if not dbl then
      break
    end

    local blick = projectTimeAxis:getBlickFromSeconds(msPos / 1000)
    if blickNext <= blick then
      curFreq = nextFreq

      if nextIndex <= numNote then
        nextNote = trackNoteGroup:getNote(nextIndex)
        nextIndex = nextIndex + 1
        blickNext = nextNote:getOnset()
        blickLast = nextNote:getEnd()
        nextFreq = SV:pitch2Freq(nextNote:getPitch())
      elseif blickLast < blick then
        dbl = nil
        break
      end
    end

    local f0 = string.unpack("<d", dbl)
    if 0 ~= f0 then
      trackAutomation:add(blick, 1200 * math.log(f0 / curFreq, 2.0))
    else
      trackAutomation:add(blick, 0)
    end

    dbl = fh:read(8)
    msPos = msPos + 5
  end

  -- 未処理の時間がある?
  if dbl then
    -- 処理継続
    SV:setTimeout(timeout, f0process)
  else
    -- 後でf0endを開始
    SV:setTimeout(timeout, f0end)
  end
end

function f0end()
  fh:close()
  -- 簡略化
  trackAutomation:simplify(0, track:getDuration())

  SV:showMessageBox(plugin_name, "ピッチベンドを更新しました")

  -- 後でmgcstartを開始
  SV:setTimeout(timeout, mgcstart)
end

function mgcstart()
  trackAutomation = track:getGroupReference(1):getTarget():getParameter("loudness")
  trackAutomation:removeAll()

  fh = io.open(mgcfile, "rb")

  is_end, mgc = read_mgc(fh)
  msPos = 0

  -- 後でmgcprocessを開始
  SV:setTimeout(timeout, mgcprocess)
end

function mgcprocess()
  -- 処理範囲を表示
  disp_scroll(msPos)

  -- 適当に5ms×100個分を処理
  local i
  for i = 1, 100 do
    if 1 == is_end then
      break
    end

    local blick = projectTimeAxis:getBlickFromSeconds(msPos / 1000)
    mgc = (7 + mgc) / (48 - 7) * 48
    if mgc < -48 then
      mgc = -48
    elseif 12 < mgc then
      mgc = 12
    end
    trackAutomation:add(blick, mgc)

    is_end, mgc = read_mgc(fh)
    msPos = msPos + 5
  end

  -- 未処理の時間がある?
  if 1 ~= is_end then
    -- 処理継続
    SV:setTimeout(timeout, mgcprocess)
  else
    -- 後でmgcendを開始
    SV:setTimeout(timeout, mgcend)
  end
end

function mgcend()
  fh:close()

  -- 簡略化
  trackAutomation:simplify(0, track:getDuration())

  SV:showMessageBox(plugin_name, "ラウドネスを更新しました")

  -- トラックの先頭に戻る
  disp_scroll(0)

  -- 後でkiritanendを開始
  SV:setTimeout(timeout, kiritanend)
end

function kiritanend()
  -- スクリプト終了
  SV:finish()
end

function read_mgc(fh)
  local mgc = 0
  local is_end = 0
  local dbl = fh:read(8)
  if not dbl then
     is_end = 1
  else
    mgc = string.unpack("<d", dbl)
  end

  fh:read(8 * 59)

  return is_end, mgc
end

function disp_scroll(msPos)
  local disp_blick = projectTimeAxis:getBlickFromSeconds(msPos / 1000)
  local range = piano_roll:getTimeViewRange()
  if disp_blick < range[1] or range[2] < disp_blick then
    piano_roll:setTimeLeft(disp_blick)
  end
end

AIきりたんと同じ歌があるトラックを選択してから メニュー「スクリプト|AIきりたん|AIきりたん(Lua)」を選択するとf0とmgcを指定するダイアログが表示されます。 それぞれフルパスで指定してください。 Windowsはパスにアルファベットを使ってください、漢字を使えません。 ボタン「確定」をクリックすると、トラックの歌声およびノートのプロパティをリセットして平坦な歌声にします。 続いてピッチベンドを設定、それからラウドネスを設定します。 設定には時間がかかるので、ピアノロールの設定している小節をスクロールしながら進めます。 ピアノロールのパラメータ(ピッチベンドとラウドネス)を表示させておくと、設定処理の経過が分かります。
表示されるダイアログ

AIきりたんのように歌う琴葉茜・葵さんです。

元の動画はこちらです。