ルモーリン

Synthesizer V Pro用レコーディングモーション生成プラグイン

投稿:2021-10-21、更新:2021-11-07

Synthesizer V Proで弦巻マキちゃん(弦巻マキAI)に歌ってもらっても、その楽曲用のMMD(MikuMikuDance)モーションがない。 かといってモーションを自作できる技量もない。 止め絵(イラスト)をお借りしても良い。 けれど動く弦巻マキちゃんを見たい、見せたい❤ そうだ、レコーディングスタジオで収録してる風なモーションなら動きが単純だし、できるんじゃないか?

2021-11-07 【変更点】 ・グループに対応 ・手を上げる挙動の所要時間を設定(速さを設定)

プラグインが1000行越えたので先に使い方を説明します。 初めに出力したいトラックを選択してメニュー「スクリプト|MMD|レコーディングモーション生成(Lua)」を選択してください。 設定ダイアログが表示されます。
ダイアログ
上から順に

  • モーションを出力するvmdファイル
  • 首を拍に合わせて左右に振る
  • 首を発音に合わせて上下に振る
  • 体を小節に合わせて左右に振る
  • 腕を上げる割合
  • 腕を上げる時間
つみだんご様制作の公式弦巻マキちゃんSynthesizer V仕様モデルの場合はデフォルトで良い感じに動いてくれますから、 一度デフォルトで出力してみて、そこから加減してみるのをお勧めします。 モデルや楽曲で動かしたい範囲が違いますから、上限の角度は「ここまで動かさないだろ。」という程大きくしてあります。 発声なし区間が3小節以上あれば間奏と判断して腕を下げるモーションも生成されます。 腕を上げる時間を短くすると素早く上げて、長くするとゆっくり上げます。 フレーズ間が短い場合は途中で手を止めます。

こちらをご覧ください。 レコーディングのモーションの他に、まばたきはランダム登録、口パク(リップモーション)はリップモーション生成プラグインで生成しました。

  1. バグがあるのが前提です
  2. Synthesizer V Proのプロジェクトを壊したり
  3. vmdファイルの出力が止まらずに巨大なファイルが生成されたり
  4. MMDで読み込むとMMDのプロジェクトを壊すかも知れません
  5. くれぐれもバックアップを確保してから作業してください
  6. プラグインの元ネタとして好きに書き替えてお使いください
  7. Synthesizer V Proのプラグインがもっと増えますように

-- レコーディングモーション生成

local plugin_name = "レコーディングモーション生成(Lua)"

function getClientInfo()
	return {
		name = plugin_name,
		category = "MMD",
		author = "lemorin_jp",
		versionNumber = 2,
		minEditorVersion = 0
	}
end

local fmt_long = "<I4"
local fmt_float = "<f"
local fmt_byte = "B"

-- 設定
local preferences = {
	left_right = 1.5,
	up_down = 3.0,
	trunk = 0.5,
	arm_ratio = 1.0,
	arm_up_time = 0.5,
}

-- 変換できる文字列
local check_born_name = [[
レコーディング

上半身2
首
頭

左腕
右腕
左ひじ
右ひじ
左手捩
右手捩
左手首
右手首

左親指0
左親指1
左親指2
左人指1
左人指2
左人指3
左中指1
左中指2
左中指3
左薬指1
左薬指2
左薬指3
左小指1
左小指2
左小指3
右親指0
右親指1
右親指2
右人指1
右人指2
右人指3
右中指1
右中指2
右中指3
右薬指1
右薬指2
右薬指3
右小指1
右小指2
右小指3

下半身
]]

local sjis_char = {
	["\n"] = "\n", -- 文字列チェック用(変換に使わない)

	["2"] = "\x32",
	["ー"] = "\x81\x5b",
	["じ"] = "\x82\xb6",
	["ひ"] = "\x82\xd0",
	["0"] = "\x82\x4f",
	["1"] = "\x82\x50",
	["2"] = "\x82\x51",
	["3"] = "\x82\x52",
	["ィ"] = "\x83\x42",
	["グ"] = "\x83\x4f",
	["コ"] = "\x83\x52",
	["デ"] = "\x83\x66",
	["レ"] = "\x83\x8c",
	["ン"] = "\x83\x93",
	["右"] = "\x89\x45",
	["下"] = "\x89\xba",
	["左"] = "\x8d\xb6",
	["指"] = "\x8e\x77",
	["手"] = "\x8e\xe8",
	["首"] = "\x8e\xf1",
	["小"] = "\x8f\xac",
	["上"] = "\x8f\xe3",
	["親"] = "\x90\x65",
	["人"] = "\x90\x6c",
	["身"] = "\x90\x67",
	["中"] = "\x92\x86",
	["頭"] = "\x93\xaa",
	["半"] = "\x94\xbc",
	["薬"] = "\x96\xf2",
	["腕"] = "\x98\x72",
	["捩"] = "\x9d\x80",
}
--[[
	[""] = "\x\x",
]]

function main()
	-- SJISに変換できるかチェック
	local checkd = utf8_2_sjis(check_born_name)

	-- プロジェクトのパス
	local path_func = string.gmatch(SV:getProject():getFileName(), ".+\\")
	local project_path = path_func()
	-- プロジェクトがない場合
	if nil == project_path then
		project_path = ""
	end

	-- ダイアログの内容
	local myForm = {
		title = plugin_name,
		message = "レコーディングモーション",
		buttons = "OkCancel",
		widgets = {
			{
				type = "TextBox",
				label = "vmdファイル Windowsは漢字NG",
				name = "vmdfile",
				default = project_path .. "recordingmotion.vmd",
			},
			{
				type = "Slider",
				label = "首を振る(左右)",
				name = "left_right",
				format = "%5.1f°",
				minValue = 0.1,
				maxValue = 10.0,
				interval = 0.1,
				default = preferences.left_right,
			},
			{
				type = "Slider",
				label = "首を振る(上下)",
				name = "up_down",
				format = "%5.1f°",
				minValue = 0.1,
				maxValue = 10.0,
				interval = 0.1,
				default = preferences.up_down,
			},
			{
				type = "Slider",
				label = "体の揺れ",
				name = "trunk",
				format = "%5.1f°",
				minValue = 0.1,
				maxValue = 5.0,
				interval = 0.1,
				default = preferences.trunk,
			},
			{
				type = "Slider",
				label = "腕を上げる角度(比率)",
				name = "arm_ratio",
				format = "%3.0f%%",
				minValue = 1,
				maxValue = 100,
				interval = 1,
				default = math.floor(preferences.arm_ratio * 100),
			},
			{
				type = "Slider",
				label = "腕を上げる時間",
				name = "arm_up_time",
				format = "%3.1f秒",
				minValue = 0.1,
				maxValue = 1.0,
				interval = 0.1,
				default = preferences.arm_up_time,
			},
		}
	}

	-- ダイアログ表示(モーダル)
	local result = SV:showCustomDialog(myForm)
	if result.status then
		-- 設定内容を拾う
		local vmdfile = result.answers.vmdfile
		preferences.left_right = result.answers.left_right
		preferences.up_down = result.answers.up_down
		preferences.trunk = result.answers.trunk
		preferences.arm_ratio = result.answers.arm_ratio / 100

		-- 出力先に同名ファイルがないか、上書き確認した場合
		local isgo = false
		if file_exists(vmdfile) then
			if SV:showOkCancelBox(plugin_name, "vmdファイルが既にあります。上書きしてよいですか?\n" .. vmdfile) then
				isgo = true
			end
		else
			isgo = true
		end
		if isgo then
			local phrase, note = grab_phrase()
			local motion_phrase, kubi_phrase = phrase2motion(phrase)
			local kubi_note = note2motion(note)

			-- 2つの首モーションを合成する
			local motion_note = kubi2motion(kubi_phrase, kubi_note)

			-- 体幹モーション
			local motion_trunk = create_trunk()

			-- 全モーションをフレーム順にまとめる
			local motion = {}
			local i
			for i = 1, #motion_phrase do
				table.insert(motion, motion_phrase[i])
			end
			for i = 1, #motion_note do
				table.insert(motion, motion_note[i])
			end
			for i = 1, #motion_trunk do
				table.insert(motion, motion_trunk[i])
			end
			-- フレーム順に並べ替え、同フレームの場合はボーン名順
			table.sort(motion,
				function (a, b)
					if a.frame ~= b.frame then
						return a.frame < b.frame
					end
					return a.bone < b.bone
				end
			)

			local rc = write_vmd(vmdfile, motion)
			if 1 == rc then
				SV:showMessageBox(plugin_name, "vmdファイルを出力しました")
			else
				SV:showMessageBox(plugin_name, "vmdファイルを開けません\n" .. vmdfile)
			end
		end
	end

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

-- 体幹モーション生成
function create_trunk()
	local motion = {}

	-- 歌い終わった後も揺らすために、プロジェクト全体の長さを使う
	local project = SV:getProject()
	local timeaxis = project:getTimeAxis()
	local size_second = timeaxis:getSecondsFromBlick(project:getDuration())

	local trunk_home = {
		{"上半身2",	0.0,	0.0,	0.0},
		{"頭",		0.0,	0.0,	0.0},
		{"下半身",	0.0,	0.0, 	0.0},
	}
	local trunk_left = {
		{"上半身2",	0.0,	0.0,	preferences.trunk},
		{"頭",		0.0,	0.0,	preferences.trunk},
		{"下半身",	0.0,	0.0, 	preferences.trunk},
	}
	local trunk_right = {
		{"上半身2",	0.0,	0.0,	-preferences.trunk},
		{"頭",		0.0,	0.0,	-preferences.trunk},
		{"下半身",	0.0,	0.0, 	-preferences.trunk},
	}

	-- 最初は直立
	local second = 0
	local frame = math.floor(second * 30)
	insert_form(motion, frame, trunk_home)
	second = seek_measure(second, 1)

	local isLeft = 1
	while second < size_second do
		-- 小節の頭で左右に揺れる
		frame = math.floor(second * 30)
		if 1 == isLeft then
			insert_form(motion, frame, trunk_left)
		else
			insert_form(motion, frame, trunk_right)
		end
		isLeft = 1 - isLeft

		second = seek_measure(second, 1)
	end

	-- 最後は直立に戻る
	frame = math.floor(size_second * 30)
	insert_form(motion, frame, trunk_home)

	return motion
end

-- 1つのボーンで2つのモーションを合成
function kubi2motion(phrase, note)
	local motion = {}

	table.sort(phrase,
		function (a, b)
			return a.frame < b.frame
		end
	)

	table.sort(note,
		function (a, b)
			return a.frame < b.frame
		end
	)

	local frame_max = phrase[#phrase].frame
	if frame_max < note[#note].frame then
		frame_max = note[#note].frame
	end

	local bone = "首"

	table.insert(phrase, {
		frame = frame_max + 1,
		x = phrase[#phrase].x,
		y = phrase[#phrase].y,
		z = phrase[#phrase].z,
	})
	local p = 1
	local p_step = 0
	local p_x = 0
	local p_y = 0
	local p_z = 0

	table.insert(note, {
		frame = frame_max + 1,
		x = note[#note].x,
		y = note[#note].y,
		z = note[#note].z,
	})
	local n = 1
	local n_step = 0
	local n_x = 0
	local n_y = 0
	local n_z = 0

	local frame
	for frame = 0, frame_max do
		local is_motion = false
		while phrase[p + 1].frame < frame do
			p = p + 1
			p_step = 0
			is_motion = true
		end

		while note[n + 1].frame < frame do
			n = n + 1
			n_step = 0
			is_motion = true
		end

		if is_motion then
			local x
			if nil ~= p_x and nil ~= n_x then
				x = (p_x + n_x) / 2
			elseif nil ~= p_x then
				x = p_x
			elseif nil ~= n_x then
				x = n_x
			else
				x = 0.0
			end

			local y
			if nil ~= p_y and nil ~= n_y then
				y = (p_y + n_y) / 2
			elseif nil ~= p_y then
				y = p_y
			elseif nil ~= n_y then
				y = n_y
			else
				y = 0.0
			end

			local z
			if nil ~= p_z and nil ~= n_z then
				z = (p_z + n_z) / 2
			elseif nil ~= p_z then
				z = p_z
			elseif nil ~= n_z then
				z = n_z
			else
				z = 0.0
			end

			local kubi = {
				{bone,	x,	y,	z},
			}
			insert_form(motion, frame, kubi)
		end

		if phrase[p].frame < phrase[p + 1].frame then
			local dist = phrase[p + 1].frame - phrase[p].frame
			if nil ~= phrase[p].x then
				p_x = phrase[p].x - phrase[p].x * p_step / dist + phrase[p + 1].x * p_step / dist
			end
			if nil ~= phrase[p].y then
				p_y = phrase[p].y - phrase[p].y * p_step / dist + phrase[p + 1].y * p_step / dist
			end
			if nil ~= phrase[p].z then
				p_z = phrase[p].z - phrase[p].z * p_step / dist + phrase[p + 1].z * p_step / dist
			end
		end

		if note[n].frame < note[n + 1].frame then
			local dist = note[n + 1].frame - note[n].frame
			if nil ~= note[n].x then
				n_x = note[n].x - note[n].x * n_step / dist + note[n + 1].x * n_step / dist
			end
			if nil ~= note[n].y then
				n_y = note[n].y - note[n].y * n_step / dist + note[n + 1].y * n_step / dist
			end
			if nil ~= note[n].z then
				n_z = note[n].z - note[n].z * n_step / dist + note[n + 1].z * n_step / dist
			end
		end

		p_step = p_step + 1
		n_step = n_step + 1
	end

	return motion
end

-- ファイルの有無チェック
function file_exists(file)
	local result = false
	local fh = io.open(file, "rb")
	if nil ~= fh then
		local buff = fh:read(1)
		if 0 < string.len(buff) then
			result = true
		end
		fh:close(fh)
	end
	return result
end

-- フレーズ収集
function grab_phrase()
	-- 選択中のトラックのメイングループ
	local track = SV:getMainEditor():getCurrentTrack()
	local timeaxis = SV:getProject():getTimeAxis()

	local track_note = {}
	local groupnum = track:getNumGroups()
	local group_index
	for group_index = 1, groupnum do
		local notegroup_ref = track:getGroupReference(group_index)
		local offset_blick = notegroup_ref:getTimeOffset()
		local notegroup = notegroup_ref:getTarget()
		local num_note = notegroup:getNumNotes()
		local note_index
		for note_index = 1, num_note do
			local note = notegroup:getNote(note_index)
			local n = {
				start = offset_blick + note:getOnset(),
				stop = offset_blick + note:getEnd(),
			}
			table.insert(track_note, n)
		end
	end
	table.sort(track_note,
		function (a, b)
			return a.start < b.start
		end
	)

	-- 選択中のトラックのグループ
	local phrase ={}
	local note_list = {}
	local phrase_start
	local phrase_stop
	local pronunciation_last
	local note_index
	for note_index = 1, #track_note do
		-- 音符1個
		local note = track_note[note_index]

		if nil == phrase_start then
			phrase_start = note.start
			pronunciation_last = phrase_start
			phrase_stop = note.stop
		end

		if note.start <= phrase_stop then
			phrase_stop = note.stop
			pronunciation_last = note.start
		else
			local p = {
				start = timeaxis:getSecondsFromBlick(phrase_start),
				last = timeaxis:getSecondsFromBlick(pronunciation_last),
				stop = timeaxis:getSecondsFromBlick(phrase_stop)
			}
			table.insert(phrase, p)

			phrase_start = note.start
			pronunciation_last = phrase_start
			phrase_stop = note.stop
		end
 
		local note_single = {
			start = timeaxis:getSecondsFromBlick(note.start),
			stop = timeaxis:getSecondsFromBlick(note.stop),
		}
		table.insert(note_list, note_single)
	end
	if nil ~= phrase_start then
		local p = {
			start = timeaxis:getSecondsFromBlick(phrase_start),
			last = timeaxis:getSecondsFromBlick(pronunciation_last),
			stop = timeaxis:getSecondsFromBlick(phrase_stop)
		}
		table.insert(phrase, p)
	end

	SV:showMessageBox(plugin_name, "音符数 " .. #track_note .. "\nフレーズ数 " .. #phrase)

	return phrase, note_list
end

-- フレーズからモーション
function phrase2motion(phrase)
	local timeaxis = SV:getProject():getTimeAxis()
	local motion = {}
	local kubi = {}

	-- 気を付け開始
	local frame = math.floor(0 * 30)
	local kiwotsuke = {
		{"左腕",	0.0,	0.0,	42.0},
		{"左ひじ",	0.0,	0.0,	0.0},
		{"左手捩",	0.0,	0.0, 	0.0},
		{"左手首",	0.0,	0.0, 	10.0},
		{"右腕",	0.0,	0.0,	-45.0},
		{"右ひじ",	0.0,	0.0,	0.0},
		{"右手捩",	0.0,	0.0, 	0.0},
		{"右手首",	0.0,	0.0, 	-10.0},

		{"左親指0",	0.0,	0.0,	0.0},
		{"左人指1",	0.0,	0.0,	0.0},
		{"左人指2",	0.0,	0.0,	0.0},
		{"左人指3",	0.0,	0.0,	0.0},
		{"左中指1",	0.0,	0.0,	0.0},
		{"左中指2",	0.0,	0.0,	0.0},
		{"左中指3",	0.0,	0.0,	0.0},
		{"左薬指1",	0.0,	0.0,	0.0},
		{"左薬指2",	0.0,	0.0,	0.0},
		{"左薬指3",	0.0,	0.0,	0.0},
		{"左小指1",	0.0,	0.0,	0.0},
		{"左小指2",	0.0,	0.0,	0.0},
		{"左小指3",	0.0,	0.0,	0.0},

		{"右親指0",	0.0,	0.0,	0.0},
		{"右人指1",	0.0,	0.0,	0.0},
		{"右人指2",	0.0,	0.0,	0.0},
		{"右人指3",	0.0,	0.0,	0.0},
		{"右中指1",	0.0,	0.0,	0.0},
		{"右中指2",	0.0,	0.0,	0.0},
		{"右中指3",	0.0,	0.0,	0.0},
		{"右薬指1",	0.0,	0.0,	0.0},
		{"右薬指2",	0.0,	0.0,	0.0},
		{"右薬指3",	0.0,	0.0,	0.0},
		{"右小指1",	0.0,	0.0,	0.0},
		{"右小指2",	0.0,	0.0,	0.0},
		{"右小指3",	0.0,	0.0,	0.0},
	}
	insert_form(motion, frame, kiwotsuke)

	local kubi_home = {"首",	nil,	0.0,				nil}
	local kubi_left = {"首",	nil,	preferences.left_right,		nil}
	local kubi_right = {"首",	nil,	-preferences.left_right,	nil}

	local utaidashi = {
		{"左手捩",	60.0,	90.0, 	60.0},
		{"左手首",	0.0,	0.0, 	0.0},
		{"右手捩",	60.0,	-90.0, 	-60.0},
		{"右手首",	0.0,	0.0, 	0.0},

		{"左親指0",	-12.2,	9.5,	-1.0},
		{"左人指1",	0.0,	-10.0,	95.0},
		{"左人指2",	0.0,	0.0,	60.0},
		{"左人指3",	0.0,	1.2,	49.2},
		{"左中指1",	0.2,	1.8,	96.7},
		{"左中指2",	0.0,	0.0,	60.0},
		{"左中指3",	0.1,	0.4,	28.5},
		{"左薬指1",	-1.0,	12.1,	102.7},
		{"左薬指2",	0.0,	0.0,	60.0},
		{"左薬指3",	0.0,	0.1,	24.5},
		{"左小指1",	10.1,	17.9,	85.1},
		{"左小指2",	0.0,	0.0,	80.0},
		{"左小指3",	0.0,	0.3,	18.1},

		{"右親指0",	-12.2,	-9.5,	1.0},
		{"右人指1",	0.0,	10.0,	-95.0},
		{"右人指2",	0.0,	0.0,	-60.0},
		{"右人指3",	0.0,	-1.2,	-49.2},
		{"右中指1",	0.2,	-1.8,	-96.7},
		{"右中指2",	0.0,	0.0,	-60.0},
		{"右中指3",	0.1,	-0.4,	-28.5},
		{"右薬指1",	-1.0,	-12.1,	-102.7},
		{"右薬指2",	0.0,	0.0,	-60.0},
		{"右薬指3",	0.0,	-0.1,	-24.5},
		{"右小指1",	10.1,	-17.9,	-85.1},
		{"右小指2",	0.0,	0.0,	-80.0},
		{"右小指3",	0.0,	-0.3,	-18.1},
	}

	if 0 < #phrase then
		-- 歌い出しの1小節前を探す
		local before_second = seek_measure(phrase[1].start, -1)

		-- 気を付け終了
		frame = math.floor(before_second * 30)
		insert_form(motion, frame, kiwotsuke)
	
		-- 歌い出し(フレーズ頭とぶつかるので1フレーム前)
		frame = math.floor(phrase[1].start * 30) - 1
		insert_form(motion, frame, utaidashi)
		table.insert(kubi, {
			frame = frame,
			x = kubi_home[2],
			y = kubi_home[3],
			z = kubi_home[4],
		})
	end

	local arm_home = {
		{"左腕",	0.0,	0.0,	42.0},
		{"左ひじ",	40.0,	-100.0,	-40.0},
		{"右腕",	0.0,	0.0,	-45.0},
		{"右ひじ",	40.0,	100.0, 	40.0},
	}

	local is_left = true
	local i
	for i = 1, #phrase do
		local is_kiwotsuke = false
		-- 前のフレーズから3小節以上空いたら気を付けで休む
		if 1 < i and phrase[i - 1].stop <= seek_measure(phrase[i].start, -3) then
			-- 前のフレーズ直後に歌い出し
			frame = math.floor(phrase[i - 1].stop * 30) + 1
			insert_form(motion, frame, utaidashi)

			-- 前のフレーズから1小節後~今のフレーズの1小節前で気を付け
			local second = seek_measure(phrase[i - 1].stop, 1)
			frame = math.floor(second * 30)
			insert_form(motion, frame, kiwotsuke)
			second = seek_measure(phrase[i].start, -1)
			frame = math.floor(second * 30)
			insert_form(motion, frame, kiwotsuke)

			-- 今のフレーズの直前
			frame = math.floor(phrase[i].start * 30) - 1
			insert_form(motion, frame, utaidashi)

			is_kiwotsuke = true
		end

		local p = phrase[i]

		-- フレーズの開始
		local second = p.start
		frame = math.floor(second * 30)
		local angle = preferences.arm_ratio
		if 1 < i and (p.start - phrase[i - 1].stop) < preferences.arm_up_time then
			-- フレーズ間が短時間の場合は腕をちょっと上げる
			angle = angle * (p.start - phrase[i - 1].stop) / preferences.arm_up_time
			local arm_up = {
				{"左腕",	0.0 + 30.0 * angle,	0.0,			42.0 - 2.0 * angle},
				{"左ひじ",	40.0 - 10.0 * angle,	-100.0 - 40.0 * angle,	-40.0 - 30.0 * angle},
				{"右腕",	0.0 + 30.0 * angle,	0.0, 			-45.0 + 5.0 * angle},
				{"右ひじ",	40.0 - 10.0 * angle,	100.0 + 40.0 * angle,	40.0 + 30.0 * angle},
			}
			insert_form(motion, frame, arm_up)
		elseif not is_kiwotsuke and 1 < i and preferences.arm_up_time < (p.start - phrase[i - 1].stop) then
			-- 気を付けの場合を除きフレーズ間が長時間の場合は先に腕を上げて待つ
			local arm_up = {
				{"左腕",	0.0 + 30.0 * angle,	0.0,			42.0 - 2.0 * angle},
				{"左ひじ",	40.0 - 10.0 * angle,	-100.0 - 40.0 * angle,	-40.0 - 30.0 * angle},
				{"右腕",	0.0 + 30.0 * angle,	0.0, 			-45.0 + 5.0 * angle},
				{"右ひじ",	40.0 - 10.0 * angle,	100.0 + 40.0 * angle,	40.0 + 30.0 * angle},
			}
			insert_form(motion, math.floor((phrase[i - 1].stop + preferences.arm_up_time) * 30), arm_up)
			insert_form(motion, frame, arm_up)
		else
			-- 普通(?)
			local arm_up = {
				{"左腕",	0.0 + 30.0 * angle,	0.0,			42.0 - 2.0 * angle},
				{"左ひじ",	40.0 - 10.0 * angle,	-100.0 - 40.0 * angle,	-40.0 - 30.0 * angle},
				{"右腕",	0.0 + 30.0 * angle,	0.0, 			-45.0 + 5.0 * angle},
				{"右ひじ",	40.0 - 10.0 * angle,	100.0 + 40.0 * angle,	40.0 + 30.0 * angle},
			}
			insert_form(motion, frame, arm_up)
		end

		-- 拍に合わせて首を横に振る
		local blick = timeaxis:getBlickFromSeconds(second)
		local tempo = timeaxis:getTempoMarkAt(blick)
		local beat_second = 60 / tempo.bpm

		while second < p.last do
			-- 拍の頭
			frame = math.floor(second * 30)
			table.insert(kubi, {
				frame = frame,
				x = kubi_home[2],
				y = kubi_home[3],
				z = kubi_home[4],
			})

			-- 次の拍までの中間点で戻す
			frame = math.floor((second + beat_second * 0.5) * 30)
			if is_left then
				table.insert(kubi, {
					frame = frame,
					x = kubi_left[2],
					y = kubi_left[3],
					z = kubi_left[4],
				})
				is_left = false
			else
				table.insert(kubi, {
					frame = frame,
					x = kubi_right[2],
					y = kubi_right[3],
					z = kubi_right[4],
				})
				is_left = true
			end
	
			second = second + beat_second
		end

		-- 最後の発音
		frame = math.floor(p.last * 30)
		insert_form(motion, frame, arm_home)
		table.insert(kubi, {
			frame = frame,
			x = kubi_home[2],
			y = kubi_home[3],
			z = kubi_home[4],
		})

		-- フレーズの終了
		frame = math.floor(p.stop * 30)
		insert_form(motion, frame, arm_home)
	end

	if 0 < #phrase then
		-- 歌い終わり
		frame = math.floor(phrase[#phrase].stop * 30)
		insert_form(motion, frame, utaidashi)

		-- 歌い終わりの1小節後を探す
		local after_second = seek_measure(phrase[#phrase].stop, 1)

		-- 気を付け開始
		frame = math.floor(after_second * 30)
		insert_form(motion, frame, kiwotsuke)
	end

	return motion, kubi
end

-- 小節単位の相対位置
function seek_measure(second, offset)
	local timeaxis = SV:getProject():getTimeAxis()
	local blick = timeaxis:getBlickFromSeconds(second)
	local measure = timeaxis:getMeasureAt(blick)
	local measure_mark = timeaxis:getMeasureMarkAt(measure)
	local tempo = timeaxis:getTempoMarkAt(blick)

	return second + 60 / tempo.bpm * measure_mark.numerator * offset
end

-- 間の小節数
function dist_measure(start, stop)
	local start_blick = timeaxis:getBlickFromSeconds(start)
	local start_measure = timeaxis:getMeasureAt(start_blick)
	local stop_blick = timeaxis:getBlickFromSeconds(stop)
	local stop_measure = timeaxis:getMeasureAt(stop_blick)

	return stop_measure - start_measure
end

-- ノートからモーション
function note2motion(note)
	local timeaxis = SV:getProject():getTimeAxis()
	local kubi = {}

	local kubi_home = {"首",	0.0,			nil, 	nil}
	local kubi_down = {"首",	-preferences.up_down,	nil, 	nil}

	-- 気を付け開始
	local frame = math.floor(0 * 30)
	table.insert(kubi, {
		frame = frame,
		x = kubi_home[2],
		y = kubi_home[3],
		z = kubi_home[4],
	})

	local last_frame
	local i
	for i = 1, #note do
		local n = note[i]

		-- ノートの開始
		frame = math.floor(n.start * 30)
		if nil == last_frame or last_frame < frame then
			table.insert(kubi, {
				frame = frame,
				x = kubi_home[2],
				y = kubi_home[3],
				z = kubi_home[4],
			})
		end
 
		-- ノートに合わせて首を縦に振る
		frame = math.floor((n.start + n.stop) / 2 * 30)
		table.insert(kubi, {
			frame = frame,
			x = kubi_down[2],
			y = kubi_down[3],
			z = kubi_down[4],
		})

		-- ノートの終了
		frame = math.floor(n.stop * 30)
		table.insert(kubi, {
			frame = frame,
			x = kubi_home[2],
			y = kubi_home[3],
			z = kubi_home[4],
		})
		last_frame = frame
	end

	return kubi
end

function insert_form(motion, frame, form)
	local i
	for i = 1, #form do
		insert_motion(motion, frame, form[i][1], form[i][2], form[i][3], form[i][4])
	end
end

function insert_motion(motion, key_frame, bone_name, rotate_x, rotate_y, rotate_z)
	table.insert(motion, {
		frame = key_frame,
		bone = bone_name,
		x = rotate_x,
		y = rotate_y,
		z = rotate_z,
	})
end

function write_vmd(vmdfile, motion)
	local rc = 0

	local fh = io.open(vmdfile, "wb")
	if fh then
		-- ヘッダ
		write_string(fh, "Vocaloid Motion Data 0002", 30)
		-- バージョン「レコーティング」
		write_string(fh, utf8_2_sjis('レコーディング'), 20)

		-- ここからモーション
		-- ボーン個数
		local motion_count = 0
		local i
		for i = 1, #motion do
			if nil ~= motion[i].frame then
				motion_count = motion_count + 1
			end
		end
		write_long(fh, motion_count)

		-- ここからキー毎
		for i = 1, #motion do
			if nil ~= motion[i].frame then
				local bone = utf8_2_sjis(motion[i].bone)
				-- ボーン名
				if nil == bone then
					-- 想定外のボーンは名無しで
					write_string(fh, "", 15)
				else
					write_string(fh, bone, 15)
				end

				-- フレーム番号
				write_long(fh, motion[i].frame)

				-- 位置
				write_float(fh, 0.0)
				write_float(fh, 0.0)
				write_float(fh, 0.0)

				-- 回転
				local qx, qy, qz, qw = mmd2quaternion(motion[i].x, motion[i].y, motion[i].z)
				write_float(fh, qx)
				write_float(fh, qy)
				write_float(fh, qz)
				write_float(fh, qw)

				-- 補完
				local j
				for j = 0, 63 do
					write_byte(fh, 0)
				end
			end
		end
		-- ここまで

		-- スキン個数
		write_long(fh, 0)

		-- カメラ個数
		write_long(fh, 0)

		-- 照明個数
		write_long(fh, 0)

		-- セルフ影個数
		write_long(fh, 0)

		-- モデル表示個数
		write_long(fh, 0)

		fh:close()

		rc = 1
	end

	return rc
end

function utf8_2_sjis(text)
	local text_sjis = ""
	local len_utf8 = utf8.len(text)
	local i
	for i = 1, len_utf8 do
		local char_utf8 = utf8_sub(text, i, i)
		local char_sjis = sjis_char[char_utf8]
		if nil == char_sjis then
			SV:showMessageBox(plugin_name, "ボーン名に変換できない文字がありました:'" .. char_utf8 .. "'")
		else
			text_sjis = text_sjis .. char_sjis
		end
	end

	return text_sjis
end

-- string.subのutf8版
function utf8_sub(str, head, tail)
	local head_offset = utf8.offset(str, head)
	local tail_offset = utf8.offset(str, tail + 1) - 1
	return string.sub(str, head_offset, tail_offset)
end

function write_long(fh, v)
	fh:write(string.pack(fmt_long, v))
end

function write_float(fh, v)
	fh:write(string.pack(fmt_float, v))
end

function write_byte(fh, v)
	fh:write(string.pack(fmt_byte, v))
end

function write_string(fh, v, s)
	fh:write(padding_0x00(v, s))
end

function padding_0x00(str, byte)
	local pad = ""
	local i
	for i = 1, byte do
		pad = pad .. "\0"
	end

	return string.sub(str .. pad, 1, byte)
end

function mmd2quaternion(mx, my, mz)
	local ex = math.rad(mx)
	local ey = math.rad(-my)
	local ez = math.rad(-mz)
	local hx = ex / 2
	local hy = ey / 2
	local hz = ez / 2
	local qx = math.cos(hx) * math.sin(hy) * math.sin(hz) + math.sin(hx) * math.cos(hy) * math.cos(hz)
	local qy = -math.sin(hx) * math.cos(hy) * math.sin(hz) + math.cos(hx) * math.sin(hy) * math.cos(hz)
	local qz = math.cos(hx) * math.cos(hy) * math.sin(hz) - math.sin(hx) * math.sin(hy) * math.cos(hz)
	local qw = math.sin(hx) * math.sin(hy) * math.sin(hz) + math.cos(hx) * math.cos(hy) * math.cos(hz)
	return qx, qy, qz, qw
end