NEUTRINOの歌詞入力を簡単にしてみた②

本記事では、NEUTRINOの歌詞入力を少し簡単にするツールの処理内容を紹介していきます。

どんなツールっすか? という方は、前回記事をお読みくだされ。
(プライベート開発用のgitアカウント作るのが億劫で更新が遅くなってしまった...)

chord-in-code.hatenablog.com


今回のお品書き

Pythonコード

実際に使用しているコードは以下です。

import os
import glob
from pykakasi import kakasi

def conv4MuseScore(text_file):
    """
    テキストファイルの中身をMuseScoreに貼り付けられる形に変換する。\n
    Args:
        text_file : 読み込む歌詞ファイル(.txt)
    Returns:
        convert_file : 変換後の歌詞ファイル(.txt)
    """
    # 変換クラスの生成
    kaka = kakasi()

    # テキストファイル読み込み
    with open(text_file, "r", encoding="utf-8") as f:
        lyrics = [s.strip() for s in f.readlines()] # 改行ごとにリスト化(1)
        lyrics = [i for i in lyrics if i != ""] # 空欄を削除(2)
    
    # 全単語を統合する文字列
    all_str = ""
    
    # 1行ごとに変換
    for lyric in lyrics:
        lyric = lyric.replace(" ", "").replace(" ", "") # 半角&全角スペースを削除(3)
        sentence = kaka.convert(lyric) # ひらがな変換(4)

        for word in sentence:
            all_str += word["hira"] # 1単語ずつ統合(5)
    
    # MuseScore用の文字列
    ms_str = ""

    # 半角スペースの挿入(6)
    for char in list(all_str):
        if char in ["ゃ", "ゅ", "ょ"]:
            ms_str += char
        else:
            ms_str += " " + char
    
    # 最初の半角スペースを削除(7)
    ms_str = ms_str[1:]

    # ファイル名取得
    file_name = os.path.splitext(os.path.basename(text_file))[0]

    # 変換ファイルに書き込み
    with open("./02_converted_data/" + file_name + "_converted.txt", "w", encoding="utf-8") as f:
        f.write(ms_str)
    
    return "./02_converted_data/" + file_name + "_converted.txt"

def main():
    text_list = glob.glob("./01_text_data/*.txt")

    for tl in text_list:
        conv4MuseScore(tl)
    
    return 0

if __name__ == '__main__':
    print("--main start--")

    main()

    print("--main end--")

本記事では上記コード内にコメントを入れてある(1)~(7)の処理内容を説明します。
(globやらファイル出力やらは本題じゃないので説明しません)

処理内容

ここからは処理内容について説明していきます。
上述のコードはざっと以下の様な処理手順になっています。

  1. テキストファイル読み込み
  2. テキストを行単位のリストに変換
  3. リストを一繋ぎのひらがなに変換
  4. MuseScore用に変換
  5. ファイルへの出力

1&5については、ありふれた処理なので特に触れませんが、
2~4はそれぞれ何をしているのか記載しておきます。

なお、分かりやすくするために、ここからは以下の様な
テキストファイルの変換を例にして説明していきます。

明日の天気は

晴れ 時々 曇り

でしょう

テキストを行単位のリストに変換

テキストファイルを読み込んで、行単位のリストを作成します。

text_file = "test_message.txt" # テキストファイルを指定

with open(text_file, "r", encoding="utf-8") as f:
    lyrics = [s.strip() for s in f.readlines()] # 改行ごとにリスト化
    print(lyrics)

# => ['明日の天気は', '', '晴れ\u3000時々 曇り', '', 'でしょう']

「\u3000」はUnicodeエスケープシーケンスでの全角スペースのことです。

見た目的にとても気になりますが、
一旦無視してリスト内の空の要素を取り除きます。

lyrics = [i for i in lyrics if i != ""] # 空欄を削除
print(lyrics)

# => ['明日の天気は', '晴れ\u3000時々 曇り', 'でしょう']

これで、必要な行をリストとして取得することができました。

リストを一繋ぎのひらがなに変換

ここでは先ほど取得したリストに対してforループを回して
1行毎にひらがなに変換していきます。

ひらがなへの変換はpykakasiというライブラリを使ってみました。
pykakasiの詳細な使い方は以下のサイトを見てくだされ。
office54.net

pykakasiを使うと1つの文を単語に分解してから
ひらがなに変換してくれます。

以下の様なイメージ。

お仕事って大変だよね
⇒['お', '仕事', 'って', '大変', 'だよね']
⇒['お', 'しごと', 'って', 'たいへん', 'だよね']

(あくまでイメージです。本当はひらがな以外も含んだ辞書型データのリストが取得されます。)

ということで、pykakasiを使って1行毎に変換していくわけなのですが、
この処理をする時に余計なスペースが入っていると変換が失敗しやすくなります。

明日
⇒ちゃんと「あした」と変換される

明 日
⇒1単語と認識されず、「めい」「にち」と変換される

なので、pykakasiに渡す前にスペースは全て削除しておきます。

# 1行ごとに変換
for lyric in lyrics:
    lyric = lyric.replace(" ", "").replace(" ", "") # 半角&全角スペースを削除
    print(lyric)

# (以下、出力結果)
# 明日の天気は
# 晴れ時々曇り
# でしょう

無事に不要なスペースを削除できたので、pykakasiを使ってひらがな情報を取得します。

from pykakasi import kakasi
kaka = kakasi()

# (中略)

# 1行ごとに変換
for lyric in lyrics:
    lyric = lyric.replace(" ", "").replace(" ", "") # 半角&全角スペースを削除
    sentence = kaka.convert(lyric) # ひらがな変換

    # 単語ごとにループ処理
    for word in sentence:
        print(word["hira"])

# (以下、出力結果)
# あした
# の
# てんき
# は
# はれ
# ときどき
# くもり
# でしょう

あとは、各単語を足し合わせれば一繋ぎのひらがなは作成できますね。

足し合わせる処理の説明は、for文で一つ一つ足していくだけなので
ここでの説明は割愛します。

MuseScore用に変換

ここまでで「漢字、改行、スペースを含むテキスト」を
「一行のひらがな」に変換するところまではできました。

あとはNEUTRINO用に文字の間に半角スペースを入れていきます。

all_str = "あしたのてんきははれときどきくもりでしょう"

# MuseScore用の文字列
ms_str = ""

# 半角スペースの挿入
for char in list(all_str):
    ms_str += " " + char

print(ms_str)

# =>  あ し た の て ん き は は れ と き ど き く も り で し ょ う

list(文字列)で一文字ずつのリストを作成できることさえ知っていれば
for文で半角スペースを挟むだけなので、特に難しくないと思います。

ただし、このままでは文末の「でしょう」が「で し ょ う」の4音扱いになってしまいます。
「ゃ」、「ゅ」、「ょ」を発音する時は直前の文字と合わせて1音扱いが適切なので
「ゃ」、「ゅ」、「ょ」の前には半角スペースを入れないようにします。

all_str = "あしたのてんきははれときどきくもりでしょう"

# MuseScore用の文字列
ms_str = ""

# 半角スペースの挿入
for char in list(all_str):
    if char in ["ゃ", "ゅ", "ょ"]:
        ms_str += char
    else:
        ms_str += " " + char

print(ms_str)

# =>  あ し た の て ん き は は れ と き ど き く も り で しょ う

これで1音毎に間に半角スペースを入れることができました。
あとは、先頭に余計なスペースが残っているので消しておきます。

ms_str = " あ し た の て ん き は は れ と き ど き く も り で しょ う"

# 最初の半角スペースを削除
ms_str = ms_str[1:]

print(ms_str)

# => あ し た の て ん き は は れ と き ど き く も り で しょ う

コード配布

前回のexeは特に配布していませんが、Pythonコードはgithubで公開しておきます。
GitHub - tory0601/lyrics4MuseScore


なお、使用しているpykakasiがGNU General Public License v3 or later (GPLv3+)に指定されているので
このコードもGNU General Public License v3 or later (GPLv3+)とします。


ではでは、今回はこの辺で。

NEUTRINOの歌詞入力を簡単にしてみた①

タイトルの通り、NEUTRINOの歌詞入力を少しでも簡単にするのが今回の目的です。

NEUTRINOって何?

f:id:tory_0601:20220323215941p:plain

NEUTRINOというソフトウェアはご存知でしょうか?

この記事を読んでいる時点で、使ったことある方が大半だと思いますが、
知らない方のために、本家の説明文を貼り付けておきます。

NEUTRINO – NEURAL SINGING SYNTHESIZER はニューラルネットワークを用いた歌声シンセサイザーです。
本ソフトウェアはフリーウェアです。
楽譜から発声タイミング・音の高さ・声質・声のかすれ具合などをニューラルネットワークで推定します。
上記の推定されたパラメータを元にvocoderで音声を合成します。

引用元
NEUTRINO
https://n3utrino.work/


雑に言えば、入力した音&歌詞の通りに歌ってくれるソフトウェアですね。
これが無料で使えるんだから、そりゃ驚くわと。


そんな素敵なソフトウェアなのですが、個人的にどうにかならんかと悩んでいることがあります。


それは

とにかく歌詞入力が面倒

ということです。


あまり細かい操作はここでは説明しませんが、NEUTRINOに歌詞を読み込ませるためには
MuseScoreというフリーソフト上で
音符一音ごとに一文字ずつ歌詞を入力していく作業が発生してしまいます。

f:id:tory_0601:20220327211210p:plain

この作業、一度だけでも億劫なのですが、後からメロディーラインを変更したい時に
「え...また歌詞を入れ直すの...?」
という絶望が待っていたりします。

一応、MuseScore上でメロディを変更する手もあるのですが
DAW上のメロディと整合取れなくなるのが個人的に好きじゃないんすよね...。


とまぁ、そんな感じでどうにか少しでも楽にならんかなと思っていたのですが
せっかくだしPythonでなんとかしてみようというのが本記事のお話。

作ったみたツール

前置きが長くなったので、作ったツールの紹介はサクッとやります。

今回はフォルダ内のテキストファイルを読み込んで、
ひらがなに変換&一文字ずつ半角スペース入れるツールを作ってみました。


「半角スペース入れる意味がわからん」って人もいるかもなので説明しておくと
MuseScoreは半角スペースが入ったテキストをコピーして
譜面上でCtrl + Vを連打すれば一文字ずつ貼り付けられていく仕様だったりします。

以下の記事でも、紹介されています。
musirak.com


操作方法は、歌詞入りのテキストファイルを以下のフォルダに入れて
exeを実行するだけ。(あら簡単!)
f:id:tory_0601:20220327212645p:plain

左側が変換前、右側が変換後のテキストファイルです。
f:id:tory_0601:20220327211834p:plain


変換後のテキストをコピーすれば、MuseScore上はCtrl + Vを連打するだけで
歌詞が一文字ずつ入力されていきます。(やったね)


このツールを使えば、
スマホとかでメモしていた歌詞をCtrl + Vを連打するだけで、MuseScoreに入力できるようになる

...っていうと嘘になりますが、それに近い状態を作ることはできそうです。


まぁやっぱり、NEUTRINO用に書き言葉を話し言葉に直したりする作業は必要なので
多少は手作業が残ってしまいますが、全部入力するよりは楽だし、
手作業で修正したテキストファイルを保持しておけば、メロディライン変更時は
本当にCtrl + Vの連打だけで歌詞入力が完了するので、だいぶマシになったかなとは思っています。

ツールの配布について

とまぁ今回作ってみたツールなのですが、
現状配布の予定はありません。

理由としては、twitterで呟いてみたものの
以下のような有様で「あ、需要ねえなこれ...」って感じだったからです。

ただ、せっかく作ってみたのに、お蔵入りするのも寂しいので
記事化して成仏させておくか...ということで今に至ります。

(万が一欲しい方いたら、コメントでもリプでもくだされ)


まあ需要がなくても、とりあえず次回の記事で
Pythonコードの公開まではしようと思います。

では、今日はこの辺で。

Midoを使ってMIDIトラック内の音数を取得する

PythonのライブラリMidoを使って
MIDIトラック内の音数を取得するのが今回の目的です。
(音数が取得できると、歌詞を考えるツールとか作るのに役立ちそうかなと)

必要ライブラリのインポート

まずは、PythonMIDIファイルを扱うために、
Midoをインポートします。

import mido

任意のトラック内に含まれる音数を取得

忙しい人向け

「理屈は良いから、音数を取得するコードが欲しい」という人向けに
今回作成した関数を載せます。(Midoはきちんとインポートしてね)

※動作保証とかはしないので、自己責任で使ってください。

def count_note(midi_file, track_num=0):
    """
    指定トラックの音数を取得する。\n
    なお、指定トラックが存在しない場合はNoneを返す。\n
    Args:
        midi_file : MIDIファイル
        track_num : トラック番号(デフォルト値 0)
    Returns:
        count : 音数
    """
    # MIDIの読み込み
    midi = mido.MidiFile(midi_file)

    # カウンターの初期化
    count = 0

    # 引数の確認
    if type(track_num) is int and track_num >= 0 and track_num < len(midi.tracks):

        # 指定トラック内の音を数える
        for msg in midi.tracks[track_num]:
            if msg.type == "note_on" and msg.dict()["velocity"] != 0:
                count += 1
    else:
        return None
        
    return count

以下の様に使うことができます。

# MIDIファイル
midi_file = "MIDI_test.mid"

# トラック番号
track_No = 1

# 音数の取得
notes = count_note(midi_file, track_No)
print(notes)

処理内容を知りたい人向け

処理内容を理解してから使いたい人向けに
count_note関数の処理内容を説明します。

MIDI内での音の定義

MIDIファイルは"Messageの集合体"のようなもので、
楽曲情報はMessageとして管理されています。

f:id:tory_0601:20220224230927p:plain
MIDIの構造イメージ

MIDIファイル内で「音を発する」というのは以下のようなMessageになります。

Message('note_on', channel=0, note=74, velocity=102, time=0)

Messageの意味は以下(らしい)です。

  • 'note_on' => このMessageは「音を発する」指令ですよ、という宣言
  • channel => どのチャンネルで鳴らすかを指定(0~15のチャンネルに音色を登録できるらしい)
  • note => 音の高さ(国際表記の場合、note=60 => C4)
  • velocity => 音の強弱(DAWでもvelocityって言うよね)
  • time => 直前のMessageから何tick後に鳴らし始めるか

正直、自分は理解しきってないのですが(音数を数えるだけなら不要だし)
以下の記事で詳しく解説されています。

hjp.hatenablog.com

まぁ、ここでは「'note_on'というMessageがあったら、音を発しているんだな」ってことが
分かっていれば、大体OKです。

音数を数える

ここで勘の良い人は、「じゃあ、音数を知りたい時は'note_on'のMessageを数えれば良いんだな」と気づくと思います。

実際その通りなのですが、一点だけ注意すべきことがあります。

それは、消音(ミュート)を'note_on'のvelocity=0としている場合があることです。

音を発する
Message('note_on', channel=0, note=74, velocity=102, time=0)

消音(普通は'note_off'で宣言する)
Message('note_off', channel=0, note=74, velocity=102, time=480)

まさかの消音
Message('note_on', channel=0, note=74, velocity=0, time=0)

そのため、音数を数える場合はvelocityが0でない'note_on'を数える必要があります。

では、velocity=0の'note_on'に注意しつつ、トラック番号0の音数を
Pythonで取得してみます。

# カウンターの初期化
count = 0

# トラック番号0内のMessage毎にループ
for msg in midi.tracks[0]:

    # 'note_on'でかつvelocity=0でないMessageを判定
    if msg.type == "note_on" and msg.dict()["velocity"] != 0:

        # 音を発するMessageの場合、カウンターを+1する
        count += 1

上記でしれっと使っているけど、Messageに.typeを付けるとメッセージのタイプ、
.dict()をつけると、Messageを辞書型に変換したものが取得できますよ。

トラックが存在しない場合の対処

上記までで、「音数を取得する」機能は完成なんですが、
関数化するにあたって、「存在しないトラック番号を指定された場合」の対応を入れておきます。

# track_numが、INT型で、かつ0 <= track_num < MIDIファイル内のトラック数となっているか判定
if type(track_num) is int and track_num >= 0 and track_num < len(midi.tracks):
    # 指定トラック番号が問題ない場合の処理
else:
    # 指定トラック番号が不適な場合の処理

ということで、これまでの処理を合わせると以下のようになります。
(忙しい人向けで書いたやつと一緒です)

def count_note(midi_file, track_num=0):
    """
    指定トラックの音数を取得する。\n
    なお、指定トラックが存在しない場合はNoneを返す。\n
    Args:
        midi_file : MIDIファイル
        track_num : トラック番号(デフォルト値 0)
    Returns:
        count : 音数
    """
    # MIDIの読み込み
    midi = mido.MidiFile(midi_file)

    # カウンターの初期化
    count = 0

    # 引数の確認
    if type(track_num) is int and track_num >= 0 and track_num < len(midi.tracks):

        # 指定トラック内の音を数える
        for msg in midi.tracks[track_num]:
            if msg.type == "note_on" and msg.dict()["velocity"] != 0:
                count += 1
    else:
        return None
        
    return count

重ねて書いておきますが、上記関数はご自由にお使い頂いて問題ないですが、
動作保証はしないので、自己責任でお願いします。

今回はこの辺で。

PythonでMIDIファイルのトラック数を取得する

PythonMIDIファイルに含まれているトラック数を取得するのが今回の目的です。
(これも超簡単です)

必要ライブラリのインポート

まずは、PythonMIDIファイルを扱うために、
Midoをインポートします。

import mido

トラック数を取得

Midoを使って、手持ちのMIDIファイルをPython上で読み込みます。
(以下は、test.midというファイルを読み込む例)

# MIDIファイルの読み込み
midi = mido.MidiFile("test.mid")

MIDIデータは「Trackのリスト」みたいなもので、
.tracksを付けることでリスト型のデータを取得できます。

# MIDIに含まれるトラックリストを取得
track_list = midi.tracks

後は、リストの長さ(=トラック数)を取得するだけです。

# MIDIファイル内のトラック数を取得
track_num = len(track_list)

PythonでMIDIファイルの再生時間を取得する

PythonMIDIファイルの再生時間を取得するのが今回の目的です。
(超簡単です)

必要ライブラリのインポート

まずは、PythonMIDIファイルを扱うために、
Midoをインポートします。

import mido


「いや、そもそもMidoってインストールしてないんですが...」
って人は前回記事を読んでくだされ。
chord-in-code.hatenablog.com

再生時間の取得

Midoを使って、手持ちのMIDIファイルをPython上で読み込みます。
(以下は、test.midというファイルを読み込む例)

# MIDIファイルの読み込み
midi = mido.MidiFile("test.mid")

読み込んだMIDIデータの再生時間を取得するには、.lengthを付けるだけ。

# 再生時間(秒単位)
print(midi.length)

PythonでMIDIから色々な情報を取得したい

とりあえずPythonMIDIファイルを扱えるようにするのが今回の目的です。

そもそもMIDIってどんなファイルなの?

MIDIファイルって、そもそも何なの?」
という人は、このブログに辿り着かないと思うので、省略。

どうしても知りたい人は、以下を読めば良いかと。
www.dtmstation.com

ここでは細かい話は抜きにして
MIDIデータとはPCが演奏するための楽譜データである
と思っておけば、問題ないです。

さて、いるのかどうかは分からないけど、
「これからPythonMIDIデータを扱ってみたい」という方がいたら
MIDIファイルは以下のように考えておいてくだされ。

f:id:tory_0601:20220224230927p:plain
MIDIの構造イメージ

ここで出てくる"Track"は「フォルダみたいなもの」で、それ自体は情報を持ってないです。
実際の「テンポや音程の情報」は"Message"に記述されています。

この辺のイメージがあれば、もうビビることなく触れると思うので
実際にPythonを使って読み込んでいきます。

どうやってPythonで読み込む?

PythonMIDIファイルを扱うためには、
Midoという外部ライブラリをインストールする必要があります。

公式ドキュメントはこちら
Mido - MIDI Objects for Python — Mido 1.2.10 documentation


開発環境にもよって変わりますが、
基本的には以下2つのコマンドを実行すれば良いです。
python-rtmidiはportを使うのに必要らしいのだけど、現時点では正直よくわかってない)

> pip install mido
> pip install python-rtmidi

動作確認してみる

とりあえず、何かしらMIDIファイルを準備します。
DAWやMuseScoreで作る、フリーのMIDIファイルをダウンロードする、...などなど)

MIDIファイルが準備できたらMidoを使って読み込んでみます。
(以下は3つの音符しか入ってない簡素なファイル"new_song.mid"を読み込んだ例)

import mido
midi = mido.MidiFile("new_song.mid")
print(midi)
MidiFile(type=1, ticks_per_beat=480, tracks=[
  MidiTrack([
    MetaMessage('set_tempo', tempo=500000, time=0),
    Message('note_on', channel=0, note=64, velocity=100, time=0),
    Message('note_off', channel=0, note=64, velocity=64, time=480),
    Message('note_on', channel=0, note=66, velocity=100, time=0),
    Message('note_off', channel=0, note=66, velocity=64, time=480),
    Message('note_on', channel=0, note=68, velocity=100, time=0),
    Message('note_off', channel=0, note=68, velocity=64, time=480),
    MetaMessage('end_of_track', time=0)])
])

読み込んだMIDIファイルにもよりますが、
こんな感じでMessageが沢山表示されていれば、
正常に動作していると思います。

とりあえず、今回はMidoの動作確認ができたので終了。
次回、簡単に抜き出せそうな情報を抜いてみる予定です。

作曲に使えるツールを作ってみたいよね、という話

コロナの影響もあり、

「stay homeで楽しめる趣味」ということで、

作曲を始めてみたのだけど...

 

環境構築

・機材(PC、オーディオインターフェースMIDIキーボードなど)

・ソフト(DAWVOCALOIDなど)

 

楽曲制作

・コード進行

・メロディ

・歌詞

・アレンジ

・ミックス

 

動画

・イラスト

・動画編集

 

 

え...、作曲って考えることも作業も多くて、難すぎん...?

 

当然、多少なりとも音楽の知識も必要になってくるので

普通の人なら脳がパンクするはず。(するよね?)

 

 

加えて、近頃はボカロPが市民権を得てきていることもあって

楽曲制作を行っている人が沢山いるので

最近始めた無名ボカロPは楽曲を作っても、

ほぼ聞いてもらえないといった悲しい現実が待っていたりする。

 

 

払うもの(機材購入費用、時間、気力、労力)に対して

得るもの(リアクション、収益、自己満)が少ないので

途中で止めてしまう人も、多いよねという話。

 

 

そんな現実が見えてきたので、楽曲制作を少しでも楽にするために

なんかしらツールを作れないかな...というのが、このブログの目的。

 

多分、Pythonで頑張る。