123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- // 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)
- }
- }
|