resound

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Nov 9, 2024 License: MIT Imports: 4 Imported by: 1

README ¶

Resound 🔉

Resound is a library for applying sound effects when playing sounds back with Ebitengine. Resound was made primarily for game development, as you might expect.

Why did you make this?

C'mon man, you already know what it is

The general advantages of using Resound is two-fold. Firstly, it allows you to easily add non-standard effects (like low-pass filtering, distortion, or panning) to sound or music playback. Secondly, it allows you to easily apply these effects across multiple groups of sounds, like a DSP. The general idea of using buses / channels is, again, taken from how Godot does things, along with other DAWs and music creation tools, like Renoise, Reason, and SunVox.

How do I use it?

There's a couple of different ways to use resound.

  1. Effects themselves satisfy io.ReadSeeker, like an ordinary audio stream from Ebitengine, so you can create a player from them using Ebiten's built-in context.NewPlayer() functionality. However, it's easier to create a resound.Player, as it has support for applying Effects directly onto the player:

// Let's assume our sound is read in or embedded as a series of bytes.
var soundBytes []byte

const sampleRate = 44100

func main() {

    // So first, we'll create an audio context, decode some bytes into a stream,
    // create a loop, etc. 
    context := audio.NewContext(sampleRate)

    reader := bytes.NewReader(soundBytes)

    stream, err := vorbis.DecodeWithSampleRate(sampleRate, reader)

    if err != nil {
        panic(err)
    }

    loop := audio.NewInfiniteLoop(stream, stream.Length())

    delay := effects.NewDelay().SetWait(0.1).SetStrength(0.2)

    player := resound.NewPlayer(loop)

    // Effects in Resound wrap streams (including other effects), so you could just use them
    // like you would an ordinary audio stream in Ebitengine.

    // Now we create a new player of the original loop + delay:
    player, err := context.NewPlayer(delay)

    // (Note that if you're going to change effect parameters in real time, you may want to
    // lower the internal buffer size for Players using (*audio.Player).SetBufferSize())

    if err != nil {
        panic(err)
    }

    // Play it, and you're good to go.
    player.Play()


}

  1. You can also apply effects to a Player.

  2. Apply effects to a DSP Channel, and then play sounds through there. This allows you to automatically play sounds back using various shared properties (a shared volume, shared panning, shared filter, etc).


// Let's, again, assume our sound is read in or embedded as a series of bytes.
var soundBytes []byte

// Here, though, we'll be creating a DSPChannel.
var dsp *resound.DSPChannel

const sampleRate = 44100

func main() {

    // So first, we'll create an audio context, decode some bytes into a stream,
    // create a loop, etc. 
    audio.NewContext(sampleRate)

    reader := bytes.NewReader(soundBytes)

    stream, err := vorbis.DecodeWithSampleRate(sampleRate, reader)

    if err != nil {
        panic(err)
    }

    loop := audio.NewInfiniteLoop(stream, stream.Length())

    // But here, we create a DSPChannel. A DSPChannel represents a group of effects
    // that sound streams play through. When playing a stream through a DSPChannel,
    // the stream takes on the effects applied to the DSPChannel. We don't have to
    // pass a stream to effects when used with a DSPChannel, because every stream
    // played through the channel takes the effect.
    dsp = resound.NewDSPChannel()
    dsp.AddEffect("delay", effects.NewDelay(nil).SetWait(0.1).SetStrength(0.25))
    dsp.AddEffect("distort", effects.NewDistort(nil).SetStrength(0.25))
    dsp.AddEffect("volume", effects.NewVolume(nil).SetStrength(0.25))

    // Now we create a new player from the DSP channel. This will return a
    // *resound.ChannelPlayback object, which works similarly to an audio.Player
    // (in fact, it embeds the *audio.Player).
    player := dsp.CreatePlayer(loop)

    // Play it, and you're good to go, again - this time, it will run its playback
    // through the effect stack in the DSPChannel, in this case Delay > Distort > Volume.
    player.Play()

}

To-do

  • Global Stop - Tracking playing sounds to globally stop all sounds that are playing back
  • DSPChannel Stop - ^, but for a DSP channel
  • Volume normalization - done through the AudioProperties struct.
  • Beat / rhythm analysis?
  • Replace all usage of "strength" with "wet/dry".
Effects
  • Volume
  • Pan
  • Delay
  • Distortion
  • Low-pass Filter
  • Bitcrush (?)
  • High-pass Filter
  • Reverb
  • Mix / Fade (between two streams, or between a stream and silence, and over a customizeable time) - Fading is now partially implemented, but not mixing
  • Loop (like, looping a signal after so much time has passed or the signal ends)
  • Pitch shifting
  • Playback speed adjustment
  • 3D Sound (quick and easy panning and volume adjustment based on distance from listener to source)
Generators
  • Silence

  • Static

    ... And whatever else may be necessary.

Known Issues

  • Currently, effects directly apply on top of streams, which means that any effects that could make streams longer (like reverbs or delays) will get cut off if the source stream ends.
  • All effect parameters are ordinary values (floats, bools, etc), but since they're accessible through user-facing functions as well as used within the effect's Read() function, this creates a race condition, particularly for fading a Volume effect. This should probably be properly synchronized in some way.

Documentation ¶

Index ¶

Constants ¶

This section is empty.

Variables ¶

This section is empty.

Functions ¶

This section is empty.

Types ¶

type AnalysisResult ¶

type AnalysisResult struct {
	Normalization float64
}

AnalysisResult is an object that contains the results of an analysis performed on a stream.

type AudioBuffer ¶

type AudioBuffer []byte

AudioBuffer wraps a []byte of audio data and provides handy functions to get and set values for a specific position in the buffer.

func (AudioBuffer) Get ¶

func (ab AudioBuffer) Get(i int) (l, r float64)

Get returns the values for the left and right audio channels at the specified stream sample index. The values returned for the left and right audio channels range from 0 to 1.

func (AudioBuffer) Len ¶

func (ab AudioBuffer) Len() int

func (AudioBuffer) Set ¶

func (ab AudioBuffer) Set(i int, l, r float64)

Set sets the left and right audio channel values at the specified stream sample index. The values should range from 0 to 1.

func (AudioBuffer) String ¶

func (ab AudioBuffer) String() string

type AudioProperties ¶

type AudioProperties map[any]*AudioProperty

func NewAudioProperties ¶

func NewAudioProperties() AudioProperties

func (AudioProperties) Get ¶

func (ap AudioProperties) Get(id any) *AudioProperty

Get gets the audio property associated with some identifier. This could be, for example, the original filepath of the audio stream.

type AudioProperty ¶

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

AudioProperty is an object that allows associating an AnalysisResult for a specific stream with a name for that stream.

func (*AudioProperty) Analyze ¶

func (ap *AudioProperty) Analyze(stream io.ReadSeeker, scanCount int64) (AnalysisResult, error)

Analyze analyzes the provided audio stream, returning an AnalysisResult object. The stream is the audio stream to be used for scanning, and the scanCount is the number of times the function should scan various parts of the audio stream. The higher the scan count, the more accurate the results should be, but the longer the scan would take. A scanCount of 16 means it samples the stream 16 times evenly throughout the file. If a scanCount of 0 or less is provided, it will default to 64.

func (*AudioProperty) ResetAnalyzation ¶

func (ap *AudioProperty) ResetAnalyzation()

type DSPChannel ¶

type DSPChannel struct {
	Active      bool
	Effects     map[any]IEffect
	EffectOrder []IEffect
	// contains filtered or unexported fields
}

DSPChannel represents an audio channel that can have various effects applied to it. Any Players that have a DSPChannel set will take on the effects applied to the channel as well.

func NewDSPChannel ¶

func NewDSPChannel() *DSPChannel

NewDSPChannel returns a new DSPChannel.

func (*DSPChannel) AddEffect ¶ added in v0.3.0

func (d *DSPChannel) AddEffect(id any, effect IEffect) *DSPChannel

AddEffect adds the specified Effect to the DSPChannel under the given identification. Note that effects added to DSPChannels don't need to specify source streams, as the DSPChannel automatically handles this.

func (*DSPChannel) Close ¶ added in v0.3.0

func (d *DSPChannel) Close()

Close closes the DSP channel. When closed, any players that play on the channel do not play and automatically close their sources. Closing the channel can be used to stop any sounds that might be playing back on the DSPChannel.

type IEffect ¶

type IEffect interface {
	io.ReadSeeker
	ApplyEffect(data []byte, bytesRead int) // This function is called when sound data goes through an effect. The effect should modify the data byte buffer.
}

IEffect indicates an effect that implements io.ReadSeeker and generally takes effect on an existing audio stream. It represents the result of applying an effect to an audio stream, and is playable in its own right.

type Player ¶ added in v0.3.0

type Player struct {
	*audio.Player
	DSPChannel *DSPChannel
	Source     io.ReadSeeker

	EffectOrder []IEffect
	Effects     map[any]IEffect
}

Player handles playback of audio and effects. Player embeds audio.Player and so has all of the functions and abilities of the default audio.Player while also applying effects either played from its source, through the Player's Effects, or through the Player's DSPChannel.

func NewPlayer ¶ added in v0.3.0

func NewPlayer(sourceStream io.ReadSeeker) (*Player, error)

NewPlayer creates a new Player to playback an io.ReadSeeker-fulfilling audio stream.

func NewPlayerFromPlayer ¶ added in v0.3.0

func NewPlayerFromPlayer(player *audio.Player) *Player

NewPlayerFromPlayer creates a new resound.Player from an existing *audio.Player.

func (*Player) AddEffect ¶ added in v0.3.0

func (p *Player) AddEffect(id any, effect IEffect) *Player

AddEffect adds the specified Effect to the Player, with the given ID.

func (*Player) CopyProperties ¶ added in v0.3.0

func (p *Player) CopyProperties(other *Player) *Player

CopyProperties copies the properties (effects, current DSP Channel, etc) from one resound.Player to the other. Note that this won't duplicate the current state of playback of the internal audio stream.

func (*Player) Effect ¶ added in v0.3.0

func (p *Player) Effect(id any) IEffect

Effect returns the effect associated with the given id. If an effect with the provided ID doesn't exist, this function will return nil.

func (*Player) Read ¶ added in v0.3.0

func (p *Player) Read(bytes []byte) (n int, err error)

func (*Player) Seek ¶ added in v0.3.0

func (p *Player) Seek(offset int64, whence int) (int64, error)

func (*Player) SetDSPChannel ¶ added in v0.3.0

func (p *Player) SetDSPChannel(c *DSPChannel) *Player

SetDSPChannel sets the DSPChannel to be used for playing audio back through the Player.

Directories ¶

Path Synopsis
examples
dsp

Jump to

Keyboard shortcuts

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