以前、『PowerShellで演奏する電子オルゴール』や『Python3で演奏する電子オルゴール』の記事を書きましたが、同様の処理をGolangに移植してみました。
GitHubにて、ソースコードおよびサンプルの楽譜データも公開しています。
MIDI関連のWin32APIを利用し、Golangで楽譜データを演奏させてみます。
音階とノートナンバーの変換表や、楽譜データのフォーマットは、PowerShell版やPython3版と同じです。
#音階データとノート番号の変換表
楽譜データを入力する際に、ドレミ...もしくはdo,re,mi,...というように 音階で入力できるように、音階データとノート番号の変換表を作成しておきます。
// 全角カタカナ表記
ド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)
『こぎつねこんこん』だとこんな感じになります。 ちなみに、同じ長さの音符であれば、和音も鳴らすことができます。
ド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を呼び出す処理は、別のパッケージとしてまとめておきます。
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)))
}
#メインプログラム
メインプログラム側の処理です。
起動時の引数をチェックし、ノート番号の定義ファイルおよび楽譜ファイルを読み込んだあと、
指定された長さで音を鳴らしていきます。
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>