Compare commits

...
10 Commits
Author SHA1 Message Date
dualfroz 6c932402eb Describe this repository
Test / Test with Go 1.25.x on macos-latest (push) Waiting to run
Test / Test with Go 1.25.x on ubuntu-latest (push) Waiting to run
Test / Test with Go 1.25.x on windows-latest (push) Waiting to run
Test / Test with Go 1.26.x on macos-latest (push) Waiting to run
Test / Test with Go 1.26.x on ubuntu-latest (push) Waiting to run
Test / Test with Go 1.26.x on windows-latest (push) Waiting to run
Test / Test with Go 1.27.x on macos-latest (push) Waiting to run
Test / Test with Go 1.27.x on ubuntu-latest (push) Waiting to run
Test / Test with Go 1.27.x on windows-latest (push) Waiting to run
Test / Test GBM with Go 1.25.x (push) Waiting to run
Test / Test GBM with Go 1.26.x (push) Waiting to run
Test / Test GBM with Go 1.27.x (push) Waiting to run
Test / Cross build with Go 1.25.x (push) Waiting to run
Test / Cross build with Go 1.26.x (push) Waiting to run
Test / Cross build with Go 1.27.x (push) Waiting to run
Test / Test Cgo with Go 1.25.x on macos-latest (push) Waiting to run
Test / Test Cgo with Go 1.25.x on ubuntu-latest (push) Waiting to run
Test / Test Cgo with Go 1.25.x on windows-latest (push) Waiting to run
Test / Test Cgo with Go 1.26.x on macos-latest (push) Waiting to run
Test / Test Cgo with Go 1.26.x on ubuntu-latest (push) Waiting to run
Test / Test Cgo with Go 1.26.x on windows-latest (push) Waiting to run
Test / Test Cgo with Go 1.27.x on macos-latest (push) Waiting to run
Test / Test Cgo with Go 1.27.x on ubuntu-latest (push) Waiting to run
Test / Test Cgo with Go 1.27.x on windows-latest (push) Waiting to run
Test / Test WebAssembly with Go 1.25.x (push) Waiting to run
Test / Test WebAssembly with Go 1.26.x (push) Waiting to run
Test / Test WebAssembly with Go 1.27.x (push) Waiting to run
2026-10-22 21:04:08 +02:00
dualfroz ecb0c10f07 audio: rename the context disposal to close, like the other unexported close methods 2026-10-05 16:19:48 +02:00
dualfroz 4300b92d4b audio: pair locks with defer and move the disposal of a context to audio.go 2026-10-04 12:56:43 +02:00
dualfroz 8198c5c4b3 .github/workflows: run go test -race for all packages (#3848)
Concurrency tests in the root package, text/v2, vector, internal/thread,
internal/hook, internal/gamepaddb, exp/textinput and exp/vmhost only
catch a data race under the race detector. Run go test -race on ./...
with -p=1, like the plain Linux step, in one Linux cell with the newest
Go, as the races are not platform specific and every Go version in the
matrix compiles the same code.

Closes #3569
2026-10-03 22:21:53 +09:00
upstream 9480cbfc46 Upstream hajimehoshi/ebiten state of the touched files before b07f89656b 2026-10-03 15:21:53 +02:00
dualfroz fab8fc22e0 audio: run TestPlayOnIdleContext on the fake clock of synctest
The test was added after the survey in #3716, and waits on real time in
the same way.

Updates #3716
2026-10-03 16:06:04 +02:00
dualfroz d22b1a2b09 audio: run the tests waiting on real time on the fake clock of synctest
TestGC, TestSameSourcePlayers, TestDeferredDeviceCreation,
TestPauseAndStopReading, TestPauseAndStopReadingWhilePaused,
TestPositionNotGrowingAfterFinished and
TestPlayerErrorDoesNotStopOtherPlayers now run in a synctest bubble, so
their sleeps and deadlines take no real time.

A bubble must not leave goroutines behind, so ResetContextForTesting
now stops the goroutine updating the players and the dummy players
reading their sources, and waits for them to exit. Before, every test
leaked them.

Updates #3716
2026-10-03 16:06:04 +02:00
upstream 87bac3749b Upstream hajimehoshi/ebiten state of the touched files before 040fcc781c 2026-10-02 16:34:40 +02:00
dualfroz ec545f506b internal/gamepad: don't fail when inotify is unavailable on Linux (#3705)
The inotify limits are per user, so another program can exhaust them and
make InotifyInit1 or InotifyAddWatch fail. That aborted RunGame with
"gamepad: InotifyAddWatch failed: no space left on device" even though
inotify is only used to detect hotplugged gamepads. GLFW ignores these
errors, so do the same and keep the gamepads found by the initial scan.

Closes #3304
2026-09-14 01:12:12 +09:00
upstream 3462b6821c Upstream hajimehoshi/ebiten state of the touched files before 39aa8ab4c0 2026-09-13 18:12:12 +02:00
6 changed files with 2539 additions and 0 deletions

No files matched your search

+365
View File
@@ -0,0 +1,365 @@
name: Test
on: [push, pull_request]
concurrency:
# A push event has no head_ref and falls back to a unique run ID, so pushes are never cancelled.
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
go: ['1.25.x', '1.26.x', '1.27.x']
name: Test with Go ${{ matrix.go }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
env:
DISPLAY: ':99.0'
defaults:
run:
shell: bash
steps:
- name: Git
run: |
# See actions/checkout#135
git config --global core.autocrlf false
git config --global core.eol lf
- name: Checkout
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- name: Install dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install libgl1 libgl1-mesa-dri libx11-6 libxcursor1 libxext6 libxi6 libxinerama1 libxrandr2 libxrender1
- name: Xvfb
if: runner.os == 'Linux'
run: |
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
- name: gofmt
# gofmt's results can differ between Go versions, but not between OSes.
if: runner.os == 'Linux' && matrix.go == '1.27.x'
run: |
files=$(gofmt -l .)
if [ -n "$files" ]; then
echo "$files"
exit 1
fi
- name: go vet (Linux)
if: runner.os == 'Linux'
run: |
go list ./... ./skills/run-ebitengine-app-headless/_driver | grep -v -x -F -f .github/workflows/govetblock_linbsd.txt | xargs go vet
- name: go vet (macOS)
if: runner.os == 'macOS'
run: |
go list ./... ./skills/run-ebitengine-app-headless/_driver | grep -v -x -F -f .github/workflows/govetblock_darwin.txt | xargs go vet
- name: go vet (Windows)
if: runner.os == 'Windows'
run: |
go list ./... ./skills/run-ebitengine-app-headless/_driver | grep -v -x -F -f .github/workflows/govetblock_windows.txt | xargs go vet
- name: go vet (vettool)
run: |
go install ./internal/vettools
go vet -vettool=$(which vettools)${{ runner.os == 'Windows' && '.exe' || '' }} -v ./... ./skills/run-ebitengine-app-headless/_driver
- name: go build
run: |
go build -v ./... ./skills/run-ebitengine-app-headless/_driver
# Compile without optimization to check potential stack overflow.
# The option '-gcflags=all=-N -l' is often used at Visual Studio Code.
# See also https://go.googlesource.com/vscode-go/+/HEAD/docs/debugging.md#launch and the issue #2120.
go build "-gcflags=all=-N -l" -v ./...
- name: go build (cross-compile from Windows)
if: runner.os == 'Windows'
run: |
# Check that cross-compilation works from a non-Linux host.
env GOOS=darwin GOARCH=arm64 go build -v ./...
env GOOS=linux GOARCH=amd64 go build -v ./...
- name: go build (cross-compile from macOS)
if: runner.os == 'macOS'
run: |
# Check that cross-compilation works from a non-Linux host.
env GOOS=linux GOARCH=amd64 go build -v ./...
env GOOS=windows GOARCH=amd64 go build -v ./...
- name: go build (macOS)
if: runner.os == 'macOS'
run: |
env GOOS=darwin GOARCH=amd64 go build -o flappy_amd64 -v ./examples/flappy
env GOOS=darwin GOARCH=arm64 go build -o flappy_arm64 -v ./examples/flappy
lipo -create flappy_amd64 flappy_arm64 -output flappy
file flappy
rm flappy
- name: go mod vendor
run: |
mkdir /tmp/vendoring
cd /tmp/vendoring
go mod init foo
# TODO: Is there a way to create a complete list of Ebitengine sub-packages?
echo 'package main' > main.go
echo 'import (' >> main.go
echo ' _ "github.com/hajimehoshi/ebiten/v2"' >> main.go
echo ' _ "github.com/hajimehoshi/ebiten/v2/audio"' >> main.go
echo ')' >> main.go
echo 'func main() {}' >> main.go
go mod edit -replace github.com/hajimehoshi/ebiten/v2=$GITHUB_WORKSPACE
go mod tidy
go mod vendor
go build -v .
- name: go test (Linux)
if: runner.os == 'Linux'
run: |
go test -shuffle=on -v -p=1 ./...
- name: go test -race (Linux)
if: runner.os == 'Linux' && matrix.go == '1.27.x'
run: |
go test -race -shuffle=on -v -p=1 ./...
- name: go test (Linux 386)
if: runner.os == 'Linux'
run: |
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install libgl1:i386 libgl1-mesa-dri:i386 libx11-6:i386 libxcursor1:i386 libxext6:i386 libxi6:i386 libxinerama1:i386 libxrandr2:i386 libxrender1:i386
env GOARCH=386 go test -shuffle=on -v -p=1 ./...
- name: go test (Linux OpenGL ES)
if: runner.os == 'Linux'
run: |
sudo apt-get install libegl1 libgles2
env EBITENGINE_GRAPHICS_LIBRARY=opengl go test -shuffle=on -v -p=1 ./...
- name: go test (Windows)
if: runner.os == 'Windows'
run: |
go test -shuffle=on -v ./...
env EBITENGINE_DIRECTX=version=12 go test -shuffle=on -v ./...
- name: go test (Windows 386)
if: runner.os == 'Windows'
run: |
env GOARCH=386 go test -shuffle=on -v ./...
env GOARCH=386 EBITENGINE_DIRECTX=version=12 go test -shuffle=on -v ./...
gbm:
strategy:
matrix:
go: ['1.25.x', '1.26.x', '1.27.x']
name: Test GBM with Go ${{ matrix.go }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install libdrm2 libegl1 libegl-mesa0 libgbm1 libgl1-mesa-dri libgles2 linux-modules-extra-$(uname -r)
- name: Load vkms
run: |
# vkms is a virtual KMS device with a connected output. Mesa renders for it with its software driver.
sudo modprobe vkms
sudo udevadm settle
sudo chmod a+rw /dev/dri/card*
- name: go test (GBM)
run: |
# With neither DISPLAY nor WAYLAND_DISPLAY set, Ebitengine presents through GBM and KMS.
# Only one process can be the DRM master at a time, so packages must run one by one.
go test -shuffle=on -v -p=1 ./...
cross-build:
strategy:
matrix:
go: ['1.25.x', '1.26.x', '1.27.x']
name: Cross build with Go ${{ matrix.go }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- name: go build
run: |
env GOOS=darwin GOARCH=amd64 go build -v ./...
env GOOS=darwin GOARCH=arm64 go build -v ./...
env GOOS=freebsd GOARCH=amd64 go build "-gcflags=github.com/ebitengine/purego/internal/fakecgo=-std" -v ./...
env GOOS=freebsd GOARCH=arm64 go build "-gcflags=github.com/ebitengine/purego/internal/fakecgo=-std" -v ./...
env GOOS=js GOARCH=wasm go build -v ./...
env GOOS=linux GOARCH=386 go build -v ./...
env GOOS=linux GOARCH=amd64 go build -v ./...
env GOOS=linux GOARCH=arm go build -v ./...
env GOOS=linux GOARCH=arm64 go build -v ./...
env GOOS=netbsd GOARCH=amd64 go build -v ./...
env GOOS=netbsd GOARCH=arm64 go build -v ./...
env GOOS=windows GOARCH=386 go build -v ./...
env GOOS=windows GOARCH=amd64 go build -v ./...
env GOOS=windows GOARCH=arm64 go build -v ./...
cgo:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
go: ['1.25.x', '1.26.x', '1.27.x']
name: Test Cgo with Go ${{ matrix.go }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- name: Git
run: |
# See actions/checkout#135
git config --global core.autocrlf false
git config --global core.eol lf
- name: Checkout
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- name: Setup JDK
uses: actions/setup-java@v5
with:
java-version: '11'
distribution: 'adopt'
- name: Install dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install libegl-dev
- name: Prepare ebitenmobile test
run: |
cd /tmp
git clone --depth=1 https://github.com/hajimehoshi/go-inovation
cd go-inovation
go mod edit -replace=github.com/hajimehoshi/ebiten/v2=$GITHUB_WORKSPACE
go mod tidy
- name: go build (NintendoSDK)
if: runner.os == 'Linux'
run: |
go build -tags=nintendosdk -v ./...
- name: go build (PlayStation 5)
if: runner.os == 'Linux'
run: |
go build -tags=playstation5 -v ./...
- name: go test internal/glfw (xlibcheck)
if: runner.os == 'Linux'
run: |
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install gcc-multilib
sudo apt-get install libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev libxrender-dev
sudo apt-get install libxcursor-dev:i386 libxi-dev:i386 libxinerama-dev:i386 libxrandr-dev:i386 libxrender-dev:i386
go test -shuffle=on -v -tags=ebitenginexlibcheck ./internal/glfw/
env CGO_ENABLED=1 GOARCH=386 go test -shuffle=on -v -tags=ebitenginexlibcheck ./internal/glfw/
- name: Install ebitenmobile
run: |
go install ./cmd/ebitenmobile
- name: ebitenmobile bind (Android)
run: |
cd /tmp/go-inovation
ebitenmobile bind -target android -androidapi 23 -javapkg com.hajimehoshi.goinovation -o inovation.aar -v github.com/hajimehoshi/go-inovation/mobile
- name: ebitenmobile bind (iOS)
if: runner.os == 'macOS'
run: |
cd /tmp/go-inovation
ebitenmobile bind -target ios -o Inovation.xcframework -v github.com/hajimehoshi/go-inovation/mobile
wasm:
strategy:
matrix:
go: ['1.25.x', '1.26.x', '1.27.x']
name: Test WebAssembly with Go ${{ matrix.go }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- name: Git
run: |
# See actions/checkout#135
git config --global core.autocrlf false
git config --global core.eol lf
- name: Checkout
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- name: Install Chrome
uses: browser-actions/setup-chrome@latest
- name: Install wasmbrowsertest
run: |
wasmbrowsertest_version=eec8f6b3828fd793968ed35171566ab20de7b4cc
go install github.com/agnivade/wasmbrowsertest@${wasmbrowsertest_version}
mv $(go env GOPATH)/bin/wasmbrowsertest${{ runner.os == 'Windows' && '.exe' || '' }} $(go env GOPATH)/bin/go_js_wasm_exec${{ runner.os == 'Windows' && '.exe' || '' }}
go install github.com/agnivade/wasmbrowsertest/cmd/cleanenv@${wasmbrowsertest_version}
- name: go test (Wasm)
run: |
# Disable AppArmor for Ubuntu 23.10+.
# https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md
echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
# Wasm tests don't work on macOS with the headless mode enabled, but the headless mode cannot be disabled in GitHub Actions (#2972).
# Wasm tests time out on Windows (#3076).
max_attempts=3
for i in $(seq 1 $max_attempts); do
env GOOS=js GOARCH=wasm cleanenv -remove-prefix GITHUB_ -remove-prefix JAVA_ -remove-prefix PSModulePath -remove-prefix STATS_ -remove-prefix RUNNER_ -- go test -shuffle=on -v ./... -test.paniconexit0=false && break
if [ $i -eq $max_attempts ]; then
echo "All $max_attempts attempts failed."
exit 1
fi
echo "Attempt $i failed. Retrying in 10 seconds..."
sleep 10
done
+12
View File
@@ -0,0 +1,12 @@
# Contributions to hajimehoshi/ebiten
A dead simple 2D game engine for Go
This repository holds only my commits to the upstream project, with their original author, date and diff.
Each of my commits is preceded by a snapshot of the upstream files it touched, so every diff shows exactly what I changed.
## Merged pull requests
- [audio: run the tests waiting on real time on synctest's fake clock](https://github.com/hajimehoshi/ebiten/pull/3849) (2026-10-06)
- [.github/workflows: run go test -race for all packages](https://github.com/hajimehoshi/ebiten/pull/3848) (2026-10-03)
- [internal/gamepad: don't fail when inotify is unavailable on Linux](https://github.com/hajimehoshi/ebiten/pull/3705) (2026-09-13)
+768
View File
@@ -0,0 +1,768 @@
// 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 audio provides audio players.
//
// This package must be used within an Ebitengine game; it does not work standalone, as the
// audio device is initialized only after the game starts running. To play audio outside an
// Ebitengine game, use Oto ([github.com/ebitengine/oto/v3]) directly.
//
// The stream format must be 16-bit little endian or 32-bit float little endian, and 2 channels. The format is as follows:
//
// [data] = [sample 1] [sample 2] [sample 3] ...
// [sample *] = [channel 1] [channel 2]
// [channel *] = [byte 1] [byte 2] ...
//
// A channel is 2 bytes in the 16-bit integer format, and 4 bytes in the 32-bit float format.
//
// An audio context (audio.Context object) has a sample rate you can specify
// and all streams you want to play must have the same sample rate.
//
// An audio context can generate 'players' (audio.Player objects),
// and you can play sound by calling Play function of players.
// When multiple players play, mixing is automatically done.
// Note that too many players may cause distortion.
//
// For the simplest example to play sound, see the wav package in the examples.
package audio
import (
"bytes"
"errors"
"fmt"
"io"
"reflect"
"runtime"
"sync"
"time"
"github.com/hajimehoshi/ebiten/v2/audio/internal/convert"
"github.com/hajimehoshi/ebiten/v2/internal/hook"
)
const (
channelCount = 2
bitDepthInBytesInt16 = 2
bitDepthInBytesFloat32 = 4
)
// A Context represents a current state of audio.
//
// At most one Context object can exist in one process.
// This means only one constant sample rate is valid in your one application.
//
// For a typical usage example, see examples/wav/main.go.
type Context struct {
playerFactory *playerFactory
sampleRate int
err error
ready bool
playingPlayers map[*playerImpl]struct{}
m sync.Mutex
// cond is signalled under m when a player is added to playingPlayers or when the context is
// closed. The goroutine updating the players waits on it while playingPlayers is empty.
cond *sync.Cond
// closed reports that the goroutine updating the players must exit. A context lives until the
// process exits, so this is set only when a test discards the context.
closed bool
// updater tracks the goroutine updating the players, so that a test discarding the context can wait
// for the goroutine to exit.
updater sync.WaitGroup
}
var (
theContext *Context
theContextLock sync.Mutex
)
// NewContext creates a new audio context with the given sample rate.
//
// sampleRate specifies the number of samples that should be played during one second.
// Usual numbers are 44100 or 48000. One context has only one sample rate. You cannot play multiple audio
// sources with different sample rates at the same time.
//
// NewContext panics when sampleRate is not positive or an audio context is already created.
func NewContext(sampleRate int) *Context {
if sampleRate <= 0 {
panic(fmt.Sprintf("audio: sample rate must be positive but was %d", sampleRate))
}
theContextLock.Lock()
defer theContextLock.Unlock()
if theContext != nil {
panic("audio: context is already created")
}
c := &Context{
sampleRate: sampleRate,
playerFactory: newPlayerFactory(sampleRate),
playingPlayers: map[*playerImpl]struct{}{},
}
c.cond = sync.NewCond(&c.m)
theContext = c
h := getHook()
h.OnSuspendAudio(func() error {
if err := c.playerFactory.suspend(); err != nil {
return err
}
if err := c.onSuspend(); err != nil {
return err
}
return nil
})
h.OnResumeAudio(func() error {
if err := c.playerFactory.resume(); err != nil {
return err
}
if err := c.onResume(); err != nil {
return err
}
return nil
})
h.AppendHookOnBeforeUpdateWithVMGuestInfo(func(vmGuest bool) error {
var err error
theContextLock.Lock()
if theContext != nil {
err = theContext.error()
}
theContextLock.Unlock()
if err != nil {
return err
}
// Create the audio device here, on the first update after the UI backend is
// initialized, rather than when a player is first touched. Deferring the creation
// keeps the device from being created before the environment is known (#969, #970,
// #2715, #3438). This also initializes the device when there is no player and the
// program waits for IsReady() to be true.
ready, err := c.playerFactory.initContextIfNeeded(vmGuest)
if err != nil {
return err
}
if ready != nil {
go func() {
<-ready
c.setReady()
}()
}
return nil
})
// In the current Ebitengine implementation, update might not be called when the window is in the background (#3154).
// In this case, an audio player's position is not updated correctly with AppendHookOnBeforeUpdateWithVMGuestInfo.
// Use a distinct goroutine to update the player states.
c.updater.Go(func() {
for {
// Block while there is no player to update, so that an idle context does not wake
// up periodically. The sweep itself can remove the last player, so the check is
// repeated after every sweep.
if !c.waitForPlayingPlayers() {
return
}
// A failure is local to the player which reported it. Record it and keep updating
// the other players (#3647).
if err := c.updatePlayers(); err != nil {
c.setError(err)
}
time.Sleep(time.Second / 100)
}
})
return c
}
// CurrentContext returns the current context or nil if there is no context.
func CurrentContext() *Context {
theContextLock.Lock()
c := theContext
theContextLock.Unlock()
return c
}
func (c *Context) setError(err error) {
c.m.Lock()
defer c.m.Unlock()
c.err = errors.Join(c.err, err)
}
func (c *Context) error() error {
c.m.Lock()
defer c.m.Unlock()
if c.err != nil {
return fmt.Errorf("audio: context error: %w", c.err)
}
if err := c.playerFactory.error(); err != nil {
return fmt.Errorf("audio: context error: %w", err)
}
return nil
}
func (c *Context) setReady() {
c.m.Lock()
c.ready = true
c.m.Unlock()
}
// waitForPlayingPlayers blocks until at least one player is registered as playing, and reports
// whether the players should be updated. It returns false once the context is closed.
func (c *Context) waitForPlayingPlayers() bool {
c.m.Lock()
defer c.m.Unlock()
for len(c.playingPlayers) == 0 && !c.closed {
c.cond.Wait()
}
return !c.closed
}
// close makes the goroutine updating the players exit, and waits for it to exit.
func (c *Context) close() {
c.markClosed()
c.updater.Wait()
}
func (c *Context) markClosed() {
c.m.Lock()
defer c.m.Unlock()
c.closed = true
c.cond.Broadcast()
}
func (c *Context) addPlayingPlayer(p *playerImpl) {
c.m.Lock()
defer c.m.Unlock()
c.playingPlayers[p] = struct{}{}
c.cond.Signal()
// An uncomparable ident is out of the duplication check (#3039).
ident := p.sourceIdent()
if !reflect.ValueOf(ident).Comparable() {
return
}
// Check the source duplication. The players other than p have been checked against each
// other when they were added, so it is enough to compare p with them.
for playing := range c.playingPlayers {
if playing == p {
continue
}
playingIdent := playing.sourceIdent()
if !reflect.ValueOf(playingIdent).Comparable() {
continue
}
if playingIdent == ident {
c.err = errors.Join(c.err, errors.New("audio: the same source must not be used by multiple Player objects"))
return
}
}
}
func (c *Context) removePlayingPlayer(p *playerImpl) {
c.m.Lock()
delete(c.playingPlayers, p)
c.m.Unlock()
}
func (c *Context) onSuspend() error {
// A Context must not call playerImpl's functions with a lock, or this causes a deadlock (#2737).
// Copy the playerImpls and iterate them without a lock.
var players []*playerImpl
c.m.Lock()
players = make([]*playerImpl, 0, len(c.playingPlayers))
for p := range c.playingPlayers {
players = append(players, p)
}
c.m.Unlock()
for _, p := range players {
if err := p.Err(); err != nil {
return err
}
p.onContextSuspended()
}
return nil
}
func (c *Context) onResume() error {
// A Context must not call playerImpl's functions with a lock, or this causes a deadlock (#2737).
// Copy the playerImpls and iterate them without a lock.
var players []*playerImpl
c.m.Lock()
players = make([]*playerImpl, 0, len(c.playingPlayers))
for p := range c.playingPlayers {
players = append(players, p)
}
c.m.Unlock()
for _, p := range players {
if err := p.Err(); err != nil {
return err
}
p.onContextResumed()
}
return nil
}
func (c *Context) updatePlayers() error {
// A Context must not call playerImpl's functions with a lock, or this causes a deadlock (#2737).
// Copy the playerImpls and iterate them without a lock.
var players []*playerImpl
c.m.Lock()
players = make([]*playerImpl, 0, len(c.playingPlayers))
for p := range c.playingPlayers {
players = append(players, p)
}
c.m.Unlock()
// In the current implementation, players cannot remove themselves from the context when the underlying player stops by itself.
// The underlying player can become paused after finishing playback,
// but there is no way to notify this to players so far.
// Instead, let's check the states proactively every frame.
//
// A failing player must not stop the sweep: the other players still need their positions
// updated and their pending plays started (#3647). Collect the errors and go on.
var errs []error
for _, p := range players {
if err := p.Err(); err != nil {
errs = append(errs, err)
// A player whose device player failed does not play again. Drop it from the playing
// players so that its error is reported once, not on every sweep.
c.removePlayingPlayer(p)
continue
}
// Start playing if Play was requested before the audio device was created.
if err := p.startIfPending(); err != nil {
errs = append(errs, err)
continue
}
p.updatePosition()
// The player itself decides whether it is removed, so that a player restarted between
// the check and the removal is not dropped from the context.
p.removeFromContextIfNotPlaying()
}
return errors.Join(errs...)
}
// IsReady returns a boolean value indicating whether the audio is ready or not.
//
// On some browsers, user interaction like click or pressing keys is required to start audio.
func (c *Context) IsReady() bool {
c.m.Lock()
defer c.m.Unlock()
return c.ready
}
// SampleRate returns the sample rate.
func (c *Context) SampleRate() int {
return c.sampleRate
}
// Player is an audio player which has one stream.
//
// Even when all references to a Player object are gone,
// the object is not GCed until the player finishes playing.
// This means that if a Player plays an infinite stream,
// the object is never GCed unless [Player.Pause] or [Player.PauseAndStopReading] is called.
type Player struct {
p *playerImpl
cleanup runtime.Cleanup
}
// NewPlayer creates a new player with the given stream.
//
// src's format must be linear PCM (signed 16-bit little endian, 2 channel stereo)
// without a header (e.g. RIFF header).
// The sample rate must be same as that of the audio context.
//
// The player is seekable when src is io.Seeker.
// Attempt to seek the player that is not io.Seeker causes panic.
//
// Note that the given src can't be shared with other Player objects.
//
// NewPlayer does not touch src. The player asks src for its current position when it
// first uses src, at [Player.Play] or [Player.SetPosition].
//
// A Player doesn't close src even if src implements io.Closer.
// Closing the source is src owner's responsibility.
//
// For new code, NewPlayerF32 is preferable to NewPlayer, since Ebitengine will treat only 32bit float audio internally in the future.
//
// A Player for 16bit integer must be used with 16bit integer version of audio APIs, like vorbis.DecodeWithoutResampling or audio.NewInfiniteLoop, not vorbis.DecodeF32 or audio.NewInfiniteLoopF32.
func (c *Context) NewPlayer(src io.Reader) (*Player, error) {
_, seekable := src.(io.Seeker)
f32Src := convert.NewFloat32BytesReaderFromInt16BytesReader(src)
pi, err := c.playerFactory.newPlayer(c, f32Src, seekable, src, bitDepthInBytesFloat32)
if err != nil {
return nil, err
}
p := &Player{p: pi}
p.cleanup = runtime.AddCleanup(p, (*playerImpl).finalize, p.p)
return p, nil
}
// NewPlayerF32 creates a new player with the given stream.
//
// src's format must be linear PCM (32bit float, little endian, 2 channel stereo)
// without a header (e.g. RIFF header).
// The sample rate must be same as that of the audio context.
//
// The player is seekable when src is io.Seeker.
// Attempt to seek the player that is not io.Seeker causes panic.
//
// Note that the given src can't be shared with other Player objects.
//
// NewPlayerF32 does not touch src. The player asks src for its current position when it
// first uses src, at [Player.Play] or [Player.SetPosition].
//
// A Player doesn't close src even if src implements io.Closer.
// Closing the source is src owner's responsibility.
//
// For new code, NewPlayerF32 is preferable to NewPlayer, since Ebitengine will treat only 32bit float audio internally in the future.
//
// A Player for 32bit float must be used with 32bit float version of audio APIs, like vorbis.DecodeF32 or audio.NewInfiniteLoopF32, not vorbis.DecodeWithoutResampling or audio.NewInfiniteLoop.
func (c *Context) NewPlayerF32(src io.Reader) (*Player, error) {
_, seekable := src.(io.Seeker)
pi, err := c.playerFactory.newPlayer(c, src, seekable, src, bitDepthInBytesFloat32)
if err != nil {
return nil, err
}
p := &Player{p: pi}
p.cleanup = runtime.AddCleanup(p, (*playerImpl).finalize, p.p)
return p, nil
}
// NewPlayer creates a new player with the given stream.
//
// Deprecated: as of v2.2. Use (*Context).NewPlayer instead.
func NewPlayer(context *Context, src io.Reader) (*Player, error) {
return context.NewPlayer(src)
}
// NewPlayerFromBytes creates a new player with the given bytes.
//
// As opposed to NewPlayer, you don't have to care if src is already used by another player or not.
// src can be shared by multiple players.
//
// The format of src should be same as noted at NewPlayer.
func (c *Context) NewPlayerFromBytes(src []byte) *Player {
p, err := c.NewPlayer(bytes.NewReader(src))
if err != nil {
// Errors should never happen.
panic(fmt.Sprintf("audio: NewPlayerFromBytes failed: %v", err))
}
return p
}
// NewPlayerF32FromBytes creates a new player with the given bytes.
//
// As opposed to NewPlayerF32, you don't have to care if src is already used by another player or not.
// src can be shared by multiple players.
//
// The format of src should be same as noted at NewPlayerF32.
func (c *Context) NewPlayerF32FromBytes(src []byte) *Player {
p, err := c.NewPlayerF32(bytes.NewReader(src))
if err != nil {
// Errors should never happen.
panic(fmt.Sprintf("audio: NewPlayerF32FromBytes failed: %v", err))
}
return p
}
// NewPlayerFromBytes creates a new player with the given bytes.
//
// Deprecated: as of v2.2. Use (*Context).NewPlayerFromBytes instead.
func NewPlayerFromBytes(context *Context, src []byte) *Player {
return context.NewPlayerFromBytes(src)
}
// finalize closes the player when its Player wrapper is no longer reachable.
//
// A Player becomes unreachable as early as the moment one of its methods reads the playerImpl out
// of it, so finalize can run while that method is still in flight. Every Player method keeps its
// receiver alive with runtime.KeepAlive until the delegated call returns, so that finalize does
// not close the player under it.
func (p *playerImpl) finalize() {
if !p.IsPlaying() {
_ = p.Close()
}
}
// Close closes the stream.
//
// When Close is called, the stream owned by the player is NOT closed,
// even if the stream implements io.Closer.
//
// Close returns error when the player is already closed.
//
// Deprecated: as of v2.10. Use [Player.PauseAndStopReading] instead.
func (p *Player) Close() error {
defer runtime.KeepAlive(p)
return p.p.Close()
}
// Play plays the stream.
func (p *Player) Play() {
defer runtime.KeepAlive(p)
p.p.Play()
}
// IsPlaying returns a boolean indicating whether the player is playing.
func (p *Player) IsPlaying() bool {
defer runtime.KeepAlive(p)
return p.p.IsPlaying()
}
// Rewind rewinds the current position to the start.
//
// The passed source to NewPlayer must be io.Seeker, or Rewind panics.
// Rewind does not panic before Play is called, as it needs no source access
// to rewind a player that has not read the source yet.
//
// Rewind returns error when seeking the source stream returns error.
func (p *Player) Rewind() error {
defer runtime.KeepAlive(p)
return p.p.Rewind()
}
// SetPosition sets the position with the given offset.
//
// The passed source to NewPlayer must be io.Seeker, or SetPosition panics.
// A zero offset does not panic before Play is called, as it needs no source
// access to reset a player that has not read the source yet.
//
// SetPosition returns error when seeking the source stream returns an error.
// SetPosition returns an error for a negative offset.
func (p *Player) SetPosition(offset time.Duration) error {
defer runtime.KeepAlive(p)
return p.p.SetPosition(offset)
}
// Seek seeks the position with the given offset.
//
// Deprecated: as of v2.6. Use SetPosition instead.
func (p *Player) Seek(offset time.Duration) error {
return p.SetPosition(offset)
}
// Pause pauses the playing.
func (p *Player) Pause() {
defer runtime.KeepAlive(p)
p.p.Pause()
}
// PauseAndStopReading pauses the playing and stops reading the source.
// After PauseAndStopReading returns, this player does not read the source until [Player.Play],
// [Player.SetPosition], or [Player.Rewind] is called, so the source can be closed safely.
// The buffered data is kept, and [Player.Play] resumes the playing without a gap.
// PauseAndStopReading blocks until an ongoing read from the source finishes, if any.
func (p *Player) PauseAndStopReading() {
defer runtime.KeepAlive(p)
p.p.PauseAndStopReading()
}
// Position returns the current position in time.
//
// As long as the player continues to play, Position's returning value is increased monotonically,
// even though the source stream loops and its position goes back.
func (p *Player) Position() time.Duration {
defer runtime.KeepAlive(p)
return p.p.Position()
}
// Current returns the current position in time.
//
// Deprecated: as of v2.6. Use Position instead.
func (p *Player) Current() time.Duration {
return p.Position()
}
// Volume returns the current volume of this player, which is 0 or larger.
func (p *Player) Volume() float64 {
defer runtime.KeepAlive(p)
return p.p.Volume()
}
// SetVolume sets the volume of this player, which must be in the range of [0, math.MaxFloat32].
// A volume larger than 1 amplifies the sound and might cause clipping.
// A value out of the range, including NaN, is treated as 0.
func (p *Player) SetVolume(volume float64) {
defer runtime.KeepAlive(p)
p.p.SetVolume(volume)
}
// SetBufferSize adjusts the buffer size of the player.
// If a nonpositive value or a value too large to represent is specified, the default buffer size is used.
// A small buffer size is useful if you want to play a real-time PCM for example.
// Note that the audio quality might be affected if you modify the buffer size.
func (p *Player) SetBufferSize(bufferSize time.Duration) {
defer runtime.KeepAlive(p)
p.p.SetBufferSize(bufferSize)
}
type hooker interface {
OnSuspendAudio(f func() error)
OnResumeAudio(f func() error)
AppendHookOnBeforeUpdateWithVMGuestInfo(f func(vmGuest bool) error)
}
var hookerForTesting hooker
func getHook() hooker {
if hookerForTesting != nil {
return hookerForTesting
}
return &hookerImpl{}
}
type hookerImpl struct{}
func (h *hookerImpl) OnSuspendAudio(f func() error) {
hook.OnSuspendAudio(f)
}
func (h *hookerImpl) OnResumeAudio(f func() error) {
hook.OnResumeAudio(f)
}
func (h *hookerImpl) AppendHookOnBeforeUpdateWithVMGuestInfo(f func(vmGuest bool) error) {
hook.AppendHookOnBeforeUpdateWithVMGuestInfo(f)
}
// ResampleReader converts the sample rate of the given signed 16bit integer, little-endian, 2 channels (stereo) stream.
// length is the length of the source stream in bytes. 0 indicates the length is unknown.
// from is the original sample rate.
// to is the target sample rate.
// ResampleReader panics if from or to is not positive.
//
// If the source ends before length bytes, the remainder of the result is silence.
//
// If the original sample rate equals the new one, ResampleReader returns source as it is and does not apply length.
// When the rates differ, the returned value's Read returns only whole samples (4 bytes each), and its Seek rounds a position in
// the middle of a sample down to a sample boundary.
// For a non-empty buffer shorter than one sample, Read returns [io.ErrShortBuffer], or [io.EOF] once the stream has ended.
//
// The returned value implements io.Seeker when the source implements io.Seeker.
// The returned value might implement io.Seeker even when the source doesn't implement io.Seeker, but
// there is no guarantee that the Seek function works correctly.
func ResampleReader(source io.Reader, length int64, from, to int) io.Reader {
validateResamplingSampleRates(from, to)
if from == to {
return source
}
return convert.NewResampling(source, resamplingSourceLength(length), from, to, bitDepthInBytesInt16)
}
// ResampleReaderF32 converts the sample rate of the given 32bit float, little-endian, 2 channels (stereo) stream.
// length is the length of the source stream in bytes. 0 indicates the length is unknown.
// from is the original sample rate.
// to is the target sample rate.
// ResampleReaderF32 panics if from or to is not positive.
//
// If the source ends before length bytes, the remainder of the result is silence.
//
// If the original sample rate equals the new one, ResampleReaderF32 returns source as it is and does not apply length.
// When the rates differ, the returned value's Read returns only whole samples (8 bytes each), and its Seek rounds a position in
// the middle of a sample down to a sample boundary.
// For a non-empty buffer shorter than one sample, Read returns [io.ErrShortBuffer], or [io.EOF] once the stream has ended.
//
// The returned value implements io.Seeker when the source implements io.Seeker.
// The returned value might implement io.Seeker even when the source doesn't implement io.Seeker, but
// there is no guarantee that the Seek function works correctly.
func ResampleReaderF32(source io.Reader, length int64, from, to int) io.Reader {
validateResamplingSampleRates(from, to)
if from == to {
return source
}
return convert.NewResampling(source, resamplingSourceLength(length), from, to, bitDepthInBytesFloat32)
}
// Resample converts the sample rate of the given signed 16bit integer, little-endian, 2 channels (stereo) stream.
// length is the length of the source stream in bytes. 0 indicates the length is unknown.
// from is the original sample rate.
// to is the target sample rate.
// Resample panics if from or to is not positive.
//
// If the source ends before length bytes, the remainder of the result is silence.
//
// If the original sample rate equals the new one, Resample returns source as it is and does not apply length.
// When the rates differ, the returned value's Read returns only whole samples (4 bytes each), and its Seek rounds a position in
// the middle of a sample down to a sample boundary.
// For a non-empty buffer shorter than one sample, Read returns [io.ErrShortBuffer], or [io.EOF] once the stream has ended.
//
// Deprecated: as of v2.9. Use ResampleReader instead.
func Resample(source io.ReadSeeker, length int64, from, to int) io.ReadSeeker {
validateResamplingSampleRates(from, to)
if from == to {
return source
}
return convert.NewResampling(source, resamplingSourceLength(length), from, to, bitDepthInBytesInt16)
}
// ResampleF32 converts the sample rate of the given 32bit float, little-endian, 2 channels (stereo) stream.
// length is the length of the source stream in bytes. 0 indicates the length is unknown.
// from is the original sample rate.
// to is the target sample rate.
// ResampleF32 panics if from or to is not positive.
//
// If the source ends before length bytes, the remainder of the result is silence.
//
// If the original sample rate equals the new one, ResampleF32 returns source as it is and does not apply length.
// When the rates differ, the returned value's Read returns only whole samples (8 bytes each), and its Seek rounds a position in
// the middle of a sample down to a sample boundary.
// For a non-empty buffer shorter than one sample, Read returns [io.ErrShortBuffer], or [io.EOF] once the stream has ended.
//
// Deprecated: as of v2.9. Use ResampleReaderF32 instead.
func ResampleF32(source io.ReadSeeker, length int64, from, to int) io.ReadSeeker {
validateResamplingSampleRates(from, to)
if from == to {
return source
}
return convert.NewResampling(source, resamplingSourceLength(length), from, to, bitDepthInBytesFloat32)
}
// resamplingSourceLength converts a length given to ResampleReader, ResampleReaderF32, Resample, or ResampleF32,
// where 0 indicates an unknown length, to a length for convert.NewResampling,
// where a negative value indicates an unknown length and 0 an empty stream.
//
// TODO: Let these functions take a negative value instead of 0 for an unknown length (#3395).
func resamplingSourceLength(length int64) int64 {
if length == 0 {
return -1
}
return length
}
func validateResamplingSampleRates(from, to int) {
if from <= 0 {
panic(fmt.Sprintf("audio: original sample rate must be positive but was %d", from))
}
if to <= 0 {
panic(fmt.Sprintf("audio: target sample rate must be positive but was %d", to))
}
}
+1011
View File
File diff suppressed because it is too large. Load diff
+380
View File
@@ -0,0 +1,380 @@
// Copyright 2018 The Ebiten Authors
//
// 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 audio
import (
"io"
"sync"
"time"
)
type (
dummyContext struct {
// suspendErr and resumeErr are the errors Suspend and Resume return, to simulate a device
// which fails to suspend or resume.
suspendErr error
resumeErr error
// players are the players created since the context was reset last. ResetContextForTesting
// stops them, so that a player reading an infinite source does not keep reading after its test.
players []*dummyPlayer
mu sync.Mutex
}
dummyPlayer struct {
r io.Reader
playing bool
volume float64
// eof is whether the source has been exhausted by playing through. Like the real players,
// a player which has finished its source refuses to play until Seek resets this state.
eof bool
// drained is whether the simulated device has output the data it had buffered when the
// source was exhausted. See BufferedSize.
drained bool
// readGen is incremented by PauseAndStopReading to stop the goroutine reading r.
readGen int
// readers tracks the goroutines reading r, so that a test can wait for them to stop.
readers sync.WaitGroup
// err is the first non-EOF error the source returned. Like the real players, a player
// whose source failed stops for good and reports the error from Err.
err error
mu sync.Mutex
}
)
func (c *dummyContext) NewPlayer(r io.Reader) player {
p := &dummyPlayer{
r: r,
volume: 1,
}
c.mu.Lock()
defer c.mu.Unlock()
c.players = append(c.players, p)
return p
}
// stopPlayers stops the players' goroutines reading their sources, and waits for them to exit.
func (c *dummyContext) stopPlayers() {
for _, p := range c.takePlayers() {
p.PauseAndStopReading()
p.readers.Wait()
}
}
func (c *dummyContext) takePlayers() []*dummyPlayer {
c.mu.Lock()
defer c.mu.Unlock()
players := c.players
c.players = nil
return players
}
func (c *dummyContext) MaxBufferSize() int {
return 48000 * channelCount * bitDepthInBytesInt16 / 4
}
func (c *dummyContext) Suspend() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.suspendErr
}
func (c *dummyContext) Resume() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.resumeErr
}
func (c *dummyContext) Err() error {
return nil
}
func (p *dummyPlayer) Pause() {
p.mu.Lock()
p.playing = false
p.mu.Unlock()
}
func (p *dummyPlayer) Play() {
p.mu.Lock()
defer p.mu.Unlock()
if p.eof || p.err != nil {
return
}
p.playing = true
gen := p.readGen
p.readers.Go(func() {
var buf [4096]byte
for {
stopped, err := p.readOnce(gen, buf[:])
if stopped {
return
}
if err != nil {
break
}
time.Sleep(time.Millisecond)
}
p.mu.Lock()
defer p.mu.Unlock()
// The source is exhausted only when it was played through. A paused player would still
// have unplayed data in its buffer in a real player.
if p.playing {
p.eof = true
}
})
}
// readOnce performs one read from the source with the mutex held, so that PauseAndStopReading waits
// for an in-flight read. stopped reports that reading must not continue: PauseAndStopReading was
// called, or the source failed. err is io.EOF when the source is exhausted.
func (p *dummyPlayer) readOnce(gen int, buf []byte) (stopped bool, err error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.readGen != gen {
return true, nil
}
_, err = p.r.Read(buf)
if err != nil && err != io.EOF {
p.err = err
p.playing = false
return true, nil
}
return false, err
}
func (p *dummyPlayer) PauseAndStopReading() {
p.mu.Lock()
p.playing = false
p.readGen++
p.mu.Unlock()
}
func (p *dummyPlayer) IsPlaying() bool {
p.mu.Lock()
defer p.mu.Unlock()
return p.playing
}
func (p *dummyPlayer) Volume() float64 {
// The volume is not shared with the goroutine Play spawns, but the mutex is held so that the
// test double is uniformly safe like the real player it stands in for.
p.mu.Lock()
defer p.mu.Unlock()
return p.volume
}
func (p *dummyPlayer) SetVolume(volume float64) {
// See Volume for why the mutex is held.
p.mu.Lock()
defer p.mu.Unlock()
p.volume = volume
}
// BufferedSize always reports an empty buffer, as the simulated device consumes the data as soon as
// it is read.
//
// The context calls this exactly once per tick, before it reads the position and checks IsPlaying.
// The player keeps playing for one more tick after its source is exhausted, so that the position
// reaches its final value while the player is still playing, as a real player which still has
// buffered data to output.
func (p *dummyPlayer) BufferedSize() int {
p.mu.Lock()
defer p.mu.Unlock()
if p.eof {
if p.drained {
p.playing = false
}
p.drained = true
}
return 0
}
func (p *dummyPlayer) Err() error {
p.mu.Lock()
defer p.mu.Unlock()
return p.err
}
func (p *dummyPlayer) SetBufferSize(bufferSize int) {
}
func (p *dummyPlayer) Seek(offset int64, whence int) (int64, error) {
// Seeking discards the buffered data and resets the finished state as real players do, so
// that the source can be played again.
p.mu.Lock()
defer p.mu.Unlock()
p.eof = false
p.drained = false
return 0, nil
}
var dummyContextForTesting = &dummyContext{}
func init() {
driverForTesting = dummyContextForTesting
}
// SetSuspendErrorForTesting makes the simulated device fail to suspend with err, or succeed when err
// is nil.
func SetSuspendErrorForTesting(err error) {
c := dummyContextForTesting
c.mu.Lock()
defer c.mu.Unlock()
c.suspendErr = err
}
// SetResumeErrorForTesting makes the simulated device fail to resume with err, or succeed when err is
// nil.
func SetResumeErrorForTesting(err error) {
c := dummyContextForTesting
c.mu.Lock()
defer c.mu.Unlock()
c.resumeErr = err
}
type dummyHook struct {
onSuspend func() error
onResume func() error
updates []func(vmGuest bool) error
}
func (h *dummyHook) OnSuspendAudio(f func() error) {
h.onSuspend = f
}
func (h *dummyHook) OnResumeAudio(f func() error) {
h.onResume = f
}
func (h *dummyHook) AppendHookOnBeforeUpdateWithVMGuestInfo(f func(vmGuest bool) error) {
h.updates = append(h.updates, f)
}
func init() {
hookerForTesting = &dummyHook{}
}
// SuspendForTesting runs the suspend hook the current context registered.
func SuspendForTesting() error {
return hookerForTesting.(*dummyHook).onSuspend()
}
// ResumeForTesting runs the resume hook the current context registered.
func ResumeForTesting() error {
return hookerForTesting.(*dummyHook).onResume()
}
func UpdateForTesting() error {
for _, f := range hookerForTesting.(*dummyHook).updates {
if err := f(false); err != nil {
return err
}
}
return nil
}
func PlayersCountForTesting() int {
c := CurrentContext()
c.m.Lock()
n := len(c.playingPlayers)
c.m.Unlock()
return n
}
func EnsurePlayerForTesting(p *Player) error {
// ensurePlayer accesses player state with its caller holding the mutex, as Play and startIfPending do.
p.p.m.Lock()
defer p.p.m.Unlock()
return p.p.ensurePlayer()
}
func BufferSizeForTesting(p *Player) int {
// initBufferSize is protected by the player mutex and can change when the underlying player is created.
p.p.m.Lock()
defer p.p.m.Unlock()
return p.p.initBufferSize
}
// PlayingButUntrackedForTesting reports whether the player is playing but is not tracked by its
// context as a playing player. This must never be true: an untracked playing player is not
// updated by the context and is not guarded from being garbage-collected.
//
// The player's lock is held across both checks, so that a player which is being started or
// stopped concurrently is not reported.
func PlayingButUntrackedForTesting(p *Player) bool {
pi := p.p
// isPlaying requires its caller to hold the player mutex. Keep it held across both checks.
pi.m.Lock()
defer pi.m.Unlock()
if pi.closed || !pi.isPlaying() {
return false
}
c := pi.context
// The context mutex protects playingPlayers from concurrent additions and removals.
c.m.Lock()
defer c.m.Unlock()
_, ok := c.playingPlayers[pi]
return !ok
}
// ContextCreatedForTesting reports whether the underlying audio device has been created.
func ContextCreatedForTesting() bool {
c := CurrentContext()
if c == nil {
return false
}
return c.playerFactory.currentContext() != nil
}
// ResetContextForTesting discards the current context, so that a test can create a new one.
// ResetContextForTesting stops the goroutine updating the players and the goroutines reading the
// players' sources, and waits for them to exit.
func ResetContextForTesting() {
if c := takeContextForTesting(); c != nil {
c.close()
}
dummyContextForTesting.stopPlayers()
}
func takeContextForTesting() *Context {
theContextLock.Lock()
defer theContextLock.Unlock()
c := theContext
theContext = nil
return c
}
func (i *InfiniteLoop) SetNoBlendForTesting(value bool) {
i.noBlendForTesting = value
}
// NewTimeStreamForTesting returns the stream a Player reads from, over a seekable src in signed 16bit
// integer stereo.
func NewTimeStreamForTesting(src io.ReadSeeker, sampleRate int) (io.ReadSeeker, error) {
return newTimeStream(src, true, sampleRate, bitDepthInBytesInt16)
}
+3
View File
@@ -441,6 +441,9 @@ func (g *nativeGamepadImpl) pollAbsState() error {
}
func (g *nativeGamepadImpl) handleAbsEvent(code int, value int32) {
if code < 0 || code >= len(g.absMap) {
return
}
index := g.absMap[code]
if index < 0 {
return