Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
run: |
go build -v ./...
# Compile without optimization to check potential stack overflow.
# The option '-gcflags=all=-N -l' is often used at Visual Studio Code.
# The option '-gcflags=all=-N -l' is often used in Visual Studio Code.
Comment thread
kumagi marked this conversation as resolved.
# See also https://go.googlesource.com/vscode-go/+/HEAD/docs/debugging.md#launch and the issue hajimehoshi/ebiten#2120.
go build "-gcflags=all=-N -l" -v ./...

Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Add them to "Linked Frameworks and Libraries" on your Xcode project.
### Linux, FreeBSD, OpenBSD

Oto uses PulseAudio on Linux and BSD systems via the pure-Go package `github.com/jfreymuth/pulse`,
though BSD systems are not tested well.
though BSD systems are not well tested.

If the PulseAudio server is not discoverable automatically, set `PULSE_SERVER`.

Expand All @@ -68,7 +68,7 @@ is enabled by default, need nothing extra.

## Usage

The two main components of Oto are a `Context` and `Players`. The context handles interactions with
The two main components of Oto are `Context` and `Player`. The context handles interactions with
the OS and audio drivers, and as such there can only be **one** context in your program.

From a context you can create any number of different players, where each player is given an `io.Reader` that
Expand Down Expand Up @@ -116,7 +116,7 @@ func main() {
// Usually 44100 or 48000. Other values might cause distortions in Oto
op.SampleRate = 44100

// Number of channels (aka locations) to play sounds from. Either 1 or 2.
// Number of channels to play sounds from. Either 1 or 2.
// 1 is mono sound, and 2 is stereo (most speakers are stereo).
op.ChannelCount = 2

Expand Down Expand Up @@ -146,7 +146,7 @@ func main() {
}

// Now that the sound finished playing, we can restart from the beginning (or go to any location in the sound) using seek
// newPos, err := player.(io.Seeker).Seek(0, io.SeekStart)
// newPos, err := player.Seek(0, io.SeekStart)
// if err != nil{
// panic("player.Seek failed: " + err.Error())
// }
Expand Down Expand Up @@ -209,13 +209,13 @@ Data is moved from io.Reader->internal buffer->audio device, and when the intern
is not guaranteed, so there might be a small delay. The amount of data in the buffer can be retrieved
using `Player.BufferedSize()`.

The size of the underlying buffer of a player can also be set by type-asserting the player object:
The size of the underlying buffer of a player can also be set by calling the player's `SetBufferSize` function:

```go
myPlayer.(oto.BufferSizeSetter).SetBufferSize(newBufferSize)
myPlayer.SetBufferSize(newBufferSize)
```

This works because players implement a `Player` interface and a `BufferSizeSetter` interface.
`NewPlayer` returns a `*oto.Player`, which has functions like `SetBufferSize` and `Seek`.

## Crosscompiling

Expand Down
8 changes: 4 additions & 4 deletions api_wasapi_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -486,18 +486,18 @@ type _IMMDeviceEnumerator_Vtbl struct {
UnregisterEndpointNotificationCallback uintptr
}

func (i *_IMMDeviceEnumerator) GetDefaultAudioEndPoint(dataFlow _EDataFlow, role _ERole) (*_IMMDevice, error) {
func (i *_IMMDeviceEnumerator) GetDefaultAudioEndpoint(dataFlow _EDataFlow, role _ERole) (*_IMMDevice, error) {
var endPoint *_IMMDevice
r, _, _ := syscall.Syscall6(i.vtbl.GetDefaultAudioEndpoint, 4, uintptr(unsafe.Pointer(i)),
uintptr(dataFlow), uintptr(role), uintptr(unsafe.Pointer(&endPoint)), 0, 0)
if uint32(r) != uint32(windows.S_OK) {
if isWin32Err(uint32(r)) {
return nil, fmt.Errorf("oto: IMMDeviceEnumerator::GetDefaultAudioEndPoint failed: %w", _E_NOTFOUND)
return nil, fmt.Errorf("oto: IMMDeviceEnumerator::GetDefaultAudioEndpoint failed: %w", _E_NOTFOUND)
}
if isRPCErr(uint32(r)) {
return nil, fmt.Errorf("oto: IMMDeviceEnumerator::GetDefaultAudioEndPoint failed: %w", _RPC_ERR(r))
return nil, fmt.Errorf("oto: IMMDeviceEnumerator::GetDefaultAudioEndpoint failed: %w", _RPC_ERR(r))
}
return nil, fmt.Errorf("oto: IMMDeviceEnumerator::GetDefaultAudioEndPoint failed: HRESULT(%d)", uint32(r))
return nil, fmt.Errorf("oto: IMMDeviceEnumerator::GetDefaultAudioEndpoint failed: HRESULT(%d)", uint32(r))
}
return endPoint, nil
}
Expand Down
2 changes: 1 addition & 1 deletion api_winmm_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func (m _MMRESULT) Error() string {
case _WAVERR_SYNC:
return "WAVERR_SYNC"
}
return fmt.Sprintf("MMRESULT (%d)", m)
return fmt.Sprintf("MMRESULT(%d)", m)
}

func waveOutOpen(f *_WAVEFORMATEX, callback uintptr) (uintptr, error) {
Expand Down
22 changes: 11 additions & 11 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,20 @@ type Context struct {
type Format int

const (
// FormatFloat32LE is the format of 32 bits floats little endian.
// FormatFloat32LE is the format of 32-bit floats in little endian.
FormatFloat32LE Format = iota

// FormatUnsignedInt8 is the format of 8 bits integers.
// FormatUnsignedInt8 is the format of 8-bit integers.
FormatUnsignedInt8

// FormatSignedInt16LE is the format of 16 bits integers little endian.
// FormatSignedInt16LE is the format of 16-bit integers in little endian.
FormatSignedInt16LE
)

// NewContextOptions represents options for NewContext.
type NewContextOptions struct {
// 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
// Typical values 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.
SampleRate int

Expand All @@ -70,9 +70,9 @@ type NewContextOptions struct {
// BufferSize specifies a buffer size in the underlying device.
//
// If 0 is specified, the driver's default buffer size is used.
// Set BufferSize to adjust the buffer size if you want to adjust latency or reduce noises.
// Too big buffer size can increase the latency time.
// On the other hand, too small buffer size can cause glitch noises due to buffer shortage.
// Set BufferSize to adjust the buffer size if you want to adjust latency or reduce noise.
// A buffer size that is too big increases the latency.
// On the other hand, a buffer size that is too small can cause glitch noises due to buffer shortage.
BufferSize time.Duration

// ApplicationName specifies the name of the client application.
Expand All @@ -82,7 +82,7 @@ type NewContextOptions struct {

// NewContext creates a new context with given options.
// A context creates and holds ready-to-use Player objects.
// NewContext returns a context, a channel that closes when initialization finishes, and an error if it exists.
// NewContext returns a context, a channel that closes when initialization finishes, and an error, if any.
// After the channel closes, call Context.Err to check whether initialization succeeded.
//
// Creating multiple contexts is NOT supported.
Expand All @@ -97,7 +97,7 @@ func NewContext(options *NewContextOptions) (*Context, chan struct{}, error) {

var bufferSizeInBytes int
if options.BufferSize != 0 {
// The underlying driver always uses 32bit floats.
// The underlying driver always uses 32-bit floats.
bytesPerSample := options.ChannelCount * 4
bytesPerSecond := options.SampleRate * bytesPerSample
bufferSizeInBytes = int(int64(options.BufferSize) * int64(bytesPerSecond) / int64(time.Second))
Expand Down Expand Up @@ -130,11 +130,11 @@ func NewContext(options *NewContextOptions) (*Context, chan struct{}, error) {
// Then, r's position and the current playing position don't necessarily match.
// If you want to seek the position of r, call the player's Seek function,
// which also clears the underlying buffer.
// If you want to stop using r e.g., you want to close r, call the player's PauseAndStopReading function.
// If you want to stop using r (e.g. you want to close r), call the player's PauseAndStopReading function.
//
// You cannot share r by multiple players.
//
// The returned player implements Player, BufferSizeSetter, and io.Seeker.
// The returned player is a *Player, which has functions like SetBufferSize and Seek.
// You can modify the buffer size of a player by the SetBufferSize function.
// 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.
Expand Down
4 changes: 2 additions & 2 deletions driver_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func newAudioQueue(sampleRate, channelCount int, oneBufferSizeInBytes int) (_Aud
0, //CFStringRef
0,
&audioQueue); osstatus != noErr {
return 0, nil, fmt.Errorf("oto: AudioQueueNewFormat with StreamFormat failed: %d", osstatus)
return 0, nil, fmt.Errorf("oto: AudioQueueNewOutput failed: %d", osstatus)
}

bufs := make([]_AudioQueueBufferRef, 0, bufferCount)
Expand Down Expand Up @@ -138,7 +138,7 @@ func newContext(sampleRate int, channelCount int, format mux.Format, bufferSizeI
// defaultOneBufferSizeInBytes is the default buffer size in bytes.
//
// 12288 seems necessary at least on iPod touch (7th) and MacBook Pro 2020.
// With 48000[Hz] stereo, the maximum delay is (12288*4[buffers] / 4 / 2)[samples] / 48000 [Hz] = 100[ms].
// With 48000[Hz] stereo, the maximum delay is (12288*4[buffers] / 4 / 2)[samples] / 48000 [Hz] = 128[ms].
// '4' is float32 size in bytes. '2' is a number of channels for stereo.
const defaultOneBufferSizeInBytes = 12288

Expand Down
12 changes: 5 additions & 7 deletions driver_wasapi_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,7 @@ type wasapiContext struct {
}

var (
errDeviceSwitched = errors.New("oto: device switched")
errFormatNotSupported = errors.New("oto: the specified format is not supported (there is the closest format instead)")
errDeviceSwitched = errors.New("oto: device switched")
)

const (
Expand Down Expand Up @@ -187,7 +186,7 @@ func (c *wasapiContext) isDeviceSwitched() (bool, error) {
var switched bool
var cerr error
c.comThread.Run(func() {
device, err := c.enumerator.GetDefaultAudioEndPoint(eRender, eConsole)
device, err := c.enumerator.GetDefaultAudioEndpoint(eRender, eConsole)
if err != nil {
cerr = err
return
Expand Down Expand Up @@ -303,7 +302,7 @@ func (c *wasapiContext) startOnCOMThread() (ferr error) {
}
c.enumerator = (*_IMMDeviceEnumerator)(e)

device, err := c.enumerator.GetDefaultAudioEndPoint(eRender, eConsole)
device, err := c.enumerator.GetDefaultAudioEndpoint(eRender, eConsole)
if err != nil {
if errors.Is(err, _E_NOTFOUND) {
return errDeviceNotFound
Expand Down Expand Up @@ -333,7 +332,7 @@ func (c *wasapiContext) startOnCOMThread() (ferr error) {
}

// Check the format is supported by WASAPI.
// Stereo with 48000 [Hz] is likely supported, but mono and/or other sample rates are unlikely supported.
// Stereo with 48000 [Hz] is likely supported, but mono and/or other sample rates are unlikely to be supported.
// Fallback to WinMM in this case anyway.
const bitsPerSample = 32
nBlockAlign := c.channelCount * bitsPerSample / 8
Expand Down Expand Up @@ -531,8 +530,7 @@ func (c *wasapiContext) Err() error {
// isWASAPIDeviceTransientError reports whether err from (re)starting the client
// indicates a temporarily unusable device rather than a permanent failure.
func isWASAPIDeviceTransientError(err error) bool {
return errors.Is(err, errFormatNotSupported) ||
errors.Is(err, errDeviceNotFound) ||
return errors.Is(err, errDeviceNotFound) ||
errors.Is(err, _AUDCLNT_E_DEVICE_INVALIDATED) ||
errors.Is(err, _AUDCLNT_E_RESOURCES_INVALIDATED) ||
errors.Is(err, _RPC_E_DISCONNECTED)
Expand Down
5 changes: 1 addition & 4 deletions driver_winmm_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ import (
"github.com/ebitengine/oto/v3/internal/mux"
)

// Avoid goroutines on Windows (hajimehoshi/ebiten#1768).
// Apparently, switching contexts might take longer than other platforms.

const defaultHeaderBufferSize = 4096

type header struct {
Expand Down Expand Up @@ -201,7 +198,7 @@ func (c *winmmContext) isHeaderAvailable() bool {
}

var waveOutOpenCallback = windows.NewCallback(func(hwo, uMsg, dwInstance, dwParam1, dwParam2 uintptr) uintptr {
// Queuing a header in this callback might not work especially when a headset is connected or disconnected.
// Queueing a header in this callback might not work especially when a headset is connected or disconnected.
// Just signal the condition variable and don't do other things.
const womDone = 0x3bd
if uMsg != womDone {
Expand Down
4 changes: 2 additions & 2 deletions example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (

var (
sampleRate = flag.Int("samplerate", 48000, "sample rate")
channelCount = flag.Int("channelcount", 2, "number of channel")
channelCount = flag.Int("channelcount", 2, "number of channels")
format = flag.String("format", "s16le", "source format (u8, s16le, or f32le)")
)

Expand Down Expand Up @@ -210,7 +210,7 @@ func run() error {

wg.Wait()

// Pin the players not to GC the players.
// Keep the players alive so that they are not garbage-collected.
runtime.KeepAlive(players)

return nil
Expand Down
6 changes: 3 additions & 3 deletions internal/mux/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
"time"
)

// Format must sync with oto's Format.
// Format must be kept in sync with oto's Format.
type Format int

const (
Expand Down Expand Up @@ -108,7 +108,7 @@ func (m *Mux) loop() {
}

// Sleeping is necessary especially on browsers.
// Sometimes a player continues to read 0 bytes from the source and this loop can be a busy loop in such case.
// Sometimes a player continues to read 0 bytes from the source and this loop can be a busy loop in such a case.
if allZero {
time.Sleep(time.Millisecond)
}
Expand Down Expand Up @@ -630,7 +630,7 @@ func (p *playerImpl) returnBufferToPool() {
// TODO: The term 'buffer' is confusing. Name each buffer with good terms.

// defaultBufferSize returns the default size of the buffer for the audio source.
// This buffer is used when unreading on pausing the player.
// The mux loop reads the source into this buffer, and ReadFloat32s consumes it.
func (m *Mux) defaultBufferSize() int {
bytesPerSample := m.channelCount * m.format.ByteLength()
s := m.sampleRate * bytesPerSample / 2 // 0.5[s]
Expand Down
6 changes: 3 additions & 3 deletions internal/mux/mux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ func TestPauseAndStopReadingKeepsOngoingReadResult(t *testing.T) {
}

// The player must be reusable after PauseAndStopReading.
// Consume the buffered data to make a room for a new read.
// Consume the buffered data to make room for a new read.
p.Play()
m.ReadFloat32s(make([]float32, 4096))
deadline := time.Now().Add(time.Second)
Expand Down Expand Up @@ -360,7 +360,7 @@ func TestClosingSourceAfterPauseAndStopReadingIsSafe(t *testing.T) {
_ = src.Close()
reads := src.reads.Load()

// Consume the mux's buffer to tempt it to read the source again.
// Consume the mux's buffer to prompt it to read the source again.
m.ReadFloat32s(make([]float32, 256))
time.Sleep(100 * time.Millisecond)

Expand Down Expand Up @@ -409,7 +409,7 @@ func TestSeekDiscardsOngoingReadResult(t *testing.T) {
// Wait until a read from the source is in flight.
<-src.began

// Pause not to resume playing after seeking.
// Pause so that playing does not resume after seeking.
p.Pause()

done := make(chan struct{})
Expand Down
13 changes: 5 additions & 8 deletions internal/oboe/binding_android.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@

namespace {

class Player;

// Status is the outcome of an operation. msg is null on success, and describes
// the failure otherwise. retryable, which is meaningful only for a failure, is
// false for a configuration error, which would fail the same way however many
Expand Down Expand Up @@ -65,9 +63,8 @@ Status StatusFromResult(oboe::Result result) {
}

// kStableRunDuration is how long a stream must keep playing for its start to
// count as a success. A stream that goes away sooner reached the device
// without being able to use it, so the delay before the next attempt keeps
// growing.
// count as a success. A stream that goes away sooner managed to start but not
// to keep playing, so the delay before the next attempt keeps growing.
constexpr std::chrono::seconds kStableRunDuration{3};

// StartRetryDelay returns how long to wait before the next start attempt after
Expand Down Expand Up @@ -98,7 +95,7 @@ enum class State {
kStopped,

// kStartDeferred means that a start attempt failed for a reason that can
// pass, and that LoopStartRetry attempts it again.
// be temporary, and that LoopStartRetry attempts it again.
kStartDeferred,

// kRunning means that the stream was started and has not been paused, closed
Expand All @@ -123,8 +120,8 @@ class Stream : public oboe::AudioStreamDataCallback,
public oboe::AudioStreamErrorCallback {
public:
// GetInstance returns the instance of Stream. Only one Stream object is used
// in one process. It is because multiple streams can be problematic in both
// AAudio and OpenSL (#1656, #1660).
// in one process, because multiple streams can be problematic in both AAudio
// and OpenSL (#1656, #1660).
static Stream &GetInstance();

const char *Play(int sample_rate, int channel_num, int buffer_size_in_bytes);
Expand Down
2 changes: 0 additions & 2 deletions internal/oboe/binding_android.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
extern "C" {
#endif

typedef uintptr_t PlayerID;

const char *oto_oboe_Play(int sample_rate, int channel_num,
int buffer_size_in_bytes);
const char *oto_oboe_Suspend();
Expand Down
6 changes: 3 additions & 3 deletions player.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ func (p *Player) Volume() float64 {
return p.player.Volume()
}

// SetVolume sets the current volume, which must be in the range of [0, math.MaxFloat32].
// SetVolume sets the current volume 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) {
p.player.SetVolume(volume)
}

// BufferedSize returns the byte size of the buffer data that is not sent to the audio hardware yet.
// BufferedSize returns the byte size of the buffered data that is not sent to the audio hardware yet.
func (p *Player) BufferedSize() int {
return p.player.BufferedSize()
}
Expand Down Expand Up @@ -109,7 +109,7 @@ func (p *Player) Seek(offset int64, whence int) (int64, error) {
//
// Close does nothing and always returns nil.
//
// Deprecated: as of v3.4. you don't have to call Close.
// Deprecated: as of v3.4, you don't have to call Close.
func (p *Player) Close() error {
// (*mux.Player).Close() is called by the finalizer. Let's rely on it.
return nil
Expand Down
Loading