LoginSignup
106
44

More than 3 years have passed since last update.

俺は静的"コード"解析がしたい。

Last updated at Posted at 2019-07-31

この記事は リンク情報システム の「2019 Tech Connect Summer」のリレー記事です。
engineer.hanzomon のグループメンバーによってリレーされます。

2019 Tech Connect Summer インデックスはこちら

1日目らしいです。頑張ります。
こんなニッチな記事が初っ端で大丈夫か。

俺の静的コード解析

静的コード解析とは

静的コード解析 (static code analysis) または静的プログラム解析 (static program analysis)とは、
コンピュータのソフトウェアの解析手法の一種であり、実行ファイルを実行することなく解析を行うこと。

Wiki:静的コード解析

ちょっと何言ってるかわかんない。(わかるけど)

コードと言ったら和音だろ!!!

(ここでいうコードはcodeではなくchord)
(エンジニアらしからぬ発言)

ということで「ギターフレット上で押さえたところからギターコードを解析する」プログラムを作成してみたいと思います。

要件定義

  • ギター上で押さえているフレットを指定できるGUI(音も鳴る)
  • 上記で指定した情報からコードを解析
  • そして俺にっこり

こんな感じですかね。

GUI(音も鳴る)

自分で「音も鳴る」とか書いておいて、全然どうすればいいかわかんないという悪夢
そんなリアルな音は求めてないので今回はMIDI音源を鳴らします。

MIDI音源を鳴らすテスト

GUI作成

  1. 以下のようなGUIを作成(まさにC#)
    MIDITEST01.PNG

  2. 以下のようなクラスを作成

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);
        }
    }
}
  1. 以下のように呼び出す
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);
}
  1. ボタンを押下する。

C#(ドの#)のメジャーコードが鳴りましたね。
(これがやりたかっただけ)

上記のソースを流用して以下のようなGUIを作成しました。
ラジオボタンをクリックするとMIDI音源が流れます。
以下を使ってギターで押さえた個所をコード分析します。
GUI01.PNG

指定した情報からコードを解析

ギターコードなんて死ぬほどたくさんあるので
「ここがルート音で、次の音がこれだけ離れてて…」なんて解析するのは至極面倒

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の作成

  1. music21をインストール

    > py -m pip install music21
    
  2. 以下のようなスクリプトを作成

    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メジャーと言い当てました。

そして俺にっこり

ここまでで作ったものをいい感じに結合する。

    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メジャーを入力して解析ボタンを押すと…
Result.PNG

ちゃんと出ましたね!
なんか複雑なコードも入れてみましょう。

例えばギターの神様ことジミ・ヘンドリックスが多用したコードで知られる、
E7(#9) 通称 ジミヘンコードを入力してみます。
Result02.PNG

うーん、よくわからん。
music21というライブラリは音楽全般の解析用ライブラリみたいなので
ギターコードのように簡易な表記にはしてくれないようです…

結果

コード解析できたが解析結果が読み取れない

Python ScriptのExe化とかもやってみたいと思っていたが間に合わなかったので、
機会があったらまた同じようなことに挑戦してみたいと思います。

106
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
106
44