diff --git a/audio.go b/audio.go index 93c96de..a2117a2 100644 --- a/audio.go +++ b/audio.go @@ -7,28 +7,30 @@ import ( "math" ) -// AudioBuffer is a buffer of stereo audio samples of variable length, each -// sample represented by a slice of [2]float32. [0] is left channel, [1] is -// right -type AudioBuffer [][2]float32 +type ( + // AudioBuffer is a buffer of stereo audio samples of variable length, each + // sample represented by a slice of [2]float32. [0] is left channel, [1] is + // right + AudioBuffer [][2]float32 -// AudioOutput 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 AudioOutput interface { - WriteAudio(buffer AudioBuffer) error - Close() error -} + // AudioOutput represents something where we can send audio e.g. audio output. + // WriteAudio should block if not ready to accept audio e.g. buffer full. + AudioOutput interface { + WriteAudio(buffer AudioBuffer) 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 AudioOutputs with Output(); each -// can be used to output separate sound & closed when done. -type AudioContext interface { - Output() AudioOutput - 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 AudioOutputs with Output(); each + // can be used to output separate sound & closed when done. + AudioContext interface { + Output() AudioOutput + Close() error + } +) // 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. diff --git a/instrument.go b/instrument.go deleted file mode 100644 index 2393bb6..0000000 --- a/instrument.go +++ /dev/null @@ -1,18 +0,0 @@ -package sointu - -// Instrument includes a list of units consisting of the instrument, and the number of polyphonic voices for this instrument -type Instrument struct { - Name string `yaml:",omitempty"` - Comment string `yaml:",omitempty"` - NumVoices int - 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 { - units[i] = u.Copy() - } - return Instrument{Name: instr.Name, Comment: instr.Comment, NumVoices: instr.NumVoices, Units: units} -} diff --git a/order.go b/order.go deleted file mode 100644 index 214a5a8..0000000 --- a/order.go +++ /dev/null @@ -1,23 +0,0 @@ -package sointu - -// Order is the pattern order for a track, in practice just a slice of integers, -// but provides convenience functions that return -1 values for indices out of -// bounds of the array, and functions to increase the size of the slice only by -// 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 - } - 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) - } - (*s)[index] = value -} diff --git a/patch.go b/patch.go index 714c0f8..c11c0b5 100644 --- a/patch.go +++ b/patch.go @@ -6,8 +6,252 @@ import ( "math" ) -// Patch is simply a list of instruments used in a song -type Patch []Instrument +type ( + // Patch is simply a list of instruments used in a song + Patch []Instrument + + // Instrument includes a list of units consisting of the instrument, and the number of polyphonic voices for this instrument + Instrument struct { + Name string `yaml:",omitempty"` + Comment string `yaml:",omitempty"` + NumVoices int + Units []Unit + } + + // Unit is e.g. a filter, oscillator, envelope and its parameters + Unit struct { + // 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 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"` + } + + // UnitParameter documents one parameter that an unit takes + UnitParameter struct { + Name string // thould be found with this name in the Unit.Parameters map + MinValue int // minimum value of the parameter, inclusive + MaxValue int // maximum value of the parameter, inclusive + CanSet bool // if this parameter can be set before hand i.e. through the gui + CanModulate bool // if this parameter can be modulated i.e. has a port number in "send" unit + } +) + +// UnitTypes documents all the available unit types and if they support stereo variant +// and what parameters they take. +var UnitTypes = map[string]([]UnitParameter){ + "add": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, + "addp": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, + "pop": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, + "loadnote": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, + "mul": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, + "mulp": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, + "push": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, + "xch": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, + "distort": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "drive", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, + "hold": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "holdfreq", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, + "crush": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "resolution", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, + "gain": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, + "invgain": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, + "filter": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "frequency", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "resonance", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "lowpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "bandpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "highpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "negbandpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "neghighpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, + "clip": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, + "pan": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "panning", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, + "delay": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "pregain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "dry", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "feedback", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "damp", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "notetracking", MinValue: 0, MaxValue: 2, CanSet: true, CanModulate: false}, + {Name: "delaytime", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}}, + "compressor": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "threshold", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "ratio", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, + "speed": []UnitParameter{}, + "out": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, + "outaux": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "outgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "auxgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, + "aux": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}}, + "send": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "amount", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "voice", MinValue: 0, MaxValue: 32, CanSet: true, CanModulate: false}, + {Name: "target", MinValue: 0, MaxValue: math.MaxInt32, CanSet: true, CanModulate: false}, + {Name: "port", MinValue: 0, MaxValue: 7, CanSet: true, CanModulate: false}, + {Name: "sendpop", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, + "envelope": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "decay", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "sustain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, + "noise": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, + "oscillator": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "transpose", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "detune", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "phase", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "color", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, + {Name: "frequency", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}, + {Name: "type", MinValue: int(Sine), MaxValue: int(Sample), CanSet: true, CanModulate: false}, + {Name: "lfo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "unison", MinValue: 0, MaxValue: 3, CanSet: true, CanModulate: false}, + {Name: "samplestart", MinValue: 0, MaxValue: 1720329, CanSet: true, CanModulate: false}, + {Name: "loopstart", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false}, + {Name: "looplength", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false}}, + "loadval": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, + "receive": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "left", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}, + {Name: "right", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}}, + "in": []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}}, + "sync": []UnitParameter{}, +} + +// 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 + Pulse = iota + Gate = iota + Sample = iota +) + +// 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() { + for name, unitType := range UnitTypes { + unitPorts := make([]string, 0) + for _, param := range unitType { + if param.CanModulate { + unitPorts = append(unitPorts, param.Name) + } + } + Ports[name] = unitPorts + } +} + +// Copy makes a deep copy of a unit. +func (u *Unit) Copy() Unit { + parameters := make(map[string]int) + for k, v := range u.Parameters { + parameters[k] = v + } + varArgs := make([]int, len(u.VarArgs)) + copy(varArgs, u.VarArgs) + 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": + return -1 - u.Parameters["stereo"] + case "envelope", "oscillator", "push", "noise", "receive", "loadnote", "loadval", "in", "compressor": + return 1 + u.Parameters["stereo"] + case "pan": + return 1 - u.Parameters["stereo"] + case "speed": + return -1 + case "send": + return (-1 - u.Parameters["stereo"]) * u.Parameters["sendpop"] + } + 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": + return 0 + case "mulp", "mul", "add", "addp", "xch": + return 2 * (1 + u.Parameters["stereo"]) + case "speed": + return 1 + } + return 1 + u.Parameters["stereo"] +} + +// 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 { + units[i] = u.Copy() + } + return Instrument{Name: instr.Name, Comment: instr.Comment, NumVoices: instr.NumVoices, Units: units} +} // Copy makes a deep copy of a Patch. func (p Patch) Copy() Patch { diff --git a/pattern.go b/pattern.go deleted file mode 100644 index 389bc55..0000000 --- a/pattern.go +++ /dev/null @@ -1,23 +0,0 @@ -package sointu - -// Pattern represents a single pattern of note, in practice just a slice of bytes, -// but provides convenience functions that return 1 values (hold) for indices out of -// bounds of the array, and functions to increase the size of the slice only by -// 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 - } - 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) - } - (*s)[index] = value -} diff --git a/score.go b/score.go deleted file mode 100644 index 245a794..0000000 --- a/score.go +++ /dev/null @@ -1,49 +0,0 @@ -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 // 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 { - tracks[i] = t.Copy() - } - 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 { - ret += t.NumVoices - } - 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] { - ret += t.NumVoices - } - 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 335c4e7..2479d3b 100644 --- a/song.go +++ b/song.go @@ -4,17 +4,157 @@ 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 - Score Score - Patch Patch +type ( + // 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. + Song struct { + BPM int + RowsPerBeat int + Score Score + Patch Patch + } + + // 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. + Score struct { + Tracks []Track + RowsPerPattern int // number of rows in each pattern + Length int // length of the song, in number of patterns + } + + // 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. + 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 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"` + } + + // Pattern represents a single pattern of note, in practice just a slice of + // bytes, but provides convenience functions that return 1 values (hold) for + // indices out of bounds of the array, and functions to increase the size of + // the slice only by necessary amount when a new item is added, filling the + // unused slots with 1s. + Pattern []byte + + // Order is the pattern order for a track, in practice just a slice of + // integers, but provides convenience functions that return -1 values for + // indices out of bounds of the array, and functions to increase the size of + // the slice only by necessary amount when a new item is added, filling the + // unused slots with -1s. + 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 + } + 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) + } + (*s)[index] = value +} + +// 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 + } + 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) + } + (*s)[index] = value +} + +// Copy makes a deep copy of a Track. +func (t *Track) Copy() Track { + order := make([]int, len(t.Order)) + copy(order, t.Order) + patterns := make([]Pattern, len(t.Patterns)) + for i, oldPat := range t.Patterns { + newPat := make(Pattern, len(oldPat)) + copy(newPat, oldPat) + patterns[i] = newPat + } + return Track{ + NumVoices: t.NumVoices, + Effect: t.Effect, + Order: order, + Patterns: 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 { + tracks[i] = t.Copy() + } + 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 { + ret += t.NumVoices + } + 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] { + ret += t.NumVoices + } + 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 } // Copy makes a deep copy of a Score. diff --git a/synth.go b/synth.go index 246e4eb..fde22b7 100644 --- a/synth.go +++ b/synth.go @@ -6,37 +6,39 @@ 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. Normally, 1 sample = 1 unit of time, but speed - // modulations may change this. It returns the number of samples filled (in - // stereo samples i.e. number of elements of AudioBuffer filled), the - // number of sync outputs written, the number of time steps time advanced, - // and a possible error. - Render(buffer AudioBuffer, maxtime int) (sample int, time int, err error) +type ( + // Synth represents a state of a synthesizer, compiled from a Patch. + 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. Normally, 1 sample = 1 unit of time, but speed + // modulations may change this. It returns the number of samples filled (in + // stereo samples i.e. number of elements of AudioBuffer filled), the + // number of sync outputs written, the number of time steps time advanced, + // and a possible error. + Render(buffer AudioBuffer, maxtime int) (sample 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, bpm int) 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, bpm int) error - // Trigger triggers a note for a given voice. Called between synth.Renders. - Trigger(voice int, note byte) + // 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) -} + // 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, bpm int) (Synth, error) -} + // SynthService compiles a given Patch into a Synth, throwing errors if the + // Patch is malformed. + SynthService interface { + Compile(patch Patch, bpm int) (Synth, error) + } +) // Render fills an stereo audio buffer using a Synth, disregarding all syncs and // time limits. diff --git a/track.go b/track.go deleted file mode 100644 index aacd009..0000000 --- a/track.go +++ /dev/null @@ -1,44 +0,0 @@ -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 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) - patterns := make([]Pattern, len(t.Patterns)) - for i, oldPat := range t.Patterns { - newPat := make(Pattern, len(oldPat)) - copy(newPat, oldPat) - patterns[i] = newPat - } - return Track{ - NumVoices: t.NumVoices, - Effect: t.Effect, - Order: order, - Patterns: patterns, - } -} diff --git a/unit.go b/unit.go deleted file mode 100644 index 9255283..0000000 --- a/unit.go +++ /dev/null @@ -1,87 +0,0 @@ -package sointu - -// Unit is e.g. a filter, oscillator, envelope and its parameters -type Unit struct { - // 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 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 - Pulse = iota - Gate = iota - 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 { - parameters[k] = v - } - varArgs := make([]int, len(u.VarArgs)) - copy(varArgs, u.VarArgs) - 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": - return -1 - u.Parameters["stereo"] - case "envelope", "oscillator", "push", "noise", "receive", "loadnote", "loadval", "in", "compressor": - return 1 + u.Parameters["stereo"] - case "pan": - return 1 - u.Parameters["stereo"] - case "speed": - return -1 - case "send": - return (-1 - u.Parameters["stereo"]) * u.Parameters["sendpop"] - } - 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": - return 0 - case "mulp", "mul", "add", "addp", "xch": - return 2 * (1 + u.Parameters["stereo"]) - case "speed": - return 1 - } - return 1 + u.Parameters["stereo"] -} diff --git a/unittype.go b/unittype.go deleted file mode 100644 index a8b329a..0000000 --- a/unittype.go +++ /dev/null @@ -1,144 +0,0 @@ -package sointu - -import ( - "math" -) - -// UnitParameter documents one parameter that an unit takes -type UnitParameter struct { - Name string // thould be found with this name in the Unit.Parameters map - MinValue int // minimum value of the parameter, inclusive - MaxValue int // maximum value of the parameter, inclusive - CanSet bool // if this parameter can be set before hand i.e. through the gui - CanModulate bool // if this parameter can be modulated i.e. has a port number in "send" unit -} - -// UnitTypes documents all the available unit types and if they support stereo variant -// and what parameters they take. -var UnitTypes = map[string]([]UnitParameter){ - "add": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, - "addp": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, - "pop": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, - "loadnote": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, - "mul": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, - "mulp": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, - "push": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, - "xch": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, - "distort": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "drive", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, - "hold": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "holdfreq", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, - "crush": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "resolution", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, - "gain": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, - "invgain": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, - "filter": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "frequency", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "resonance", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "lowpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "bandpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "highpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "negbandpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "neghighpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, - "clip": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, - "pan": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "panning", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, - "delay": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "pregain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "dry", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "feedback", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "damp", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "notetracking", MinValue: 0, MaxValue: 2, CanSet: true, CanModulate: false}, - {Name: "delaytime", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}}, - "compressor": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "threshold", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "ratio", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, - "speed": []UnitParameter{}, - "out": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, - "outaux": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "outgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "auxgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, - "aux": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}}, - "send": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "amount", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "voice", MinValue: 0, MaxValue: 32, CanSet: true, CanModulate: false}, - {Name: "target", MinValue: 0, MaxValue: math.MaxInt32, CanSet: true, CanModulate: false}, - {Name: "port", MinValue: 0, MaxValue: 7, CanSet: true, CanModulate: false}, - {Name: "sendpop", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, - "envelope": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "decay", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "sustain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, - "noise": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, - "oscillator": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "transpose", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "detune", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "phase", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "color", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "frequency", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}, - {Name: "type", MinValue: int(Sine), MaxValue: int(Sample), CanSet: true, CanModulate: false}, - {Name: "lfo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "unison", MinValue: 0, MaxValue: 3, CanSet: true, CanModulate: false}, - {Name: "samplestart", MinValue: 0, MaxValue: 1720329, CanSet: true, CanModulate: false}, - {Name: "loopstart", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false}, - {Name: "looplength", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false}}, - "loadval": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}, - "receive": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "left", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}, - {Name: "right", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}}, - "in": []UnitParameter{ - {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}}, - "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() { - for name, unitType := range UnitTypes { - unitPorts := make([]string, 0) - for _, param := range unitType { - if param.CanModulate { - unitPorts = append(unitPorts, param.Name) - } - } - Ports[name] = unitPorts - } -}