edau

package module
v0.0.0-...-6fc968c Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2023 License: MIT Imports: 12 Imported by: 4

README

edau

Go Reference

WARNING: discussions on Ebitengine's discord led me to publish this earlier than intended. Don't expect much yet.

edau stands for Ebitengine Digital Audio Utils. As the name implies, it's a collection of types, interfaces and utilities to work with audio on Ebitengine, the game engine written in Golang by Hajime Hoshi.

This is currently a loosely scoped, exploratory project for personal use mostly. If any of its parts end up evolving into something bigger, I may separate them into a more focused repository.

Current utilities include:

  • SpeedShifter, which can be used to play audio at different speeds, with a few interpolation functions available.
  • A tight Looper that unlike Ebitengine's InfiniteLoop doesn't do any fading between end and start loop points nor requires any padding after the end loop point. See also apps/loop_finder for a loop point finder utility.
  • A handful of low-level helper functions for audio streams and samples.

General advice for audio in Ebitengine

  • Use the .ogg format for all your audio. The implementation used for Ebitengine is around twice as performant as .mp3, it doesn't have random padding at the start and end of the audio like mp3, and given the same file size quality is better than mp3 too.
  • If you have only a few SFX's, load the audio as bytes from an Ebitengine audio stream with io.ReadAll and use audio.NewPlayerFromBytes to play them. Keeping multiple players reading from underlying compressed streams can be quite taxing, especially on WASM.
  • Consider using audio#Player.SetBufferSize if you need responsive effects or lower latency audio. For desktops, you can typically go slightly below time.Millisecond*20. For browsers I don't recommend going below time.Millisecond*50.

Pending work

Future plans include:

  • Transitions for effects and the speed shifter. This is currently an open problem that I'm still trying to figure out, as applying transitions as early as possible leads to inconsistent transition timing, and trying to apply a consistent latency seems tricky (if possible at all).
  • Better support for .ogg and a mono player, mostly intended for SFX's and saving some space.
  • Effects and effect chains, with programmable and automatable parameters and a simple descriptive UI model to be able to integrate plugins into a DAW-like program.
  • A few audio synths. Maybe even some light format to create SFX's programmatically.
  • Maybe more sophisticated loopers for more complex song structures with repetitions and multiple sections, though this can already be built on top of the existing looper.

See also...

  • SolarLune's resound library, which already includes multiple effects like volume, delay, low-pass filtering, panning, distortion, etc.
  • Transition, my incomplete 2023 Ebitengine game jam entry. The code on src/audio implements float32 processing chains for truly smooth transitions (both fades and crossfades) and mixing multiple audio sources.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrAudioContextUninitialized = errors.New("Ebitengine's audio context not initialized")

Returned by functions that require Ebitengine's audio.NewContext to have already been created, typically so the sample rate can be directly obtained from it with audio.CurrentContext().SampleRate().

Functions

func GetSampleAsF64

func GetSampleAsF64(buffer []byte) (float64, float64)

Reads the first 4 bytes from the given slice and converts them from L16, 2 channel, little-endian format to 2 channel float64 values in the [-1, 1] range. Will panic if len(buffer) < 4.

func GetSampleAsI16

func GetSampleAsI16(buffer []byte) (int16, int16)

Reads the first 4 bytes from the given slice (the first sample) and returns the left and right channel values. Results fall in the [-32768, 32767] range. Will panic if len(buffer) < 4.

func InterpHermite4Pt3Ord

func InterpHermite4Pt3Ord(samples []float64, x float64) float64

4-point, 3rd-order Hermite interpolation. Samples are considered to start at zero. x is the position at which we want to interpolate. For best results, x should be between 1.0 and 2.0.

len(samples) must be at least 4.

func InterpHermite6Pt3Ord

func InterpHermite6Pt3Ord(samples []float64, x float64) float64

6-point, 3rd-order Hermite interpolation. Samples are considered to start at zero. x is the position at which we want to interpolate. For best results, x should be between 2.0 and 3.0.

len(samples) must be at least 6.

func InterpLagrange4Pt3Ord

func InterpLagrange4Pt3Ord(samples []float64, x float64) float64

4-point, 3rd-order Lagrange interpolation. Samples are considered to start at zero. x is the position at which we want to interpolate. For best results, x should be between 1.0 and 2.0.

len(samples) must be at least 4.

func InterpLagrange6Pt5Ord

func InterpLagrange6Pt5Ord(samples []float64, x float64) float64

6-point, 5th-order Lagrange interpolation. Samples are considered to start at zero. x is the position at which we want to interpolate. For best results, x should be between 2.0 and 3.0.

len(samples) must be at least 6.

func InterpLagrangeN

func InterpLagrangeN(samples []float64, targetPosition float64) float64

Lagrange interpolation for N samples. Samples are considered to start at zero, and targetPosition must be an index near the middle of samples' slice length for best results. This function is slow; for faster interpolation use one of the fixed-N functions instead.

func NormalizeF64

func NormalizeF64(value float64) float64

Normalize a float64 value from [-32768, 32767] to [-1, 1].

func StoreF64SampleAsL16

func StoreF64SampleAsL16(buffer []byte, left, right float64)

Stores the given unnormalized ([-32768, 32767]) values as a L16, 2 channel, little-endian sample right at the start of the given slice. Values out of range will be clipped. Will panic if len(buffer) < 4.

func StoreL16Sample

func StoreL16Sample(buffer []byte, left int16, right int16)

Stores the given values as a L16, 2 channel, little-endian sample right at the start of the given slice. Will panic if len(buffer) < 4.

func StoreNormF64SampleAsL16

func StoreNormF64SampleAsL16(buffer []byte, left, right float64)

Stores the given normalized ([-1, 1]) values as a L16, 2 channel, little-endian sample right at the start of the given slice. Values out of range will be clipped. Will panic if len(buffer) < 4.

Types

type InterpolatorFunc

type InterpolatorFunc func([]float64, float64) float64

An interpolator function receives a slice of values, the position at which we want to interpolate, and returns the interpolated value.

Positions to interpolate must always be within 0 <= position <= len(samples) - 1. Responsible callers will try to keep it as close to (len(samples) - 1)/2 as possible.

Interpolation functions are mainly used for resampling processes. See InterpLagrangeN, InterpLagrange4Pt3Ord, InterpLagrange6Pt5Ord, InterpHermite4Pt3Ord, InterpHermite6Pt3Ord.

type Looper

type Looper struct {
	// contains filtered or unexported fields
}

A tight audio looper. Unlike Ebitengine's infinite looper, this looper doesn't require padding after the end point because it doesn't perform any blending during the transition. Additionally, the start and end points can be changed at any time with Looper.AdjustLoop.

func NewLooper

func NewLooper(stream io.ReadSeeker, loopStart int64, loopEnd int64) *Looper

Creates a new tight Looper.

The stream must be a L16 little-endian stream with two channels (Ebitengine's default audio format). loopStart and loopEnd must be multiples of 4, and loopStart must be strictly smaller than loopEnd. This method will panic if any of those are not respected.

The loopEnd point is not itself included in the loop. For example, to play a full audio stream in a loop, you would use NewLooper(stream, 0, stream.Length()).

If you need help to determine the loop start and end points, see apps/loop_finder.

func (*Looper) AdjustLoop

func (self *Looper) AdjustLoop(loopStart, loopEnd int64)

Sets new values for the loop starting and ending points. The values are []byte indices. Therefore, since Ebitengine audio samples require 4 bytes each, the passed start and end points must also be multiples of 4.

If the new loop end is set before the current playback position, the loop will continue playing until the previously configured end point before the new loop comes into effect.

func (*Looper) GetLoopEnd

func (self *Looper) GetLoopEnd() int64

Returns the currently configured loop ending point (in bytes, not samples).

func (*Looper) GetLoopPoints

func (self *Looper) GetLoopPoints() (int64, int64)

Like Looper.GetLoopStart and Looper.GetLoopEnd, but both at once.

func (*Looper) GetLoopStart

func (self *Looper) GetLoopStart() int64

Returns the currently configured loop starting point (in bytes, not samples).

func (*Looper) GetPosition

func (self *Looper) GetPosition() int64

Returns the current playback position. The value will always be multiple of 4, as in Ebitengine each sample is composed of 4 bytes.

func (*Looper) Length

func (self *Looper) Length() int64

Returns the underlying stream's length. The underlying stream must have a Length() int64 method or be a bytes.Reader. This method will panic otherwise.

func (*Looper) Read

func (self *Looper) Read(buffer []byte) (int, error)

Implements io.Reader.

func (*Looper) Seek

func (self *Looper) Seek(offset int64, whence int) (int64, error)

Seek seeks directly on the underlying stream. It is the caller's responsibility to make sure the seek falls inside the current loop (if that's desired).

type SpeedShifter

type SpeedShifter struct {
	// contains filtered or unexported fields
}

A SpeedShifter wraps an audio stream and allows playing it at a different speed than the original by resampling in real-time.

Valid speed shifters can only be created through NewDefaultSpeedShifter or NewSpeedShifter.

func NewDefaultSpeedShifter

func NewDefaultSpeedShifter(source io.Reader) *SpeedShifter

Creates a default SpeedShifter, which uses a 6-point Hermite interpolator for the resampling. For custom interpolators, see NewSpeedShifter instead.

The initial playback speed is 1.0.

func NewSpeedShifter

func NewSpeedShifter(source io.Reader, speed float64, windowSize int, interpolator InterpolatorFunc) *SpeedShifter

Creates a new SpeedShifter with a custom speed, window size and interpolator. If you do not want to worry about interpolators and resampling, see NewDefaultSpeedShifter instead.

The interpolator's windowSize must be multiple of 2.

func (*SpeedShifter) Read

func (self *SpeedShifter) Read(buffer []byte) (int, error)

Implements io.Reader. This function will always try to fill the buffer as much as possible, even if this requires multiple reads on the underlying stream.

The returned read length will always also be multiple of 4, aligning to Ebitengine's sample size.

func (*SpeedShifter) Seek

func (self *SpeedShifter) Seek(offset int64, whence int) (int64, error)

Implements io.Seeker, with the limitation that io.SeekCurrent seeks are not supported (unless the seek has an offset of 0, which is sometimes used to get the current playback position).

You may use Seek to rewind and start playing after stoping, but not to loop or do seamless seeking with the resampled stream itself. Seamless seeking could only be done correctly if notifying the seek in advance of the interpolation window. That's not something anyone sane wants to figure out, so seeking will seek on the underlying buffer but reset the internal interpolation window of the speed shifter.

This method panics if the underlying source doesn't implement io.Seeker.

func (*SpeedShifter) SetSpeed

func (self *SpeedShifter) SetSpeed(speed float64)

Sets the playback speed. Notice that the Ebitengine's player buffer size can cause the change to take a while to become audible. See Ebitengine's Player.SetBufferSize if you want to reduce latency. 20 milliseconds are reasonable for most desktop environments, while browsers often have trouble when pushed below 50 milliseconds. It also depends a lot on how much processing and effects you are adding to the audio.

func (*SpeedShifter) Speed

func (self *SpeedShifter) Speed() float64

Returns the currently configured playback speed.

type StdAudioStream

type StdAudioStream interface {
	io.ReadSeeker
	Length() int64
}

A common interface that all Ebitengine audio streams conform to.

func LoadAudioFileAsStream

func LoadAudioFileAsStream(filename string) (StdAudioStream, error)

Loads an .ogg, .mp3 or .wav file as a StdAudioStream. Additionally, the returned interface also implements io.Closer, which can be used to close the file associated to the stream, e.g.:

err := audioStream.(io.Closer).Close()

The sample rate used is taken from Ebitengine's audio.CurrentContext(). If no audio context has been initialized, ErrAudioContextUninitialized will be returned.

Directories

Path Synopsis
apps
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL