ebiten/examples/pcm/main.go

171 lines
3.8 KiB
Go
Raw Normal View History

2015-01-10 17:23:43 +01:00
// 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 (
"log"
"math"
2018-01-29 17:13:39 +01:00
"strings"
2020-10-03 19:35:13 +02:00
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/audio"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
2015-01-10 17:23:43 +01:00
)
const (
screenWidth = 640
screenHeight = 480
sampleRate = 48000
2015-01-10 17:23:43 +01:00
)
const (
2015-01-11 11:52:11 +01:00
freqA = 440.0
freqAS = 466.2
freqB = 493.9
freqC = 523.3
freqCS = 554.4
freqD = 587.3
freqDS = 622.3
freqE = 659.3
freqF = 698.5
freqFS = 740.0
freqG = 784.0
freqGS = 830.6
2015-01-10 17:23:43 +01:00
)
// Twinkle, Twinkle, Little Star
2018-01-29 17:13:39 +01:00
var score = strings.Replace(
`CCGGAAGR FFEEDDCR GGFFEEDR GGFFEEDR CCGGAAGR FFEEDDCR`,
" ", "", -1)
2015-01-11 11:52:11 +01:00
2018-01-29 17:13:39 +01:00
// square fills out with square wave values with the specified volume, frequency and sequence.
func square(out []int16, volume float64, freq float64, sequence float64) {
2015-01-23 02:58:18 +01:00
if freq == 0 {
for i := 0; i < len(out); i++ {
out[i] = 0
}
return
}
2016-02-09 15:04:00 +01:00
length := int(float64(sampleRate) / freq)
2015-01-10 17:23:43 +01:00
if length == 0 {
2015-01-11 11:52:11 +01:00
panic("invalid freq")
2015-01-10 17:23:43 +01:00
}
for i := 0; i < len(out); i++ {
a := int16(volume * math.MaxInt16)
2015-01-10 17:23:43 +01:00
if i%length < int(float64(length)*sequence) {
2015-01-24 13:46:30 +01:00
a = -a
2015-01-10 17:23:43 +01:00
}
out[i] = a
}
}
2018-01-29 17:13:39 +01:00
// toBytes returns the 2ch little endian 16bit byte sequence with the given left/right sequence.
func toBytes(l, r []int16) []byte {
if len(l) != len(r) {
panic("len(l) must equal to len(r)")
}
2015-06-13 20:27:02 +02:00
b := make([]byte, len(l)*4)
2016-05-13 17:25:11 +02:00
for i := range l {
2015-06-13 20:27:02 +02:00
b[4*i] = byte(l[i])
b[4*i+1] = byte(l[i] >> 8)
b[4*i+2] = byte(r[i])
b[4*i+3] = byte(r[i] >> 8)
}
2015-06-13 20:27:02 +02:00
return b
}
type Game struct {
scoreIndex int
frames int
currentNote rune
audioContext *audio.Context
}
func NewGame() *Game {
return &Game{
audioContext: audio.NewContext(sampleRate),
}
}
2018-01-29 17:13:39 +01:00
// playNote plays the note at scoreIndex of the score.
func (g *Game) playNote(scoreIndex int) rune {
2015-01-11 11:52:11 +01:00
note := score[scoreIndex]
2018-01-29 17:13:39 +01:00
// If the note is 'rest', play nothing.
if note == 'R' {
return rune(note)
2015-01-11 11:52:11 +01:00
}
2018-01-29 17:13:39 +01:00
freqs := []float64{freqC, freqD, freqE, freqF, freqG, freqA * 2, freqB * 2}
2015-01-11 11:52:11 +01:00
freq := 0.0
switch {
2018-01-29 17:13:39 +01:00
case 'A' <= note && note <= 'B':
freq = freqs[int(note)+len(freqs)-int('C')]
case 'C' <= note && note <= 'G':
freq = freqs[note-'C']
2015-01-11 11:52:11 +01:00
default:
2018-01-29 17:13:39 +01:00
panic("note out of range")
2015-01-11 11:52:11 +01:00
}
2018-01-29 17:13:39 +01:00
const vol = 1.0 / 16.0
size := (ebiten.TPS()/2 - 2) * sampleRate / ebiten.TPS()
2018-01-29 17:13:39 +01:00
l := make([]int16, size)
r := make([]int16, size)
2015-01-23 15:04:56 +01:00
square(l, vol, freq, 0.25)
square(r, vol, freq, 0.25)
2018-01-29 17:13:39 +01:00
p := g.audioContext.NewPlayerFromBytes(toBytes(l, r))
p.Play()
2018-01-29 17:13:39 +01:00
return rune(note)
2015-01-11 11:52:11 +01:00
}
func (g *Game) Update() error {
2018-01-29 17:13:39 +01:00
// Play notes for each half second.
if g.frames%30 == 0 && g.audioContext.IsReady() {
g.currentNote = g.playNote(g.scoreIndex)
2020-05-23 16:18:21 +02:00
g.scoreIndex++
g.scoreIndex %= len(score)
2017-03-27 17:56:14 +02:00
}
2020-05-23 16:18:21 +02:00
g.frames++
return nil
}
2018-01-29 17:13:39 +01:00
2020-05-23 16:18:21 +02:00
func (g *Game) Draw(screen *ebiten.Image) {
2017-03-27 17:56:14 +02:00
msg := "Note: "
2020-05-23 16:18:21 +02:00
if g.currentNote == 'R' || g.currentNote == 0 {
2017-03-27 17:56:14 +02:00
msg += "-"
} else {
2020-05-23 16:18:21 +02:00
msg += string(g.currentNote)
2017-03-27 17:56:14 +02:00
}
if !g.audioContext.IsReady() {
2018-10-13 15:41:37 +02:00
msg += "\n\n(If the audio doesn't start,\n click the screen or press keys)"
}
2017-03-27 17:56:14 +02:00
ebitenutil.DebugPrint(screen, msg)
2020-05-23 16:18:21 +02:00
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return screenWidth, screenHeight
2015-01-10 17:23:43 +01:00
}
func main() {
ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle("PCM (Ebitengine Demo)")
if err := ebiten.RunGame(NewGame()); err != nil {
2015-01-10 17:23:43 +01:00
log.Fatal(err)
}
}