Go
golang
OpenAL
GetWild
GetWildDay 15

Goでドレミファを鳴らしてGetWildを演奏する

More than 1 year has passed since last update.

Get Wild Advent Calendar 2016 の15日目です。

概要

  • 機材はないけど、どうしてもGetWildを弾きたい
  • ドレミファが鳴らせればいける…
  • → GoでOpenALを使って実装しました
  • 時間がなかったのでかなりやっつけです

実装

$ curl -O https://github.com/sioncojp/play-getwild/releases/download/1.0.0/play-getwild.tar.gz
$ tar zxvf play-getwild.tar.gz
$ ./play-getwild

中身

package main

import (
    "encoding/binary"
    "fmt"
    "log"
    "math"
    "sync"
    "time"

    "golang.org/x/mobile/exp/audio/al"
    "golang.org/x/mobile/exp/f32"
)

var pctx *Context
var pianoPlayer *Piano

const (
    Pi         = float32(math.Pi)
    Fmt        = al.FormatStereo16
    QUEUE      = 500
    SampleRate = 10000 // 音の高さのベース
)

type Oscillator func() float32

type Context struct {
    sync.RWMutex
    source     al.Source
    queue      []al.Buffer
    oscillator Oscillator
}

type Piano struct {
    notes      []bool
    oscillator Oscillator
}

func G(gain float32, f Oscillator) Oscillator {
    return func() float32 {
        return gain * f()
    }
}

func GenOscillator(freq float32) Oscillator {
    dt := 1.0 / float32(SampleRate)
    k := 2.0 * Pi * freq
    T := 1.0 / freq
    t := float32(0.0)
    return func() float32 {
        res := f32.Sin(k * t)
        t += dt
        if t > T {
            t -= T
        }
        return res
    }
}

func Multiplex(fs ...Oscillator) Oscillator {
    return func() float32 {
        res := float32(0)
        for _, osc := range fs {
            res += osc()
        }
        return res
    }
}

func GenEnvelope(press *bool, f Oscillator) Oscillator {
    dt := 1.0 / float32(SampleRate)
    top := false
    gain := float32(0.0)
    attackd := dt / 0.01
    dekeyd := dt / 0.03
    sustainlevel := float32(0.3)
    sustaind := dt / 7.0
    released := dt / 0.8
    return func() float32 {
        if *press {
            if !top {
                gain += attackd
                if gain > 1.0 {
                    top = true
                    gain = 1.0
                }
            } else {
                if gain > sustainlevel {
                    gain -= dekeyd
                } else {
                    gain -= sustaind
                }
                if gain < 0.0 {
                    gain = 0.0
                }
            }
        } else {
            top = false
            gain -= released
            if gain < 0.0 {
                gain = 0.0
            }
        }
        return gain * f()
    }
}

func NewContext(oscillator Oscillator) *Context {
    if err := al.OpenDevice(); err != nil {
        log.Fatal(err)
    }
    s := al.GenSources(1)
    return &Context{
        source:     s[0],
        queue:      []al.Buffer{},
        oscillator: oscillator,
    }
}

func NewPiano(freqs []float32) *Piano {
    p := new(Piano)
    p.notes = make([]bool, len(freqs))
    envelopes := []Oscillator{}
    for i, f := range freqs {
        base := []Oscillator{}
        for j := float32(1.0); j <= 8; j++ {
            base = append(base, G(0.5/j, GenOscillator(f*j)))
        }
        base = append(base, G(0.3, GenOscillator(f+2)))
        osc := Multiplex(base...)
        envelopes = append(envelopes, G(0.4, GenEnvelope(&p.notes[i], osc)))
    }
    p.oscillator = Multiplex(envelopes...)
    return p
}
func (p *Piano) NoteOn(key int) {
    p.notes[key] = true
}

func (p *Piano) NoteOff(key int) {
    p.notes[key] = false
}

func (p *Piano) GetOscillator() Oscillator { return p.oscillator }

func (c *Context) Play(q int) {
    c.Lock()
    defer c.Unlock()
    n := c.source.BuffersProcessed()
    if n > 0 {
        rm := c.queue[:n]
        c.queue = nil
        c.source.UnqueueBuffers(rm...)
        al.DeleteBuffers(rm...)
    }
    fmt.Println(len(c.queue))
    for len(c.queue) < QUEUE {
        b := al.GenBuffers(q) // 音の長さ
        buf := make([]byte, 2048)
        for n := 0; n < 2048; n += 2 {
            f := c.oscillator()
            v := int16(float32(92767) * f) // 音の大きさ
            binary.LittleEndian.PutUint16(buf[n:n+2], uint16(v))
        }
        b[0].BufferData(Fmt, buf, SampleRate)
        c.source.QueueBuffers(b...)
        c.queue = append(c.queue, b...)
    }
    al.PlaySources(c.source)
}

func (c *Context) Close() {
    c.Lock()
    defer c.Unlock()
    al.StopSources(c.source)
}

func PlaySound(s, q int, slp time.Duration) {
    pianoPlayer.NoteOn(s)
    pctx.Play(q)
    time.Sleep(slp * time.Millisecond)
    pctx.Close()
    pianoPlayer.NoteOff(s)
}

func main() {
    pianoPlayer = NewPiano([]float32{
        246.941650628,
        261.625565301,
        277.182630977,
        293.664767917,
        311.126983722,
        329.627556913,
        349.228231433,
        369.994422712,
        391.995435982,
        415.30469758,
        440.0,
        466.163761518,
        493.883301256,
        523.251130601,
    })

    pctx = NewContext(pianoPlayer.GetOscillator())

    PlaySound(6, 70, 500)
    PlaySound(4, 70, 500)
    PlaySound(2, 10, 1000)

    time.Sleep(100 * time.Millisecond)

    PlaySound(6, 100, 300)
    PlaySound(4, 80, 500)
    PlaySound(2, 80, 500)
    PlaySound(2, 100, 300)
    PlaySound(2, 10, 1000)

    time.Sleep(200 * time.Millisecond)

    PlaySound(2, 100, 300)
    PlaySound(4, 150, 300)
    PlaySound(6, 150, 300)
    PlaySound(6, 150, 300)
    PlaySound(6, 150, 300)
    PlaySound(7, 150, 300)
    PlaySound(6, 150, 300)
    PlaySound(2, 150, 300)
    PlaySound(2, 150, 300)
    PlaySound(6, 150, 300)

    time.Sleep(10 * time.Millisecond)

    PlaySound(6, 80, 300)
    PlaySound(4, 50, 600)
    PlaySound(2, 200, 180)
    PlaySound(2, 10, 1000)
}

感想

  • 1日でいけるかなと思ったけどかなり難しかったOpenAL(でも楽しかった)
  • 改良点ありすぎなので、時間があればちゃんとしたものを作りたいですね
  • pianoというパッケージにして色んなものを演奏出来るようにしたいなと思いました

参考