LoginSignup
8
6

More than 5 years have passed since last update.

PowerShellで演奏する電子オルゴール

Last updated at Posted at 2018-06-17

はじめに

PowerShellで演奏する電子オルゴールを制作してみました。

☆関連記事
python3で演奏する電子オルゴール
Golangで演奏する電子オルゴール

GitHubにて、ソースコードおよびサンプルの楽譜データも公開しています。
https://github.com/gx3n-inue/PS_PlayBox

MIDI関連のWin32APIを利用するクラスをC#で作成し、PowerShellからinlineで呼び出すことにより、楽譜データを演奏させてみます。

プログラムの入力や実行には、コンパイラなどは必要ありません。
テキストエディタで入力して、PowerShell上で実行するだけです。

MIDI関連のWin32APIを呼び出すC#クラス

MIDI関連のWin32APIを呼び出すC#のクラスはこんな感じです。

myMIDI.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

using HANDLE = System.IntPtr;
using MMRESULT = System.UInt32;
using UINT = System.UInt32;
using DWORD_PTR = System.IntPtr;
using DWORD = System.UInt64;
using LPHMIDIOUT = System.IntPtr;
using HMIDIOUT = System.IntPtr;

public class myMIDI
{
    //const UInt32 MIDI_MAPPER = -1;
    const UInt32 MIDI_MAPPER = UInt32.MaxValue;
    HMIDIOUT h;

    [DllImport("Winmm.dll", EntryPoint = "midiOutOpen")]
    private static extern MMRESULT midiOutOpen(
        ref LPHMIDIOUT lphmo,
        UINT uDeviceID,
        DWORD_PTR dwCallback,
        DWORD_PTR dwCallbackInstance,
        DWORD dwFlags
    );

    [DllImport("Winmm.dll", EntryPoint = "midiOutShortMsg")]
    private static extern MMRESULT midiOutShortMsg(
        HMIDIOUT hmo,
        DWORD dwMsg
    );

    [DllImport("Winmm.dll", EntryPoint = "midiOutReset")]
    private static extern MMRESULT midiOutReset(
        HMIDIOUT hmo
    );

    [DllImport("Winmm.dll", EntryPoint = "midiOutClose")]
    private static extern MMRESULT midiOutClose(
        HMIDIOUT hmo
    );

    public void Init(UInt32 timbre)
    {
        midiOutOpen(ref h, MIDI_MAPPER, (System.IntPtr)0, (System.IntPtr)0, 0);
        midiOutShortMsg(h, timbre);     // 音色を定義
    }

    public void OutOnly(UInt32 outData)
    {
        midiOutShortMsg(h, outData);    // 鍵盤を押す
    }

    public void Out(UInt32 outData, Int32 len)
    {
        midiOutShortMsg(h, outData);         // 鍵盤を押す
        System.Threading.Thread.Sleep(len);  // 一定時間音を鳴らし続ける
    }

    public void Close()
    {
        midiOutReset(h);
        midiOutClose(h);
    }

    public HMIDIOUT getHvalue()
    {
        return (h);
    }

    public void Sleep(Int32 len)
    {
        System.Threading.Thread.Sleep(len);
    }
}

メインプログラム

次に、PowerShell側のメインプログラムです。

PS_PlayBox.ps1
##--------------------------------------------------------##
## 引数の受け取り
##--------------------------------------------------------##
param($musicFile, $timbre)


##--------------------------------------------------------##
## 音階定義ファイルの読み込み
##--------------------------------------------------------##
function loadDefFile([string]$defFileName)
{
    $f = (Get-Content $defFileName) -as [string[]]
    $lines = @()

    # 音階名とノートナンバーに分け、配列に格納する
    foreach ($currentLine in $f) {

        # コメント開始位置の検出
        $commentStartPostion = $currentLine.IndexOf("//")

        if ($commentStartPostion -eq 0) {
            continue
        }
        elseif ($commentStartPostion -gt 0) {
            $currentLine = $currentLine.Substring(0, $commentStartPostion)
        }

        # スペースの削除
        $currentLine = $currentLine.Replace(" ","")

        # TABの削除
        $currentLine = $currentLine.Replace("`t", "")

        # "="で区切り、音階とノートナンバーに分けて格納する
        if ($currentLine -ne "") {
            $scale, $note = $currentLine -split "="
            $lines += New-Object PSObject -Property @{scale=$scale; note=$note}
        }
    }

    return($lines)
}


##--------------------------------------------------------##
## 音譜ファイルの読み込み
##--------------------------------------------------------##
function loadPlayFile([string]$musicFile)
{
    $f = (Get-Content $musicFile) -as [string[]]
    $lines = @()

    foreach ($currentLine in $f) {

        # コメント開始位置の検出
        $commentStartPostion = $currentLine.IndexOf("//")

        if ($commentStartPostion -eq 0) {
            continue
        }
        elseif ($commentStartPostion -gt 0) {
            $currentLine = $currentLine.Substring(0, $commentStartPostion)
        }

        # スペースの削除
        $currentLine = $currentLine.Replace(" ","")

        # TABの削除
        $currentLine = $currentLine.Replace("`t", "")

        # "="で区切り、音階と長さを配列に格納する
        if ($currentLine -ne "") {
            $scale, $tlen = $currentLine -split "="
            $lines += New-Object PSObject -Property @{scale=$scale; note=""; tlen=$tlen}
        }
    }

    return($lines)
}


##--------------------------------------------------------##
## 音階文字列を検索し、ノートナンバーをセットする
##--------------------------------------------------------##
function replaceScalt_to_Freq([array]$defs, [array]$playData)
{
    for ($i = 0; $i -lt $playData.Length; $i++) {

        $scale = $playData[$i].scale -split ","

        foreach ($temp in $scale) {

            for ($j = 0; $j -lt $defs.Length; $j++) {

                if ($temp -eq $defs[$j].scale) {

                    if ($playData[$i].note -eq "") {
                       $playData[$i].note = $defs[$j].note
                    }
                    else {
                       $playData[$i].note += "," + $defs[$j].note
                    }

                    break
                }
            }
        }
    }

    return($playData)
}


##--------------------------------------------------------##
## メイン処理
##--------------------------------------------------------##
function PlayBox_Main($musicFile, $timbre)
{
    $defs = @()
    $defs = loadDefFile "note-number.dat" 

    $playData = @()
    $playData = loadPlayFile $musicFile

    # 音階をMIDIノートナンバーに変換する
    $playData = replaceScalt_to_Freq $defs $playData


    ##-----------------------------------------##
    ## CSharpライブラリの読み込み(Win32API参照)
    ##-----------------------------------------##
    add-type -path .\myMIDI.cs -passThru
    $pm = New-Object myMIDI

    $initData = [UINT32]$timbre*256 + [Convert]::ToInt32("c0", 16)
    $pm.INIT($initData)    # MIDI初期化

    Write-Host
    Write-Host "Load Done. Play Start!!"

    for ($i = 0; $i -lt $playData.Length; $i++) {

        if ($playData[$i].note -ne "") {
            Write-Host "[$i] = "$playData[$i].scale"("$playData[$i].note"), "$playData[$i].tlen"[ms]"

            $cnote = $playData[$i].note -split ","

            foreach ($data in $cnote) {

                # MIDIに出力する
                $note_on = "7f" + $data + "90"
                $play_on = [Convert]::ToInt32($note_on, 16)

                # 鍵盤を押す
                $pm.OutOnly($play_on)
            }

            # 一定時間鳴らし続ける
            $pm.Sleep($playData[$i].tlen)

            foreach ($data in $cnote) {
                $note_off = "7f" + $data + "80"
                $play_off = [Convert]::ToInt32($note_off, 16)

                # 鍵盤を離す
                $pm.OutOnly($play_off)
            }
        } 
        else {
            Write-Host "[$i] = rest ("$playData[$i].note"), "$playData[$i].tlen"[ms]"

            # 休符
            $pm.Sleep($playData[$i].tlen)
        }
    }

    $pm.Close()
}


##--------------------------------------------------------##
## 前処理
##--------------------------------------------------------##
if (-Not($musicFile)) {
    Write-Host "Usage :"$MyInvocation.MyCommand.Name"musicDataFile <timbre>"
    exit
}

if (-Not($timbre)) {
    $timbre = 1     # ピアノ
}

# メイン処理
PlayBox_Main $musicFile $timbre

音階データとノート番号の変換表

楽譜データを入力する際に、ドレミ...もしくはdo,re,mi,...というように
音階で入力できるように、音階データとノート番号の変換表を作成しておきます。

note-number.dat
// 全角カタカナ表記
ド4=3c
ド#4=3d
レ4=3e
レ#4=3f
ミ4=40
フ4=41
フ#4=42
ソ4=43
ソ#4=44
ラ4=45
ラ#4=46
...
(長いので途中省略)
...
...
re#8=6f
mi8=70
fa8=71
fa#8=72
so8=73
so#8=74
ra8=75
ra#8=76
si8=77

楽譜データの作成

次に、演奏させたい曲の楽譜データを下記の書式で並べていきます。

音階およびオクターブ番号=演奏時間(ms)

『こぎつねこんこん』だとこんな感じになります。
ちなみに、同じ長さの音符であれば、和音も鳴らすことができます。

PB_kitune.txt
ド4,ド5 = 250
レ4 = 250
ミ4 = 250
フ4 = 250
ソ4,ソ5 = 500
ソ4,ソ5 = 500

ラ4 = 250
フ4 = 250
ド5 = 250
ラ4 = 250
ソ4,ソ5 = 1000

ラ4 = 250
フ4 = 250
ド5 = 250
ラ4 = 250
ソ4,ソ5 = 1000

ソ4 = 250
フ4 = 250
フ4 = 250
フ4 = 250

フ4 = 250
ミ4 = 250
ミ4 = 250
ミ4 = 250
ミ4 = 250
レ4 = 250
ミ4 = 250
レ4 = 250
ド4 = 250
ミ4 = 250
ソ4,ソ5 = 500

ソ4 = 250
フ4 = 250
フ4 = 250
フ4 = 250

フ4 = 250
ミ4 = 250
ミ4 = 250
ミ4 = 250

ミ4 = 250
レ4 = 250
レ4 = 250
ミ4 = 250

ド4,ド5 = 1000

演奏

では早速、演奏してみましょう。
PowerShellを起動し、下記の書式でPS_PlayBox.ps1を実行します。

PS D:\PS_PlayBox> .\PS_PlayBox.ps1
Usage : PS_PlayBox.ps1 musicDataFile <timbre>

timbreはMIDIの音色(楽器)の指定です。1~127の数値で指定してください。
以下は、さきほどの『ごぎつねこんこん』(PB_kitune.txt)をハーモニカ(23)で演奏する例です。

PS D:\PS_PlayBox> .\PS_PlayBox.ps1 .\PB_kitune.txt 23

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    myMIDI                                   System.Object

Load Done. Play Start!!
[0] =  ド4,ド5 ( 3c,48 ),  250 [ms]
[1] =  レ4 ( 3e ),  250 [ms]
[2] =  ミ4 ( 40 ),  250 [ms]
[3] =  フ4 ( 41 ),  250 [ms]
[4] =  ソ4,ソ5 ( 43,4f ),  500 [ms]
[5] =  ソ4,ソ5 ( 43,4f ),  500 [ms]
[6] =  ラ4 ( 45 ),  250 [ms]
[7] =  フ4 ( 41 ),  250 [ms]
[8] =  ド5 ( 48 ),  250 [ms]
[9] =  ラ4 ( 45 ),  250 [ms]
[10] =  ソ4,ソ5 ( 43,4f ),  1000 [ms]
[11] =  ラ4 ( 45 ),  250 [ms]
[12] =  フ4 ( 41 ),  250 [ms]
[13] =  ド5 ( 48 ),  250 [ms]
[14] =  ラ4 ( 45 ),  250 [ms]
[15] =  ソ4,ソ5 ( 43,4f ),  1000 [ms]
[16] =  ソ4 ( 43 ),  250 [ms]
[17] =  フ4 ( 41 ),  250 [ms]
[18] =  フ4 ( 41 ),  250 [ms]
[19] =  フ4 ( 41 ),  250 [ms]
[20] =  フ4 ( 41 ),  250 [ms]
[21] =  ミ4 ( 40 ),  250 [ms]
[22] =  ミ4 ( 40 ),  250 [ms]
[23] =  ミ4 ( 40 ),  250 [ms]
[24] =  ミ4 ( 40 ),  250 [ms]
[25] =  レ4 ( 3e ),  250 [ms]
[26] =  ミ4 ( 40 ),  250 [ms]
[27] =  レ4 ( 3e ),  250 [ms]
[28] =  ド4 ( 3c ),  250 [ms]
[29] =  ミ4 ( 40 ),  250 [ms]
[30] =  ソ4,ソ5 ( 43,4f ),  500 [ms]
[31] =  ソ4 ( 43 ),  250 [ms]
[32] =  フ4 ( 41 ),  250 [ms]
[33] =  フ4 ( 41 ),  250 [ms]
[34] =  フ4 ( 41 ),  250 [ms]
[35] =  フ4 ( 41 ),  250 [ms]
[36] =  ミ4 ( 40 ),  250 [ms]
[37] =  ミ4 ( 40 ),  250 [ms]
[38] =  ミ4 ( 40 ),  250 [ms]
[39] =  ミ4 ( 40 ),  250 [ms]
[40] =  レ4 ( 3e ),  250 [ms]
[41] =  レ4 ( 3e ),  250 [ms]
[42] =  ミ4 ( 40 ),  250 [ms]
[43] =  ド4,ド5 ( 3c,48 ),  1000 [ms]


PS D:\PS_PlayBox>
8
6
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
8
6