// Copyright 2015 Hajime Hoshi // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "bytes" "fmt" "image/color" "log" "math" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/audio" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/text/v2" "github.com/hajimehoshi/ebiten/v2/vector" ) const ( arcadeFontSize = 8 ) var ( arcadeFaceSource *text.GoTextFaceSource ) func init() { s, err := text.NewGoTextFaceSource(bytes.NewReader(fonts.PressStart2P_ttf)) if err != nil { log.Fatal(err) } arcadeFaceSource = s } const ( screenWidth = 320 screenHeight = 240 sampleRate = 48000 baseFreq = 220 ) // pianoAt returns an i-th sample of piano with the given frequency. func pianoAt(i int, freq float32) float32 { // Create piano-like waves with multiple sin waves. amp := []float32{1.0, 0.8, 0.6, 0.4, 0.2} x := []float32{4.0, 2.0, 1.0, 0.5, 0.25} var v float32 for j := 0; j < len(amp); j++ { // Decay a := amp[j] * float32(math.Exp(float64(-5*float32(i)*freq/baseFreq/(x[j]*sampleRate)))) v += a * float32(math.Sin(2.0*math.Pi*float64(i)*float64(freq)*float64(j+1)/sampleRate)) } return v / 5.0 } // toBytes returns the 2ch little endian 16bit byte sequence with the given left/right sequence. func toBytes(l, r []float32) []byte { if len(l) != len(r) { panic("len(l) must equal to len(r)") } b := make([]byte, len(l)*8) for i := range l { lv := math.Float32bits(l[i]) rv := math.Float32bits(r[i]) b[8*i] = byte(lv) b[8*i+1] = byte(lv >> 8) b[8*i+2] = byte(lv >> 16) b[8*i+3] = byte(lv >> 24) b[8*i+4] = byte(rv) b[8*i+5] = byte(rv >> 8) b[8*i+6] = byte(rv >> 16) b[8*i+7] = byte(rv >> 24) } return b } var ( pianoNoteSamples = map[int][]byte{} pianoNoteSamplesInited = false pianoNoteSamplesInitCh = make(chan struct{}) ) func init() { // Initialize piano data. // This takes a little long time (especially on browsers), // so run this asynchronously and notice the progress. go func() { // Create a reference data and use this for other frequency. const refFreq = 110 length := 4 * sampleRate * baseFreq / refFreq refData := make([]float32, length) for i := 0; i < length; i++ { refData[i] = pianoAt(i, refFreq) } for i := range keys { freq := baseFreq * math.Exp2(float64(i-1)/12.0) // Calculate the wave data for the freq. length := 4 * sampleRate * baseFreq / int(freq) l := make([]float32, length) r := make([]float32, length) for i := 0; i < length; i++ { idx := int(float64(i) * freq / refFreq) if len(refData) <= idx { break } l[i] = refData[idx] } copy(r, l) n := toBytes(l, r) pianoNoteSamples[int(freq)] = n } close(pianoNoteSamplesInitCh) }() } var ( pianoImage = ebiten.NewImage(screenWidth, screenHeight) ) func init() { const ( keyWidth = 24 y = 48 ) whiteKeys := []string{"A", "S", "D", "F", "G", "H", "J", "K", "L"} for i, k := range whiteKeys { x := i*keyWidth + 36 height := 112 vector.DrawFilledRect(pianoImage, float32(x), float32(y), float32(keyWidth-1), float32(height), color.White, false) op := &text.DrawOptions{} op.GeoM.Translate(float64(x+keyWidth/2), float64(y+height-12)) op.ColorScale.ScaleWithColor(color.Black) op.PrimaryAlign = text.AlignCenter text.Draw(pianoImage, k, &text.GoTextFace{ Source: arcadeFaceSource, Size: arcadeFontSize, }, op) } blackKeys := []string{"Q", "W", "", "R", "T", "", "U", "I", "O"} for i, k := range blackKeys { if k == "" { continue } x := i*keyWidth + 24 height := 64 vector.DrawFilledRect(pianoImage, float32(x), float32(y), float32(keyWidth-1), float32(height), color.Black, false) op := &text.DrawOptions{} op.GeoM.Translate(float64(x+keyWidth/2), float64(y+height-12)) op.ColorScale.ScaleWithColor(color.White) op.PrimaryAlign = text.AlignCenter text.Draw(pianoImage, k, &text.GoTextFace{ Source: arcadeFaceSource, Size: arcadeFontSize, }, op) } } var ( keys = []ebiten.Key{ ebiten.KeyQ, ebiten.KeyA, ebiten.KeyW, ebiten.KeyS, ebiten.KeyD, ebiten.KeyR, ebiten.KeyF, ebiten.KeyT, ebiten.KeyG, ebiten.KeyH, ebiten.KeyU, ebiten.KeyJ, ebiten.KeyI, ebiten.KeyK, ebiten.KeyO, ebiten.KeyL, } ) type Game struct { audioContext *audio.Context } func NewGame() *Game { return &Game{ audioContext: audio.NewContext(sampleRate), } } func (g *Game) Update() error { // The piano data is still being initialized. // Get the progress if available. if !pianoNoteSamplesInited { select { case <-pianoNoteSamplesInitCh: pianoNoteSamplesInited = true default: } } if pianoNoteSamplesInited { for i, key := range keys { if !inpututil.IsKeyJustPressed(key) { continue } g.playNote(baseFreq * float32(math.Exp2(float64(i-1)/12.0))) } } return nil } // playNote plays piano sound with the given frequency. func (g *Game) playNote(freq float32) { f := int(freq) p := g.audioContext.NewPlayerF32FromBytes(pianoNoteSamples[f]) p.Play() } func (g *Game) Draw(screen *ebiten.Image) { screen.Fill(color.RGBA{0x80, 0x80, 0xc0, 0xff}) screen.DrawImage(pianoImage, nil) ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f", ebiten.ActualTPS())) } func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { return screenWidth, screenHeight } func main() { ebiten.SetWindowSize(screenWidth*2, screenHeight*2) ebiten.SetWindowTitle("Piano (Ebitengine Demo)") if err := ebiten.RunGame(NewGame()); err != nil { log.Fatal(err) } }