xm

package module
v0.0.0-...-966acb9 Latest Latest
Warning

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

Go to latest
Published: Feb 28, 2024 License: MIT Imports: 8 Imported by: 7

README

xm

Build Status PkgGoDev

This package is intended to be used in game development in Go with Ebitengine.

If you just need to parse an XM file, you can use the xm/xmfile package without importing the xm package itself.

The xm package provides an XM music stream that produces 16-bit signed PCM LE data. This process can be describes as:

  1. Read and decode the XM file (xmfile package)
  2. Convert XM file data into something optimized for playing
  3. Create a player object that can go through this data and produce PCM chunks

This package implements some of the common XM effects. Feel free to submit a PR to fill the feature gap.

Why would you even need an XM player in your game? The answer is simple: size. This is very important in web exports of your game. An average OGG file can have a size of 6-8mb while the same song in XM can fit in ~300kb or even less.

Installation

go get github.com/quasilyte/xm

Quick Start

  1. Create a parser to decode XM files into Go objects.
// import "github.com/quasilyte/xm/xmfile"
// See ParserConfig docs to learn the options available.
xmParser := xmfile.NewParser(xmfile.ParserConfig{})
  1. Decode the XM files that you want to work with.
// xmModule can be manipulated as needed, it's just data after all.
// You can add some effects to the module, or mute some instruments, etc.
//
// There is also a Parse method that uses an io.Reader instead of []byte.
xmData, _ := os.ReadFile("path/to/music.xm")
xmModule, err := xmParser.ParseFromBytes(xmData)
  1. Compile an XM module into a playable stream.
// import "github.com/quasilyte/xm"
// You can re-load a module into a stream by using LoadModule again.
// See LoadModuleConfig docs to learn the options available.
xmStream := xm.NewStream()
err := xmStream.LoadModule(xmModule, xm.LoadModuleConfig{})
  1. Use some audio driver to play the PCM data.
// This example uses Ebitengine audio.
// This library produces 16-bit signed PCM LE data.
sampleRate := 44100
audioContext := audio.NewContext(sampleRate)
player, err := audioContext.NewPlayer(xmStream)

// Now player object can be used to play the XM track.

There is an XM event listener API available too.

You don't have to use Ebitengine, but this library was created with Ebitengine in mind.

See cmd/ebitengine-example for a full example.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type LoadModuleConfig

type LoadModuleConfig struct {
	// LinearInterpolation enables sub-samples that will make some music sound smoother.
	// On average, this option will make loaded track to require ~x2 memory.
	//
	// The best way to figure out whether you need it or not is to listen to the results.
	// Most XM players you can find have linear interpolation (lerp) enabled by default.
	//
	// A zero value means "no interpolation".
	//
	// This should not be confused with volume ramping.
	// The volume ramping is always enabled and can't be turned off.
	LinearInterpolation bool

	// BPM sets the playback speed.
	// Higher BPM will make the music play faster.
	//
	// A zero value will use the XM module default BPM value.
	// If that value is zero as well, a value of 120 will be used.
	BPM uint

	// Tempo (called "Spd" in MilkyTracker) specifies the number of ticks per pattern row.
	// Perhaps a bit counter-intuitively, higher values make
	// the song play slower as there are more resolution steps inside a
	// single pattern row.
	//
	// A zero value will use the XM module default Tempo value.
	// If that value is zero as well, a value of 6 will be used.
	// (6 is a default value in MilkyTracker.)
	Tempo uint

	// The sound device sample rate.
	// If you're using Ebitengine, it's the same value that
	// was used to create an audio context.
	// The most common value is 44100.
	//
	// A zero value will assume a sample rate of 44100.
	//
	// Note: only two values are supported right now, 44100 and 0.
	// Therefore, you can only play XM tracks at sample rate of 44100.
	// This limitation can go away later.
	SampleRate uint
}

LoadModuleConfig configures the XM module loading.

These settings can't be changed after a module is loaded.

Some extra configurations are available via Stream methods:

  • Stream.SetVolume()
  • Stream.SetLooping()

These extra configuration methods can be used even after a module is loaded.

type Stream

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

Stream wraps the compiled XM module, making it possible to Read() its PCM bytes.

The Read() method produces 16-bit little endian PCM bytes; this is what ebiten/audio package extects. Use Stream as an io.Reader argument for audio.NewPlayer().

func NewStream

func NewStream() *Stream

NewPlayer allocates a player that can load and play XM tracks. Use LoadModule method to finish player initialization.

func (*Stream) GetInfo

func (s *Stream) GetInfo() StreamInfo

GetInfo returns stream-related info. See StreamInfo for more details.

func (*Stream) LoadModule

func (s *Stream) LoadModule(m *xmfile.Module, config LoadModuleConfig) error

LoadModule assigns a new XM module to this stream.

Loading a module involves its compilation which is a slow process. You want to load modules as rarely as possible (preferably exactly once) and then play them via streams without ever releasing the memory.

func (*Stream) Read

func (s *Stream) Read(b []byte) (int, error)

Read puts next PCM bytes into provided slice.

The slice is expected to fit at least a single tick. With BPM=120, Tempo=10 and SampleRate=44100 a single tick would require 882*bytesPerSample*numChannels = 2208 bytes. Note that this library only supports stereo output (numChannels=2) and it produces 16-bit (2 bytes per sample) LE PCM data. If you need to have precise info, use Stream.GetInfo() method.

If there is a tail in b that was not written to due to the lack of space for a whole tick, n<len(b) will be returned. It doesn't make send to pass a slice that is smaller than a single tick chunk (2k+ bytes), but it makes sense to pass a bigger slice as this method will try to fit as many ticks as possible.

When stream has no bytes to produce, io.EOF error is returned.

func (*Stream) Rewind

func (s *Stream) Rewind()

Rewind prepares the stream to play the module right from the start. Doing rewind is relatively cheap.

func (*Stream) Seek

func (s *Stream) Seek(offset int64, whence int) (int64, error)

Seek partially implements io.Seeker.

You can use it for two things:

  1. (0, SeekStart) for rewind
  2. (0, SeekCurrent) to get the byte pos inside the stream

func (*Stream) SetEventHandler

func (s *Stream) SetEventHandler(f func(e StreamEvent))

SetEventHandler installs an event listener to the stream.

f is called on every stream event.

Events are produced when the XM track is being played. Therefore, calling Read() may produce multiple events.

Experimental: the events handling API may change significantly in the future.

func (*Stream) SetLooping

func (s *Stream) SetLooping(loop bool)

SetLooping enables a simple looping from the beginning of the stream. When looping is enables, Read will never return EOF.

Note that some XM tracks include the trailing jump/pattern break effect that will make it loop in a more beautiful way. Use this looping flag only if XM track does not have one. You may need to perform some XM editing if there is a jump, but you still want to use this basic looping scheme.

Note: prefer this option to the InfiniteLoop provided by Ebitengine audio. This native way of looping is ~free while InfiniteLoop has some overhead.

func (*Stream) SetVolume

func (s *Stream) SetVolume(v float64)

SetVolume adjusts the global volume scaling for the stream. The default value is 0.8; a value of 0 disables the sound. The value is clamped in [0, 1].

type StreamEvent

type StreamEvent struct {
	Kind StreamEventKind

	// Channel is an event channel ID.
	// They may not match the exact XM module channel IDs,
	// but different channel IDs are guaranteed to have unique IDs.
	//
	// Some events may be channel-independent.
	Channel int

	// Time represents the playback offset in seconds.
	// Time=2.5 means that this event happened somewhere around 2.5 seconds.
	Time float64
	// contains filtered or unexported fields
}

StreamEvent holds a single Stream event data. This object is an argument to the Stream.SetEventHandler function.

To handle the event correctly, you must first check its kind. For an event of kind EventNote there is a NoteEventData method that will return the associated data. For EventSync there is a SyncEventData.

Every event has a Time value. This is a moment when this event happened in relation to the XM track start (in seconds). The user application needs to calculate the time deltas on its own and handle these events in the right moment.

Experimental: the events handling API may change significantly in the future.

func (StreamEvent) NoteEventData

func (e StreamEvent) NoteEventData() (note, instrument int, vol float32)

NoteEventData returns the event data if e.Kind=EventSync. The return values are: note, instrument (id), volume. If there is no instrument, -1 is returned.

func (StreamEvent) SyncEventData

func (e StreamEvent) SyncEventData() (t float64)

SyncEventData returns the event data if e.Kind=EventNote. The return values are: a time to synchronize to.

type StreamEventKind

type StreamEventKind int

StreamEventKind is an event tag that should be used to differentiate between different event types. See StreamEvent docs for more info.

Experimental: the events handling API may change significantly in the future.

const (
	// EventUnknown is a sentinel value.
	// You should never receive an event of this kind.
	EventUnknown StreamEventKind = iota

	// EventNote is emitted every time a channel starts to play some note.
	// It can be triggered even of a "ghost note", so it's up to the application
	// to decide whether they need to handle that note or not.
	//
	// Use StreamEvent.NoteEventData to get the event data.
	//
	// Experimental: the events handling API may change significantly in the future.
	EventNote

	// EventSync tells the application to update their time counter to the specified value.
	//
	// As any other event, the sync event has a Time field that you should use as a
	// description of when the counter should be updated.
	// Therefore, a sync event with Time=2.0 and data argument of 2.5 should
	// force the application to set its time counter to 2.5, but only if
	// it already reached a time counter value of 2.0.
	//
	// Use StreamEvent.EventSyncData to get the event data.
	//
	// Experimental: the events handling API may change significantly in the future.
	EventSync
)

type StreamInfo

type StreamInfo struct {
	// BytesPerTick tell how much bytes this stream needs to fit a single XM tick.
	// This value is important, since any slice smaller than this will give no effect
	// for Read() function. Any greater values will work OK for it.
	BytesPerTick uint

	// MemoryUsage approximates the compiled XM module size in bytes.
	// This can be important if you want to analyze linear interpolation (sub-samples)
	// effect on your modules.
	MemoryUsage uint
}

StreamInfo contains a compiled XM module stream information like bytes per tick, etc.

Directories

Path Synopsis
cmd
internal

Jump to

Keyboard shortcuts

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