LoginSignup
4
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-02-12

以前、『PowerShellで演奏する電子オルゴール』『Python3で演奏する電子オルゴール』の記事を書きましたが、同様の処理をGolangに移植してみました。

GitHubにて、ソースコードおよびサンプルの楽譜データも公開しています。

MIDI関連のWin32APIを利用し、Golangで楽譜データを演奏させてみます。
音階とノートナンバーの変換表や、楽譜データのフォーマットは、PowerShell版やPython3版と同じです。

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

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

note-number.txt
// 全角カタカナ表記
ド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

MIDI関連Win32API呼び出し処理

MIDI関連のWin32APIを呼び出す処理は、別のパッケージとしてまとめておきます。

apimidi/apimidi.go
package apimidi

import (
    "fmt"
    "syscall"
    "time"
    "unsafe"
)

// MyMIDI ... MyMIDI struct
type MyMIDI struct {
    initData   int
    MIDIMAPPER int
    h          *uint
    dll        *syscall.DLL
}

// PrintInfo ... Print struct MyMIDI members information.
func (pm *MyMIDI) PrintInfo() {
    fmt.Printf("addr = %x, initData = %x\n", &pm.initData, pm.initData)
    fmt.Printf("addr = %x, MIDIMAPPER = %x\n", &pm.MIDIMAPPER, pm.MIDIMAPPER)
    fmt.Printf("addr = %x, h = %x\n", &pm.h, pm.h)
}

// Init ... MIDI Init
func (pm *MyMIDI) Init(initData int) {
    dll, err := syscall.LoadDLL("winmm.dll")
    if err != nil {
        panic(err)
    }

    pm.dll = dll
    pm.MIDIMAPPER = -1
    pm.initData = initData

    proc, err := pm.dll.FindProc("midiOutOpen")
    if err != nil {
        panic(err)
    }

    proc.Call(uintptr(unsafe.Pointer(&pm.h)), uintptr(pm.MIDIMAPPER), uintptr(0), uintptr(0), uintptr(0))

    proc, err = pm.dll.FindProc("midiOutShortMsg")
    if err != nil {
        panic(err)
    }

    proc.Call(uintptr(unsafe.Pointer(pm.h)), uintptr(pm.initData))
}

// Out ... MIDI Output with time.Sleep
func (pm *MyMIDI) Out(outData int, length time.Duration) {
    proc, err := pm.dll.FindProc("midiOutShortMsg")
    if err != nil {
        panic(err)
    }

    proc.Call(uintptr(unsafe.Pointer(pm.h)), uintptr(outData))
    time.Sleep(length * time.Millisecond)
}

// OutOnly ... MIDI Output without timeSleep
func (pm *MyMIDI) OutOnly(outData int) {
    proc, err := pm.dll.FindProc("midiOutShortMsg")
    if err != nil {
        panic(err)
    }

    proc.Call(uintptr(unsafe.Pointer(pm.h)), uintptr(outData))
}

// Close ... MIDI Close
func (pm *MyMIDI) Close() {
    proc, err := pm.dll.FindProc("midiOutReset")
    if err != nil {
        panic(err)
    }

    proc.Call(uintptr(unsafe.Pointer(pm.h)))
}

メインプログラム

メインプログラム側の処理です。

起動時の引数をチェックし、ノート番号の定義ファイルおよび楽譜ファイルを読み込んだあと、
指定された長さで音を鳴らしていきます。

Go_PlayBox.go
package main

import "./apimidi"

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
    "time"

    "golang.org/x/text/encoding/japanese"
    "golang.org/x/text/transform"
)

/* Go text processing suport(SJIS)
https://github.com/golang/text

** Download/Install
The easiest way to install is to run go get -u golang.org/x/text.
You can also manually git clone the repository to $GOPATH/src/golang.org/x/text.
*/

// ScaleDefs ... ScaleDefs struct
type ScaleDefs struct {
    scale string
    note  string
}

// PlayData ... struct PlayData
type PlayData struct {
    scale  string
    note   string
    length int
}

// Load the scale definition file.
func loadDefFile(filename string) []ScaleDefs {
    fp, err := os.Open(filename)
    if os.IsNotExist(err) {
        fmt.Printf("%s not found.", filename)
        return nil
    }

    // File Open
    var defs []ScaleDefs
    scanner := bufio.NewScanner(transform.NewReader(fp, japanese.ShiftJIS.NewDecoder()))
    for scanner.Scan() {
        tempStr := scanner.Text()

        pos := strings.Index(tempStr, "//")
        if pos >= 0 {
            tempStr = tempStr[:pos]
        }

        tempStr = strings.Replace(tempStr, " ", "", -1)
        tempStr = strings.Replace(tempStr, "\t", "", -1)
        flds := strings.Split(tempStr, "=")

        if tempStr != "" && len(flds) >= 2 {
            currentDef := new(ScaleDefs)
            currentDef.scale, currentDef.note = flds[0], flds[1]
            defs = append(defs, *currentDef)
        }
    }

    return defs
}

// Load the score file.
func loadPlayFile(filename string) []PlayData {
    fp, err := os.Open(filename)
    if os.IsNotExist(err) {
        fmt.Printf("%s not found.", filename)
        return nil
    }

    // File Open
    var pData []PlayData
    scanner := bufio.NewScanner(transform.NewReader(fp, japanese.ShiftJIS.NewDecoder()))
    for scanner.Scan() {
        tempStr := scanner.Text()

        pos := strings.Index(tempStr, "//")
        if pos >= 0 {
            tempStr = tempStr[:pos]
        }

        tempStr = strings.Replace(tempStr, " ", "", -1)
        tempStr = strings.Replace(tempStr, "\t", "", -1)
        flds := strings.Split(tempStr, "=")

        if tempStr != "" && len(flds) >= 2 {
            currentData := new(PlayData)
            currentData.scale = flds[0]
            currentData.note = ""
            currentData.length, err = strconv.Atoi(flds[1])

            if err != nil {
                fmt.Printf("%s Atoi() error!!", flds[1])
            }

            pData = append(pData, *currentData)
        }
    }

    return pData
}

// Search the musical scale character string and set the note number.
func replaceScale2Freq(defs *[]ScaleDefs, pData *[]PlayData) {
    for i := 0; i < len(*pData); i++ {
        scale := strings.Split((*pData)[i].scale, ",")
        for _, temp := range scale {
            for _, currentLen := range *defs {
                if temp == currentLen.scale {
                    if (*pData)[i].note == "" {
                        (*pData)[i].note = currentLen.note
                    } else {
                        (*pData)[i].note += "," + currentLen.note
                    }
                    break
                }
            }
        }
    }
}

func fileExists(name string) bool {
    _, err := os.Stat(name)
    return !os.IsNotExist(err)
}

func main() {
    if len(os.Args) < 2 {
        fmt.Printf("Usage)\n" +
            "go run " + os.Args[0] + " musicDataFile <timbre>\n")
        return
    }

    noteNumberFile := "./note-number.dat"
    if fileExists(noteNumberFile) == false {
        fmt.Printf("%s not found.", noteNumberFile)
        return
    }

    if fileExists(os.Args[1]) == false {
        fmt.Printf("%s not found.", os.Args[1])
        return
    }

    var timbre int
    if len(os.Args) >= 3 {
        readval, err := strconv.Atoi(os.Args[2])
        if err != nil {
            fmt.Printf("%s ... timbre is a not integer!!", os.Args[2])
            return
        }
        timbre = readval
    } else {
        timbre = 1
    }

    // Load the scale definition file.
    defs := loadDefFile(noteNumberFile)

    // Load PlayData file.
    pData := loadPlayFile(os.Args[1])

    // Set note number.
    replaceScale2Freq(&defs, &pData)

    initData := timbre*256 + 0xc0

    // Set Integer Size.
    const intSize = 32 << (^uint(0) >> 63)

    fmt.Printf("intSize = %d, initData = 0x%04x\n", intSize, initData)

    // Initialize the MyMIDI struct and functions
    pm := new(apimidi.MyMIDI)
    pm.Init(initData)

    fmt.Printf("Load Done. Play start!!\n")

    for i, currentpData := range pData {
        if currentpData.note != "" {
            fmt.Printf("[%d] = %s( %s ), %d [ms]\n", i, currentpData.scale, currentpData.note, currentpData.length)
            cnote := strings.Split(currentpData.note, ",")

            for _, data := range cnote {
                // Press the keyboad.
                playOn := "0x7f" + data + "90"
                playData, err := strconv.ParseInt(playOn, 0, intSize)
                if err != nil {

                }

                pm.OutOnly(int(playData))
            }

            // Keep pressed.
            time.Sleep(time.Duration(currentpData.length) * time.Millisecond)

            for _, data := range cnote {
                // Release the keyboad.
                playOff := "0x7f" + data + "80"
                playData, err := strconv.ParseInt(playOff, 0, intSize)
                if err != nil {

                }

                pm.OutOnly(int(playData))
            }
        } else {
            // Rest.
            fmt.Printf("[%d] = rest, %d [ms]\n", i, currentpData.length)
            time.Sleep(time.Duration(currentpData.length) * time.Millisecond)
        }
    }

    pm.Close()
    fmt.Println()
}

演奏

では早速、演奏してみましょう。 下記の書式でGo_PlayBox.goを実行します。

D:\Go_PlayBox> go run Go_PlayBox.go
Usage)
go run (exeファイルのpath)\Go_PlayBox.exe musicDataFile <timbre>

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

D:\Go_PlayBox> go run Go_PlayBox.go .\PB_kitune.txt 23
intSize = 64, initData = 0x17c0
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]


D:\Go_PlayBox>
4
1
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
4
1