この記事は リンク情報システム の「2019 Tech Connect Summer」のリレー記事です。
engineer.hanzomon のグループメンバーによってリレーされます。
2019 Tech Connect Summer インデックスはこちら
1日目らしいです。頑張ります。
こんなニッチな記事が初っ端で大丈夫か。
俺の静的コード解析
静的コード解析とは
静的コード解析 (static code analysis) または静的プログラム解析 (static program analysis)とは、
コンピュータのソフトウェアの解析手法の一種であり、実行ファイルを実行することなく解析を行うこと。
ちょっと何言ってるかわかんない。(わかるけど)
###コードと言ったら和音だろ!!!
(ここでいうコードはcodeではなくchord)
(エンジニアらしからぬ発言)
ということで**「ギターフレット上で押さえたところからギターコードを解析する」**プログラムを作成してみたいと思います。
要件定義
- ギター上で押さえているフレットを指定できるGUI(音も鳴る)
- 上記で指定した情報からコードを解析
- そして俺にっこり
こんな感じですかね。
GUI(音も鳴る)
自分で「音も鳴る」とか書いておいて、全然どうすればいいかわかんないという悪夢
そんなリアルな音は求めてないので今回はMIDI音源を鳴らします。
MIDI音源を鳴らすテスト
####GUI作成
class MIDISound : IDisposable
{
//-------------------------------------------------
// Windows Multimedia API
[DllImport("Winmm.dll")]
extern static uint midiOutOpen(ref IntPtr lphmo, uint uDeviceID, uint dwCallback, uint dwCallbackInstance, uint dwFlags);
[DllImport("Winmm.dll")]
extern static uint midiOutClose(IntPtr hmo);
[DllImport("Winmm.dll")]
extern static uint midiOutShortMsg(IntPtr hmo, uint dwMsg);
private const uint MMSYSERR_NOERROR = 0;
private const uint MIDI_MAPPER = 0xffffffff;
//--------------------------------------------------
private IntPtr hMidi;
public MIDISound()
{
MidiApiOpen();
}
public void Dispose() => MidiApiClose();
private bool MidiApiOpen()
{
bool bRet = false;
//MidiデバイスのOpen
if (midiOutOpen(ref this.hMidi, MIDI_MAPPER, 0, 0, 0) == MMSYSERR_NOERROR)
{
bRet = true;
midiOutShortMsg(this.hMidi, 0x19c0); // 音色を定義 アコースティックギター(スチール弦)0x19(25)
}
return bRet;
}
private bool MidiApiClose()
{
bool bRet = false;
//MidiデバイスのClose
if (midiOutClose(this.hMidi) == MMSYSERR_NOERROR)
{
bRet = true;
}
return bRet;
}
public void NoteOn(List<byte> keys)
{
byte ch = 0;
byte velocity = 0x7f;
foreach (byte key in keys)
{
uint msg;
msg = (uint)((velocity << 16) + (key << 8) + 0x90 + ch);
midiOutShortMsg(this.hMidi, msg);
}
}
public void NoteOff(List<byte> keys)
{
byte ch = 0;
byte velocity = 0x7f;
foreach (byte key in keys)
{
uint msg;
msg = (uint)((velocity << 16) + (key << 8) + 0x80 + ch);
midiOutShortMsg(this.hMidi, msg);
}
}
}
- 以下のように呼び出す
MIDISound midi = new MIDISound();
//C#
List<byte> Chord = new List<byte>() { 0x31,0x36,0x3A };
private void button1_MouseDown(object sender, MouseEventArgs e)
{
midi.NoteOn(Chord);
}
private void button1_MouseUp(object sender, MouseEventArgs e)
{
midi.NoteOff(Chord);
}
- ボタンを押下する。
C#(ドの#)のメジャーコードが鳴りましたね。
(これがやりたかっただけ)
上記のソースを流用して以下のようなGUIを作成しました。
ラジオボタンをクリックするとMIDI音源が流れます。
以下を使ってギターで押さえた個所をコード分析します。
指定した情報からコードを解析
ギターコードなんて死ぬほどたくさんあるので
「ここがルート音で、次の音がこれだけ離れてて…」なんて解析するのは至極面倒
midiファイルを読み込んだだけでコードの名前当ててくれる何かがあればなぁー
とか思っていたらpythonのmusic21というライブラリでできるようなので使ってみます。
それと同時に前章で作成したGUIからMIDIファイルを作成してみます。
・MIDIファイルの生成
MIDIファイルの構造を読み解くためにこのページを参考にしてプログラムを作成しました。
ってか難しいな…。実装したら予想以上に汚くなりました。
private bool makeMidiFile(String midiFilePath)
{
bool ret = false;
try
{
byte[] MThd = new byte[] { 0x4D, 0x54, 0x68, 0x64 };
byte[] dataLength = new byte[] { 0x00, 0x00, 0x00, 0x06 };
byte[] format = new byte[] { 0x00, 0x01 };
byte[] tracknum = new byte[] { 0x00, 0x02 };
byte[] trackLength = new byte[] { 0x00, 0x30 };
//ヘッダ作成
byte[] Header = JoinByteArray(MThd, dataLength, format, tracknum, trackLength);
byte[] MTrk = new byte[] { 0x4D, 0x54, 0x72, 0x6B };
byte[] TrackEnd = new byte[] { 0x00, 0xFF, 0x2F, 0x00 };
byte[] ClockInfo = new byte[] { 0x00, 0xFF, 0x58, 0x04, 0x04, 0x02, 0x18, 0x08 };
byte[] TempoInfo = new byte[] { 0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20 };
byte[] TrackValue = new byte[] { 0x00, 0xC0, 0x19 };
//playNotes -> GUIで指定した音
foreach (byte note in playNotes)
{
TrackValue = JoinByteArray(TrackValue, new byte[] { 0x00, 0x90, note, 0x5F });
}
bool notFirst = false;
foreach (byte note in playNotes)
{
if (notFirst == false)
{
TrackValue = JoinByteArray(TrackValue, new byte[] { 0x40, 0x90, note, 0x00 });
notFirst = true;
}
else
{
TrackValue = JoinByteArray(TrackValue, new byte[] { 0x00, 0x90, note, 0x00 });
}
}
//バイナリファイルの内容作成
byte[] midifile = JoinByteArray(Header,
MTrk, new byte[] { 0x00, 0x00, 0x00, (byte)(ClockInfo.Length + TempoInfo.Length + TrackEnd.Length) }, ClockInfo, TempoInfo, TrackEnd,
MTrk, new byte[] { 0x00, 0x00, 0x00, (byte)(TrackValue.Length + TrackEnd.Length) }, TrackValue, TrackEnd);
FileStream fs = new FileStream(midiFilePath, FileMode.Create);
BinaryWriter bw = new BinaryWriter(fs);
bw.Write(midifile);
bw.Close();
fs.Close();
ret = true;
}
catch (Exception ex)
{
ret = false;
}
return ret;
}
//複数のbyte配列を結合して一つのbyte配列にまとめる
private byte[] JoinByteArray(params byte[][] arrays)
{
List<byte> ret = new List<byte>();
foreach (byte[] array in arrays)
{
foreach (byte arr_m in array)
{
ret.Add(arr_m);
}
}
return ret.ToArray();
}
####汚ねぇソースだ
一応midiファイルを作成することができました。
・解析を実行するPythonScriptの作成
-
music21をインストール
> py -m pip install music21
-
以下のようなスクリプトを作成
import music21 as m21 import sys import os def midiChordAnalysis(path): try: piece = m21.converter.parse(path) bChords = piece.chordify() for thisChord in bChords.recurse().getElementsByClass('Chord'): print(thisChord.pitchedCommonName) except: print("Error : 読み込みエラー") return () if __name__ == '__main__': args = sys.argv if 2 <= len(args): if(os.path.exists(args[1])): midiChordAnalysis(args[1]) else: print('ERROR : ファイルが存在しません。') else: print('ERROR : 引数がありません。')
3. テスト用にmidiを作成(わかりやすくCメジャー)、読み込ませてみる。
py .\midiChordAnalysis.py .\chord.midi
C-major triad
おお、見事Cメジャーと言い当てました。
そして俺にっこり
------------
ここまでで作ったものをいい感じに結合する。
```C#
private void AnalysisBtn_Click(object sender, EventArgs e)
{
//MIDIファイルの作成
if (!makeMidiFile(midiFile))
{
MessageBox.Show("MIDIファイルが出力できませんでした。");
return;
}
string Result = String.Empty;
try
{
var pythonResult = new Process
{
//pythonExe -> Python.exeのフルパス
//pythonScript -> midiChordAnalysis.pyの相対パス
//midiFile -> 作成したmidiファイルの相対パス
StartInfo = new ProcessStartInfo()
{
FileName = pythonExe,
UseShellExecute = false,
CreateNoWindow = false,
RedirectStandardOutput = true,
Arguments = pythonScript + " " + midiFile
}
};
pythonResult.Start();
StreamReader myStreamReader = pythonResult.StandardOutput;
Result = myStreamReader.ReadLine();
pythonResult.WaitForExit();
pythonResult.Close();
}
catch (Exception ex)
{
Result = null;
}
if (Result != null && !Result.Contains("Error"))
{
label2.Text = Result;
}
else
{
label2.Text = "解析できませんでした。";
MessageBox.Show("コード解析ができませんでした。");
}
}
こんな感じにしてみました。
さて実行結果、試しにDメジャーを入力して解析ボタンを押すと…
ちゃんと出ましたね!
なんか複雑なコードも入れてみましょう。
例えばギターの神様ことジミ・ヘンドリックスが多用したコードで知られる、
E7(#9) 通称 ジミヘンコードを入力してみます。
うーん、よくわからん。
music21というライブラリは音楽全般の解析用ライブラリみたいなので
ギターコードのように簡易な表記にはしてくれないようです…
結果
###コード解析できたが解析結果が読み取れない
Python ScriptのExe化とかもやってみたいと思っていたが間に合わなかったので、
機会があったらまた同じようなことに挑戦してみたいと思います。