diff --git a/audio.go b/audio.go index f26076c..1a61c53 100644 --- a/audio.go +++ b/audio.go @@ -1,10 +1,18 @@ package sointu +// AudioSink represents something where we can send audio e.g. audio output. +// WriteAudio should block if not ready to accept audio e.g. buffer full. type AudioSink interface { WriteAudio(buffer []float32) error Close() error } +// AudioContext represents the low-level audio drivers. There should be at most +// one AudioContext at a time. The interface is implemented at least by +// oto.OtoContext, but in future we could also mock it. +// +// AudioContext is used to create one or more AudioSinks with Output(); each can +// be used to output separate sound & closed when done. type AudioContext interface { Output() AudioSink Close() error diff --git a/audioexport.go b/audioexport.go index fa6cedb..d5166dc 100644 --- a/audioexport.go +++ b/audioexport.go @@ -7,6 +7,11 @@ import ( "math" ) +// Wav converts a stereo signal of 32-bit floats (L R L R..., length should be +// divisible by 2) into a valid WAV-file, returned as a []byte array. +// +// If pcm16 is set to true, the samples in the WAV-file will be 16-bit signed +// integers; otherwise the samples will be 32-bit floats func Wav(buffer []float32, pcm16 bool) ([]byte, error) { buf := new(bytes.Buffer) wavHeader(len(buffer), pcm16, buf) @@ -17,6 +22,11 @@ func Wav(buffer []float32, pcm16 bool) ([]byte, error) { return buf.Bytes(), nil } +// Raw converts a stereo signal of 32-bit floats (L R L R..., length should be +// divisible by 2) into a raw audio file, returned as a []byte array. +// +// If pcm16 is set to true, the samples will be 16-bit signed integers; +// otherwise the samples will be 32-bit floats func Raw(buffer []float32, pcm16 bool) ([]byte, error) { buf := new(bytes.Buffer) err := rawToBuffer(buffer, pcm16, buf) diff --git a/instrument.go b/instrument.go index 2e867ed..2393bb6 100644 --- a/instrument.go +++ b/instrument.go @@ -8,6 +8,7 @@ type Instrument struct { Units []Unit } +// Copy makes a deep copy of an Instrument func (instr *Instrument) Copy() Instrument { units := make([]Unit, len(instr.Units)) for i, u := range instr.Units { diff --git a/order.go b/order.go index 8abaab2..214a5a8 100644 --- a/order.go +++ b/order.go @@ -6,6 +6,7 @@ package sointu // necessary amount when a new item is added, filling the unused slots with -1s. type Order []int +// Get returns the value at index; or -1 is the index is out of range func (s Order) Get(index int) int { if index < 0 || index >= len(s) { return -1 @@ -13,6 +14,7 @@ func (s Order) Get(index int) int { return s[index] } +// Set sets the value at index; appending -1s until the slice is long enough. func (s *Order) Set(index, value int) { for len(*s) <= index { *s = append(*s, -1) diff --git a/patch.go b/patch.go index 5fd14ff..04c9b53 100644 --- a/patch.go +++ b/patch.go @@ -9,6 +9,7 @@ import ( // Patch is simply a list of instruments used in a song type Patch []Instrument +// Copy makes a deep copy of a Patch. func (p Patch) Copy() Patch { instruments := make([]Instrument, len(p)) for i, instr := range p { @@ -17,6 +18,8 @@ func (p Patch) Copy() Patch { return instruments } +// NumVoices returns the total number of voices used in the patch; summing the +// voices of every instrument func (p Patch) NumVoices() int { ret := 0 for _, i := range p { @@ -25,6 +28,8 @@ func (p Patch) NumVoices() int { return ret } +// NumDelayLines return the total number of delay lines used in the patch; +// summing the number of delay lines of every delay unit in every instrument func (p Patch) NumDelayLines() int { total := 0 for _, instr := range p { @@ -37,6 +42,8 @@ func (p Patch) NumDelayLines() int { return total } +// NumSyns return the total number of sync outputs used in the patch; summing +// the number of sync outputs of every sync unit in every instrument func (p Patch) NumSyncs() int { total := 0 for _, instr := range p { @@ -49,6 +56,11 @@ func (p Patch) NumSyncs() int { return total } +// FirstVoiceForInstrument returns the index of the first voice of given +// instrument. For example, if the Patch has three instruments (0, 1 and 2), +// with 1, 3, 2 voices, respectively, then FirstVoiceForInstrument(0) returns 0, +// FirstVoiceForInstrument(1) returns 1 and FirstVoiceForInstrument(2) returns +// 4. Essentially computes just the cumulative sum. func (p Patch) FirstVoiceForInstrument(instrIndex int) int { ret := 0 for _, t := range p[:instrIndex] { @@ -57,6 +69,10 @@ func (p Patch) FirstVoiceForInstrument(instrIndex int) int { return ret } +// InstrumentForVoice returns the instrument number for the given voice index. +// For example, if the Patch has three instruments (0, 1 and 2), with 1, 3, 2 +// voices, respectively, then InstrumentForVoice(0) returns 0, +// InstrumentForVoice(1) returns 1 and InstrumentForVoice(3) returns 1. func (p Patch) InstrumentForVoice(voice int) (int, error) { if voice < 0 { return 0, errors.New("voice cannot be negative") @@ -70,6 +86,11 @@ func (p Patch) InstrumentForVoice(voice int) (int, error) { return 0, errors.New("voice number is beyond the total voices of an instrument") } +// FindSendTarget searches the instrument number and unit index for a unit with +// the given id. Two units should never have the same id, but if they do, then +// the first match is returned. Id 0 is interpreted as "no id", thus searching +// for id 0 returns an error. Error is also returned if the searched id is not +// found. func (p Patch) FindSendTarget(id int) (int, int, error) { if id == 0 { return 0, 0, errors.New("send targets unit id 0") @@ -84,6 +105,8 @@ func (p Patch) FindSendTarget(id int) (int, int, error) { return 0, 0, fmt.Errorf("send targets an unit with id %v, could not find a unit with such an ID in the patch", id) } +// ParamHintString returns a human readable string representing the current +// value of a given unit parameter. func (p Patch) ParamHintString(instrIndex, unitIndex int, param string) string { if instrIndex < 0 || instrIndex >= len(p) { return "" diff --git a/pattern.go b/pattern.go index 80cdc8f..389bc55 100644 --- a/pattern.go +++ b/pattern.go @@ -6,6 +6,7 @@ package sointu // necessary amount when a new item is added, filling the unused slots with 1s. type Pattern []byte +// Get returns the value at index; or 1 is the index is out of range func (s Pattern) Get(index int) byte { if index < 0 || index >= len(s) { return 1 @@ -13,6 +14,7 @@ func (s Pattern) Get(index int) byte { return s[index] } +// Set sets the value at index; appending 1s until the slice is long enough. func (s *Pattern) Set(index int, value byte) { for len(*s) <= index { *s = append(*s, 1) diff --git a/score.go b/score.go index d7ec303..245a794 100644 --- a/score.go +++ b/score.go @@ -1,11 +1,16 @@ package sointu +// Score represents the arrangement of notes in a song; just a list of tracks +// and RowsPerPattern and Length (in patterns) to know the desired length of a +// song in rows. If any of the tracks is too short, all the notes outside the +// range should be just considered as holding the last note. type Score struct { Tracks []Track - RowsPerPattern int + RowsPerPattern int // number of rows in each pattern Length int // length of the song, in number of patterns } +// Copy makes a deep copy of a Score. func (l Score) Copy() Score { tracks := make([]Track, len(l.Tracks)) for i, t := range l.Tracks { @@ -14,6 +19,8 @@ func (l Score) Copy() Score { return Score{Tracks: tracks, RowsPerPattern: l.RowsPerPattern, Length: l.Length} } +// NumVoices returns the total number of voices used in the Score; summing the +// voices of every track func (l Score) NumVoices() int { ret := 0 for _, t := range l.Tracks { @@ -22,6 +29,11 @@ func (l Score) NumVoices() int { return ret } +// FirstVoiceForTrack returns the index of the first voice of given track. For +// example, if the Score has three tracks (0, 1 and 2), with 1, 3, 2 voices, +// respectively, then FirstVoiceForTrack(0) returns 0, FirstVoiceForTrack(1) +// returns 1 and FirstVoiceForTrack(2) returns 4. Essentially computes just the +// cumulative sum. func (l Score) FirstVoiceForTrack(track int) int { ret := 0 for _, t := range l.Tracks[:track] { @@ -30,6 +42,8 @@ func (l Score) FirstVoiceForTrack(track int) int { return ret } +// LengthInRows returns just RowsPerPattern * Length, as the length is the +// length in the number of patterns. func (l Score) LengthInRows() int { return l.RowsPerPattern * l.Length } diff --git a/song.go b/song.go index 215db61..335c4e7 100644 --- a/song.go +++ b/song.go @@ -4,6 +4,12 @@ import ( "errors" ) +// Song includes a Score(the arrangement of notes in the song in one or more +// tracks) and a Patch (the list of one or more instruments). Additionally, BPM +// and RowsPerBeat fields set how fast the song should be played. Currently, BPM +// is an integer as it offers already quite much granularity for controlling the +// playback speed, but this could be changed to a floating point in future if +// finer adjustments are necessary. type Song struct { BPM int RowsPerBeat int @@ -11,16 +17,20 @@ type Song struct { Patch Patch } +// Copy makes a deep copy of a Score. func (s *Song) Copy() Song { return Song{BPM: s.BPM, RowsPerBeat: s.RowsPerBeat, Score: s.Score.Copy(), Patch: s.Patch.Copy()} } +// Assuming 44100 Hz playback speed, return the number of samples of each row of +// the song. func (s *Song) SamplesPerRow() int { return 44100 * 60 / (s.BPM * s.RowsPerBeat) } -// TBD: Where shall we put methods that work on pure domain types and have no dependencies -// e.g. Validate here +// Validate checks if the Song looks like a valid song: BPM > 0, one or more +// tracks, score uses less than or equal number of voices than patch. Not used +// much so we could probably get rid of this function. func (s *Song) Validate() error { if s.BPM < 1 { return errors.New("BPM should be > 0") diff --git a/synth.go b/synth.go index 40236cf..8a88ad4 100644 --- a/synth.go +++ b/synth.go @@ -6,17 +6,41 @@ import ( "math" ) +// Synth represents a state of a synthesizer, compiled from a Patch. type Synth interface { + // Render tries to fill a stereo signal buffer with sound from the + // synthesizer, until either the buffer is full or a given number of + // timesteps is advanced. In the process, it also fills the syncbuffer with + // the values output by sync units. Normally, 1 sample = 1 unit of time, but + // speed modulations may change this. It returns the number of samples + // filled (! in stereo samples, so the buffer will have 2 * sample floats), + // the number of sync outputs written, the number of time steps time + // advanced, and a possible error. Render(buffer []float32, syncBuffer []float32, maxtime int) (sample int, syncs int, time int, err error) + + // Update recompiles a patch, but should maintain as much as possible of its + // state as reasonable. For example, filters should keep their state and + // delaylines should keep their content. Every change in the Patch triggers + // an Update and if the Patch would be started fresh every time, it would + // lead to very choppy audio. Update(patch Patch) error + + // Trigger triggers a note for a given voice. Called between synth.Renders. Trigger(voice int, note byte) + + // Release releases the currently playing note for a given voice. Called + // between synth.Renders. Release(voice int) } +// SynthService compiles a given Patch into a Synth, throwing errors if the +// Patch is malformed. type SynthService interface { Compile(patch Patch) (Synth, error) } +// Render fills an stereo audio buffer using a Synth, disregarding all syncs and +// time limits. func Render(synth Synth, buffer []float32) error { s, _, _, err := synth.Render(buffer, nil, math.MaxInt32) if err != nil { @@ -28,6 +52,10 @@ func Render(synth Synth, buffer []float32) error { return nil } +// Play plays the Song using a given Synth, returning the stereo audio buffer +// and the sync buffer as a result (and possible errors). This is a bit +// illogical as the Song contains already the Patch; this could be probably +// refactored to just accept a SynthService and a Song. func Play(synth Synth, song Song) ([]float32, []float32, error) { err := song.Validate() if err != nil { diff --git a/track.go b/track.go index b7ff523..aacd009 100644 --- a/track.go +++ b/track.go @@ -1,12 +1,31 @@ package sointu +// Track represents the patterns and orderlist for each track. Note that each +// track has its own patterns, so one track cannot use another tracks patterns. +// This makes the data more intuitive to humans, as the reusing of patterns over +// tracks is a rather rare occurence. However, the compiler will put all the +// patterns in one global table (identical patterns only appearing once), to +// optimize the runtime code. type Track struct { + // NumVoices is the number of voices this track triggers, cycling through + // the voices. When this track triggers a new voice, the previous should be + // released. NumVoices int - Effect bool `yaml:",omitempty"` - Order Order `yaml:",flow"` - Patterns []Pattern `yaml:",flow"` + + // Effect hints the GUI if this is more of an effect track than a note + // track: if true, e.g. the GUI can display the values as hexadecimals + // instead of note values. + Effect bool `yaml:",omitempty"` + + // Order is a list telling which pattern comes in which order in the song in + // this track. + Order Order `yaml:",flow"` + + // Patterns is a list of Patterns for this track. + Patterns []Pattern `yaml:",flow"` } +// Copy makes a deep copy of a Track. func (t *Track) Copy() Track { order := make([]int, len(t.Order)) copy(order, t.Order) diff --git a/unit.go b/unit.go index fc3a09f..9255283 100644 --- a/unit.go +++ b/unit.go @@ -2,12 +2,34 @@ package sointu // Unit is e.g. a filter, oscillator, envelope and its parameters type Unit struct { - Type string `yaml:",omitempty"` - ID int `yaml:",omitempty"` + // Type is the type of the unit, e.g. "add","oscillator" or "envelope". + // Always in lowercase. "" type should be ignored, no invalid types should + // be used. + Type string `yaml:",omitempty"` + + // ID should be a unique ID for this unit, used by SEND units to target + // specific units. ID = 0 means that no ID has been given to a unit and thus + // cannot be targeted by SENDs. When possible, units that are not targeted + // by any SENDs should be cleaned from having IDs, e.g. to keep the exported + // data clean. + ID int `yaml:",omitempty"` + + // Parameters is a map[string]int of parameters of a unit. For example, for + // an oscillator, unit.Type == "oscillator" and unit.Parameters["attack"] + // could be 64. Most parameters are either limites to 0 and 1 (e.g. stereo + // parameters) or between 0 and 128, inclusive. Parameters map[string]int `yaml:",flow"` - VarArgs []int `yaml:",flow,omitempty"` + + // VarArgs is a list containing the variable number arguments that some + // units require, most notably the DELAY units. For example, for a DELAY + // unit, VarArgs is the delaytimes, in samples, of the different delaylines + // in the unit. + VarArgs []int `yaml:",flow,omitempty"` } +// When unit.Type = "oscillator", its unit.Parameter["Type"] tells the type of +// the oscillator. There is five different oscillator types, so these consts +// just enumerate them. const ( Sine = iota Trisaw = iota @@ -16,6 +38,7 @@ const ( Sample = iota ) +// Copy makes a deep copy of a unit. func (u *Unit) Copy() Unit { parameters := make(map[string]int) for k, v := range u.Parameters { @@ -26,6 +49,12 @@ func (u *Unit) Copy() Unit { return Unit{Type: u.Type, Parameters: parameters, VarArgs: varArgs, ID: u.ID} } +// StackChange returns how this unit will affect the signal stack. "pop" and +// "addp" and such will consume the topmost signal, and thus return -1 (or -2, +// if the unit is a stereo unit). On the other hand, "oscillator" and "envelope" +// will produce a signal, and thus return 1 (or 2, if the unit is a stereo +// unit). Effects that just change the topmost signal and will not change the +// number of signals on the stack and thus return 0. func (u *Unit) StackChange() int { switch u.Type { case "addp", "mulp", "pop", "out", "outaux", "aux": @@ -42,6 +71,9 @@ func (u *Unit) StackChange() int { return 0 } +// StackNeed returns the number of signals that should be on the stack before +// this unit is executed. Used to prevent stack underflow. Units producing +// signals do not care what is on the stack before and will return 0. func (u *Unit) StackNeed() int { switch u.Type { case "", "envelope", "oscillator", "noise", "receive", "loadnote", "loadval", "in": diff --git a/unittype.go b/unittype.go index 424f11a..08a8361 100644 --- a/unittype.go +++ b/unittype.go @@ -134,6 +134,10 @@ var UnitTypes = map[string]([]UnitParameter){ "sync": []UnitParameter{}, } +// Ports is static map allowing quickly finding the parameters of a unit that +// can be modulated. This is populated based on the UnitTypes list during +// init(). Thus, should be immutable, but Go not supporting that, then this will +// have to suffice: DO NOT EVER CHANGE THIS MAP. var Ports = make(map[string]([]string)) func init() {