[き舌プラグインを作ってみる]
-るるるるぁぁぁぁぁ-

111113:初版
  1. 始めに

    巻き舌は単純といえば単純なんだけど、手間がいちいちめんどくさい。楽したい一心で作ってみました。

    VOCALOID3 Job PluginはLuaと言う言語を使用してます。詳細はLuaで検索すると良いかと。自分もLuaはどんなもんかはあんまり分かってませんが、まあリファレンスマニュアルを見ながら作れば何とかなるでしょう。多分。Luaの特徴としてはC言語で言う構造体をテーブルと言う仕組みで表現しています。後は動的に宣言出来るから楽だね。

    Job Pluginの基本構造は、

    • manifest関数
      VOCALOID3にJob Pluginとして登録する際の登録情報
    • main関数
      実際の処理

    と単純です。

    そうそう、UTF-8じゃ無いと駄目です。自分はsakuraエディタで書きました。

    以下トラックって書いてあるかもだけど、実際はパートかな。

  2. 巻き舌プラグインの機能

    んじゃ巻き舌プラグインの機能ってのはどんなのにするかってーと。

    まずは巻き舌の実現方法~

    1. 巻き舌にしたいノート(音符)を見つける
    2. その直前に3つ64分音符を並べて置く。発音は3つとも"る"
    3. 3つのノートの後半128分音符分DYNを0にする
    4. 完成!

    こんな感じですねー。128分音符分どうにかするとか、エディタで拡大しないと無理なんで結構手間がかかってましたが、今後はJob Pluginで一発とかわくわくですかねー。

  3. manifest関数

    manifest用のテーブルを書いて、それを戻り値として返しておしまいです。あら簡単。

    function manifest()
        myManifest = {
            name          = "巻き舌",
            comment       = "選択した1つのnoteの前に巻き舌を入れる",
            author        = "",
            pluginID      = "",
            pluginVersion = "0.0.0.0",
            apiVersion    = "3.0.0.1"
        }
        
        return myManifest
    end
    

    nameは登録する名前、commentはプラグインの動作説明、authorは作者名、pluginVersionはプラグインのバージョン(下にある通りドットで4つに区切る必要があるっぽいです)、apiVersionは使用したapiのバージョンです。

    authorとpluginIDが空文字列ですが、authorは作った人の名前を入れてねー。pluginIDはGUID(別名UUID)を作成する必要がありますが、web上にgeneratorが転がってるのでそれを利用すればいいんじゃない?

  4. main関数

    まあサンプル(VOCALOID3インストールフォルダ内のJobPluginsフォルダにあります)見れば分かりますが、最初にこんな感じで書いてあります。ので踏襲しますw

    function main(processParam, envParam)
    	-- 実行時に渡されたパラメータを取得します.
    	local beginPosTick = processParam.beginPosTick	-- 選択範囲の始点時刻(ローカルTick).
    	local endPosTick   = processParam.endPosTick	-- 選択範囲の終点時刻(ローカルTick).
    	local songPosTick  = processParam.songPosTick	-- カレントソングポジション時刻(ローカルTick).
    
    	-- 実行時に渡された実行環境パラメータを取得します.
    	local scriptDir  = envParam.scriptDir	-- Luaスクリプトが配置されているディレクトリパス(末尾にデリミタ "\" を含む).
    	local scriptName = envParam.scriptName	-- Luaスクリプトのファイル名.
    	local tempDir    = envParam.tempDir		-- Luaプラグインが利用可能なテンポラリディレクトリパス(末尾にデリミタ "\" を含む).
    

    processParamはエディタから渡ってくる音系の情報、envParamはシステム的な情報ですね。個別のノートを複数選択しても範囲しか分かりません。今のところ。

    Tickは音を扱う為の最小時間単位ですね。VOCALOID3エディタでは、4分音符が480tickになっています。8分音符では240tick、全音符では1920tickになります。

    "--"は1行コメントです。

    	local noteEx     = {}
    	local noteExList = {}
    	local retCode
    	local idx
    

    変数の宣言をしてます。…まあスクリプトだからどこで宣言してもいいんだけどw ノートのリストを個別のノートとノートの集団を扱うのでnoteExとnoteExListを宣言。noteExはAPIマニュアルにある通りパラメータが複数あるのでテーブルです。関数の戻り値を保存するためのretCode、idxはnoteExListにデータを格納するための番号ですね。

    	-- 4分音符=480 tick→64分音符=30 tick
    	if(beginPosTick < 3 * 30) then
    		VSMessageBox("巻き舌を入れられません", 0)
    		return 1
    	end
    

    こっからが処理の本番。まずは基本のエラーチェックからw 選択範囲の先頭に空きが無いとノートを追加できないので確認。64分*3 = 64分音符は30tickなので 30 * 3 = 90tick分の空きが必要です。VSMessageBoxはダイアログ出力ですね。0のところはタイプです。main関数からreturnが0以外で返ると、Vocaloid3エディタはスクリプトで加えた変更を反映しないとのこと。

    	-- ノートを取得してノートイベント配列へ格納
    	local num, selidx
    
    	VSSeekToBeginNote()
    	idx = 1
    	num = 0
    	selidx = -1
    	retCode, noteEx = VSGetNextNoteEx()
    	while (retCode == 1) do
    		if(noteEx.posTick > endPosTick) then
    			retCode = 0
    			break
    		end
    		if(noteEx.posTick >= beginPosTick and noteEx.posTick + noteEx.durTick <= endPosTick) then
    			num = num + 1
    			if(num > 1) then
    				VSMessageBox("ノートは一つだけ選択してね", 0)
    				return 1
    			end
    
    			-- 選ばれた場所を更新
    			if(selidx == -1) then
    				selidx = idx
    			end
    		end
    		noteExList[idx] = noteEx
    		retCode, noteEx = VSGetNextNoteEx()
    		idx = idx + 1
    	end
    

    巻き舌プラグインでは一番でかい処理です。まあ大したことはないけどw

    ノートは2種類の表現があり、noteはパラメータ少し、noteExはフルパラメータになってます。で、note/noteEx型にはPosTickとdurTickが含まれます。posTickはノートの開始位置をTickで表したもの、durTickはノートの長さをTickで表したものです。これらと選択範囲を使って必要なノートを絞り込みます。

    num、selidxを追加で宣言。numは選択範囲にあるノートの個数を保存、selidxは、noteExListに格納されたノートの内、選択範囲の先頭ノート番号を保存します。

    ノートの取得方法はVSSeekToBeginNoteで一番最初に設定、VSGetNextNoteかVSGetNextNoteExで次を取得するような動きです。フォルダ取得何かでお馴染みの方法ですね。ほんとは選択範囲分だけノートがあればいいんだけど、めんどくさいので先頭の方もnoteExListに突っ込んでます。動きゃいいんだよ動きゃ。

    whileループでVSGetNextNoteExがエラー(空)になるまでぶん回ります。

    その次のifは範囲を越したらwhile終了。

    その次のifは選択範囲内かどうかを確認しています。選択範囲内なら個数カウント。

    次のifは選択範囲内のノート個数が1個以上ならエラー。複数は対応しませんよ。面倒なので。

    次のifで選択範囲内の最初のノートか判断し、最初ならnoteExListの現在のidxを覚えておきます。実際にはnoteExList[selidx]が処理対象のノートなわけです。

    その後はnoteExListにデータを追加して、次のノートを取りにいってidx(ノート追加場所)も1加算です。複数戻り値があるのは便利だなあ。

    	-- 選択ノート数確認
    	if(num == 0) then
    		VSMessageBox("ノートがありません", 0)
    		return 1
    	end
    

    選択したノートの個数0かどうかを確認。ここで引っかかることは多分無いと思いますが、一応w

    	-- 直前のノートとの距離確認
    	if(selidx > 1) then
    		if(noteExList[selidx].posTick -
    			(noteExList[selidx - 1].posTick +
    			noteExList[selidx - 1].durTick) < 3 * 30) then
    
    			VSMessageBox("巻き舌を入れられません", 0)
    			return 1
    		end
    	end
    

    直前のノートと接近してると、ノートを入れても合成に失敗するので、ある程度隙間があるかどうかを確認します(64分*3の90Tickね)。selidxが1ならばトラックの先頭ノートなので直前のノートは無いから処理スキップ。

    	-- 直前に(3)つ(64分)ノートを置いてDYNの後半128分音符分0にする
    	retCode = insert_ru(noteExList[selidx], -30)
    	retCode = retCode + insert_ru(noteExList[selidx], -60)
    	retCode = retCode + insert_ru(noteExList[selidx], -90)
    	if(retCode ~= 0) then
    		VSMessageBox("巻き舌を入れられません", 0)
    	end
    
    	-- 終了.
    	return retCode
    end
    

    さあここからが核心のノート追加とDYN削りなわけですが…あれ?insert_ruって何?…3回同じ処理があるので関数化してみました。別にループでもいいんですがw insert_ruでは選択した音符と"る"を追加するための相対tick数を指定しています。戻り値全部足して0以外なら3つの内のどれかがエラーですね。そして終了へ。main関数はこんな感じで終わります。

  5. insert_ru関数

    というわけで関数化された"る"追加部です。

    function insert_ru(note, diff)
    	-- ノート追加
    	local tmp = {}
    	tmp.posTick = note.posTick + diff
    	tmp.durTick = 30
    	tmp.noteNum = note.noteNum
    	tmp.velocity = 64
    	tmp.lyric = "る"
    	tmp.phonemes = "4 M"
    	retCode = VSInsertNote(tmp)
    	if(retCode ~= 1) then
    		VSMessageBox("ノート追加できない", 0)
    		return 1
    	end
    

    ノートを追加してますね。…あれ何でExじゃないのかって?面倒だからです。main関数だとnoteExなのは、欲張ってみました的展開の果て。必要なかったよw

    パラメータはposTick, durTick, noteNum, velocity, lyric, phonemesと最低これだけ要ります。posTickは引数として渡されたノートのposTickからdiff Tick分引いた値を入れます。durTickは64分音符分のTick、即ち30を入れときましょう。noteNumは音程です。選択したノートと同じ音程を入れときます。velocityは標準値64を入れときます。lyricは"る"。エディタ上で表示されます。phonemesは発音記号を入れます。"4 M"は"る"の発音です。lyricは飾りなのでphonemesの方が100倍は重要ですw

    VSInsertNoteで新規ノートを追加します。retCode見てますが、main側で空いてるの確認してるので、ここでは引っかからないはず。

    	-- DYN操作
    	local rtn, val
    	rtn, val = VSGetControlAt("DYN", tmp.posTick)
    	for i = 0, 15 - 1, 1 do
    		rtn = VSUpdateControlAt("DYN", tmp.posTick + 15 + i, 0)
    	end
    	rtn = VSUpdateControlAt("DYN", tmp.posTick + 30, val)
    
    	return 0
    end
    

    さて初めて出てきたコントロールパラメータ。コントロールパラメータはノートのように先頭から集めてくる方法もありますが、めんどくさいので時間指定するとそこのパラメータをとってきてくれたり書き換えてくれたりするAPI関数があります。ノートに比べると楽ですね。

    rtnとvalを宣言。rtnは戻り値を格納してますが…使ってませんねw valは追加したノートの先頭Tick+30時のDYN値を保存します。

    何でvalが必要かというと、コントロールパラメータを設定以降に他の設定が無いとずっとそのままになるからです。今回はDYNを0にしますが、それ以降にDYNのコントロールが無いと以降全部DYNが0になって音が出なくなるw それを防ぐために、事前に+30Tickのデータを取っておいて、手動でその値を後で再設定するわけです。VSGetControlAtで早速取得してますね。最初の引数はコントロールパラメータの種類、次が位置Tickです。

    次のforでは、追加したノートの後半128分音符(15Tick)分 DYNを0に変更しています。普通に書き換えるだけなので楽ですー。

    最後、さっきも書いた通りTick+30でvalに手動で書き換えてます。以上で完全終了です。

  6. んじゃ実行

    で、作った巻き舌プラグインの実行方法は下記の通りー。

    1. スクリプトはUTF-8にしたか、UTF-8だけどBOMとか付いてないかを確認。ファイル名の拡張子はとりあえずluaにしておきますか
    2. VOCALOID V3エディタの"ジョブ"→"Jobプラグインを管理"→"追加"でスクリプトを登録する
    3. VOCALOID V3エディタでノートを一つ選択
    4. "ジョブ"→"Jobプラグインを実行"→巻き舌を選択して実行
    5. すると何と言うことでしょう!発音が巻き舌に!

    あ、選択するノートの発音は"ら"行じゃ無いと意味無いよ!?

  7. 最後

    APIマニュアルはVOCALOIDストアで0円でもらえます。サンプルを見て改造したりすればそれなりに楽しめるんじゃないでしょうかね。何か既にぼかりす含めて大物も何個か準備されてるみたいだし公開解禁日が楽しみですねー。音声トラック側いじるとかめんどくさくて作る気しないんだけど、みんなよくやるねぇ。

    実際の動作サンプルはニコニコ動画のsm15966366(画面だけ))、sm16797759(音声付き)に上がってます。

    出来が微妙とか解説が分かりにくいとか気にしない!突貫工事だし!

    それからLuaは気に入りました。シンプルで良いね。

  8. おまけ

    ここでも公開できるようになったので、products/制作物のページで公開。