diff --git a/envelopes.go b/envelopes.go index 4a0fa49..bbceed9 100644 --- a/envelopes.go +++ b/envelopes.go @@ -11,77 +11,136 @@ package main // They should be normalized to have values between 0 and 1 import ( + "fmt" "math" ) +// PlanckTime is the shortest possible interval of time +const ( + PlanckTime Seconds = 1e-20 +) + var ( sqrt2π float64 = math.Sqrt(τ) // simple optimization ) // Enveloper modulates a signal type Enveloper interface { - Amplitude(t Seconds) float64 // Call this from outside - OnePeriodAmplitude(t Seconds) float64 // Implement this - Length() Seconds // Return overall length of the envelope + Amplitude(t Seconds) Volts // Call this from outside + Length() Seconds // Return overall length of the envelope } // Envelope is the 'base' type for envelopes type Envelope struct { - λ Seconds // Period of repeat - Repeats bool // Does it repeat or is it single shot? - Len Seconds // The overall length of the envelope (might be several λ long) + T0 Seconds // *Global* time when the envelope starts + λ Seconds // Period of repeat + // Repeats bool // Does it repeat or is it single shot? + Len Seconds // The overall length of the envelope (might be several λ long) } -// NewEnvelope is self-evident -func NewEnvelope(λ Seconds, reps bool, l Seconds) *Envelope { - return &Envelope{λ: λ, Repeats: reps, Len: l} +// NewEnvelope is +func NewEnvelope(t0 Seconds, λ Seconds, reps bool, l Seconds) *Envelope { + return &Envelope{T0: t0, λ: λ, Len: l} } -// Gaussian is an envelope with height 1 at μ and RMS width of σ -// f(x) = exp(-(x-μ)^2/2σ^2) μ and σ should be specified in seconds -type Gaussian struct { +// ADSR is a classic ADSR envelope +type ADSR struct { Envelope - μ, σ Seconds - σσ Seconds + Ta Seconds // Attack time (0->1) + Td Seconds // Decay (1->Ls) + Ls Volts // Sustain level (Ls) + TsMax Seconds // Maximum sustain time + TsMin Seconds // Minimum sustain time + Tr Seconds // Release time (Ls->0) + sStart LocalSeconds // when did sustain begin? + knowRelease bool // Is release time known? + releaseAt LocalSeconds // when released + tsActual Seconds // Actual sustain time (derived from keyup etc.) } -// NewGaussian makes a new one -func NewGaussian(λ Seconds, reps bool, l Seconds, newμ, newσ Seconds) *Gaussian { - e := NewEnvelope(λ, reps, l) - g := &Gaussian{Envelope: *e, μ: newμ, σ: newσ, σσ: newσ * newσ} - return g +// NewADSR makes a new one, pass ts as zero if not known at creation +func NewADSR(t0 Seconds, reps bool, ta Seconds, td Seconds, ls Volts, tsmax Seconds, tsmin Seconds, ts Seconds) *ADSR { + if tsmax < PlanckTime { + tsmax = MaxNoteLen + } + adsr := ADSR{Ta: ta, Td: td, Ls: ls, TsMax: tsmax, TsMin: tsmin, tsActual: ts, sStart: LocalSeconds(ta + td)} + adsr.Envelope = Envelope{T0: t0} + if ts > PlanckTime { // we know when release happens + adsr.releaseAt = LocalSeconds(ts + ta + td) + adsr.knowRelease = true + } + return &adsr } -// OnePeriodAmplitude fulfils Envelope interface -func (g *Gaussian) OnePeriodAmplitude(x Seconds) float64 { +// Release triggers the release at the given global time. Strangeness will result if called after release should have started +func (adsr ADSR) Release(t Seconds) { + tLocal := LocalSeconds(t - adsr.T0) + ts := tLocal - adsr.sStart // length of the sustain + tsact := max(min(Seconds(ts), adsr.TsMin), adsr.TsMax) // clip into valid range + adsr.releaseAt = LocalSeconds(tsact) + adsr.knowRelease = true +} + +// Amplitude is +func (adsr ADSR) Amplitude(t Seconds) Volts { + localT := LocalSeconds(t - adsr.T0) + var a Volts + switch { + case localT < LocalSeconds(adsr.Ta): + a = Volts(localT / LocalSeconds(adsr.Ta)) + case localT < adsr.sStart: + a = 1 - Volts((localT-LocalSeconds(adsr.Ta))*(1-LocalSeconds(adsr.Ls))/LocalSeconds(adsr.Td)) + case localT < adsr.releaseAt: + a = adsr.Ls + case localT > adsr.releaseAt: + if !adsr.knowRelease { + fmt.Printf("ADSR envelope error: in release stage of unreleased envelope") + } + default: + fmt.Printf("ADSR envelope error: in unknown portion of envelope") + } + return a +} + +func max(a, b Seconds) Seconds { + if a > b { + return a + } + return b +} - xu := float64(x - g.μ) - return math.Exp(-xu * xu / float64(2*g.σσ)) +func min(a, b Seconds) Seconds { + if a < b { + return a + } + return b } // Triangle a simple /\ with period λ type Triangle struct { - *Envelope + Envelope } -// NewTriangle is self-evident -func NewTriangle(λ Seconds, reps bool, l Seconds) *Triangle { - e := NewEnvelope(λ, reps, l) - return &Triangle{Envelope: e} +// NewTriangle makes one +func NewTriangle(t Seconds, λ Seconds, reps bool, l Seconds) *Triangle { + tr := Triangle{} + tr.Envelope = Envelope{T0: t, λ: λ, Len: l} + // fmt.Printf("New triangle at %f\n", t) + return &tr } // Amplitude is -func (tr Triangle) Amplitude(t Seconds) float64 { - return tr.OnePeriodAmplitude(Seconds(math.Mod(float64(t), float64(tr.λ)))) +func (tr Triangle) Amplitude(t Seconds) Volts { + localT := t - tr.T0 + return tr.onePeriodAmplitude(LocalSeconds(math.Mod(float64(localT), float64(tr.λ)))) } -// OnePeriodAmplitude is -func (tr Triangle) OnePeriodAmplitude(t Seconds) float64 { - // s := math.Mod(float64(t), float64(tr.λ)) +// onePeriodAmplitude is +func (tr Triangle) onePeriodAmplitude(t LocalSeconds) Volts { if Seconds(t) < (tr.λ)/2 { - return float64(t * 2 / tr.λ) + return Volts(Seconds(t) * 2 / tr.λ) } - return float64(2 - (t * 2 / tr.λ)) + return Volts(2 - (Seconds(t) * 2 / tr.λ)) } // Length is @@ -89,50 +148,23 @@ func (tr Triangle) Length() Seconds { return tr.Len } -// RepeatAmplitude is -// func (tr *Triangle) RepeatAmplitude(t Seconds) float64 { -// s := math.Mod(float64(t), float64(tr.λ)) -// if Seconds(s) < (tr.λ)/2 { -// return s * 2 / float64(tr.λ) -// } -// return 2 - (s * 2 / float64(tr.λ)) -// } -// SetPeriodandLength fulfils Enveloper interface -// func (tr Triangle) SetPeriodandLength(λ Seconds, length Seconds) { -// tr.Envelope.SetPeriodandLength(λ, length) -// } - -// RepeatAmplitude generates a sequence of gaussian envelopes of period λ seconds -// func (g *Gaussian) OneShotAmplitude(t Seconds) float64 { - -// s := math.Mod(float64(t), float64(g.λ)) -// tu := s - float64(g.μ) -// return math.Exp(-tu * tu / float64(2*g.σσ)) - -// } -// OnePeriodAmplitude is the function you should implement for new types of envelope -// func (e *Envelope) OnePeriodAmplitude(t Seconds) float64 { -// fmt.Printf("Warning: Amplitude called on base Envelope class, probably an error\n") -// return 1 -// } - -// Amplitude is the function to call from outide the class to get either the single shot envelope or the repeated one -// If you implemented OneShotAmplitude, this should give you repeats for free -// func (e *Envelope) Amplitude(t Seconds) float64 { -// if e.Repeats { -// return e.OnePeriodAmplitude(Seconds(math.Mod(float64(t), float64(e.λ)))) -// } -// return e.OnePeriodAmplitude(t) -// } +// Gaussian is an envelope with height 1 at μ and RMS width of σ +// f(x) = exp(-(x-μ)^2/2σ^2) μ and σ should be specified in seconds +type Gaussian struct { + Envelope + μ, σ Seconds + σσ Seconds +} -// Length is -// func (e *Envelope) Length() Seconds { -// return e.Len -// } - -// SetPeriodandLength fulfils Enveloper interface -// func (e *Envelope) SetPeriodandLength(λ Seconds, length Seconds) { -// e.λ = λ -// e.Len = length -// } -// SetPeriodandLength(λ Seconds, length Seconds) // Set both the period (λ) field and length +// NewGaussian makes a new one +func NewGaussian(globalT Seconds, λ Seconds, reps bool, l Seconds, newμ, newσ Seconds) *Gaussian { + e := NewEnvelope(globalT, λ, reps, l) + g := &Gaussian{Envelope: *e, μ: newμ, σ: newσ, σσ: newσ * newσ} + return g +} + +// OnePeriodAmplitude fulfils Envelope interface +func (g *Gaussian) onePeriodAmplitude(localT Seconds) Volts { + xu := float64(localT - g.μ) + return Volts(math.Exp(-xu * xu / float64(2*g.σσ))) +} diff --git a/go.mod b/go.mod index 782480b..6d1c777 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.15 require ( github.com/faiface/beep v1.0.2 github.com/veandco/go-sdl2 v0.4.5 + gitlab.com/gomidi/midi v1.21.0 golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061 // indirect ) diff --git a/line.jpg b/line.jpg index 250ba23..95471bb 100644 Binary files a/line.jpg and b/line.jpg differ diff --git a/line.png b/line.png new file mode 100644 index 0000000..cda0310 Binary files /dev/null and b/line.png differ diff --git a/line2.png b/line2.png new file mode 100644 index 0000000..2c7339e Binary files /dev/null and b/line2.png differ diff --git a/main.go b/main.go index 7ddbbe2..02a8618 100644 --- a/main.go +++ b/main.go @@ -2,12 +2,8 @@ package main import ( "fmt" - "image" "image/color" - "image/draw" - "image/jpeg" "math" - "os" "strings" "time" @@ -22,24 +18,28 @@ const ( τ = 2 * math.Pi ) -// Seconds are quantities of time +// Seconds are quantities of time, either they are global absolute times (e.g. relative to the start of a synth) +// or they are relocatable intervals without a specific beginning type Seconds float64 +// LocalSeconds are times relative to the start of an osciallator/filter/envelope etc. +type LocalSeconds Seconds + // Hertz is a fequency (1/Seconds) type Hertz Seconds // Angle is a portion of a wave, typically a phase, also radians type Angle float64 +// Volts is the notional amplitude of a signal, with +-1 volt being the maximum that can be sent to an output device +// and +1 being the maximum value of a filter etc. +type Volts float64 + const ( fontPath = "Go-Mono.ttf" fontSize = 24 ) -var recordingL = make([]float64, 0, 1000000) -var recordingR = make([]float64, 0, 1000000) -var recordIt bool - var ( imCyan = color.RGBA{100, 200, 200, 0xff} imRed = color.RGBA{255, 0, 0, 0xff} @@ -61,7 +61,7 @@ func main() { SR := Hertz(44100) mySyn := NewSynth(time.Now(), 330, SR) sr := beep.SampleRate(SR) - speaker.Init(sr, sr.N(time.Second/5)) + speaker.Init(sr, sr.N(time.Second/200)) speaker.Play(mySyn) if err := sdl.Init(sdl.INIT_VIDEO); err != nil { @@ -105,7 +105,9 @@ func main() { lowRowOut := "ABCDEFG" running := true - recordIt = true + mySyn.recordIt = true + +RunLoop: for running { for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { switch t := event.(type) { @@ -125,10 +127,13 @@ func main() { switch t.Type { case 768: // typeName = "KeyDown" + //mySyn.recordIt = true freq := MiddleCfreq c := fmt.Sprintf("%c", t.Keysym.Sym) if c == "q" { running = false + mySyn.Graphout() + break RunLoop // bail right away } p := strings.Index(lowRowIn, c) ns := "?" @@ -136,11 +141,13 @@ func main() { ns = lowRowOut[p:p+1] + "4" freq = GetFreq(ns) } - fmt.Printf("Adding sound %s at %f from key %s (index %d)\n", ns, freq, c, p) - myOsc := NewSine(freq) - myEnv := NewTriangle(1, false, 1) - myNote := &Note{BaseFreq: freq, Env: myEnv, Osc: myOsc} - mySyn.AddSound(myNote, mySyn.Now()) + globalT := mySyn.Now() + fmt.Printf("t: %7.4f | Adding sound %s at %f from key %s (index %d)\n", globalT, ns, freq, c, p) + // fmt.Printf("Keystroke at %f\n", globalT) + myOsc := NewSine(globalT, freq) + myEnv := NewTriangle(globalT, 0.2, false, 0.2) + myNote := NewNote(globalT, freq, myEnv, myOsc) + mySyn.AddSound(myNote, globalT) case 769: // typeName = "KeyUp" } @@ -151,36 +158,8 @@ func main() { } textAt(font, blue, black, mainSurf, 2, 62, fmt.Sprintf("Sounds: %d", len(mySyn.Sounds))) window.UpdateSurface() - time.Sleep(time.Millisecond) - } - } - - if recordIt { - width := 800 - w2 := float64(width) / 2 - step := 5 - height := len(recordingR) / step - upLeft := image.Point{0, 0} - lowRight := image.Point{width, height} - all := image.Rectangle{upLeft, lowRight} - img := image.NewRGBA(all) - bg := image.NewUniform(imWhite) - draw.Draw(img, all, bg, image.Pt(0, 0), draw.Over) - row := 0 - for samp := 0; samp < len(recordingR); samp += step { - lt := int(w2) - rt := int((1 + recordingR[samp]) * w2) - if lt > rt { - lt, rt = rt, lt - } - for s := lt; s < rt; s++ { - img.Set(s, row, imBlue) - } - img.Set(width/2, row, imBlack) - row++ + // time.Sleep(time.Millisecond) } - f, _ := os.Create("line.jpg") - jpeg.Encode(f, img, &jpeg.Options{Quality: 95}) } } @@ -208,37 +187,3 @@ func textAt(f *ttf.Font, fgColor sdl.Color, bgColor sdl.Color, s *sdl.Surface, x return } - -var lastprint time.Time - -// Stream satisifies beep.Streamer, computes the instantaneous amplitude for each channel. -func (syn *Synth) Stream(samples [][2]float64) (n int, ok bool) { - when := syn.lastAt - for i := range samples { - when += syn.Tick - aR := syn.Amplitude(when) - aL := syn.Amplitude(when) - samples[i][0] = aR - samples[i][1] = aL - if recordIt { - recordingR = append(recordingR, aR) - recordingL = append(recordingL, aL) - } - } - syn.lastAt = when - syn.lastTime = time.Now() - return len(samples), true -} - -// line := charts.NewLine() -// line.SetGlobalOptions( -// charts.WithTitleOpts(opts.Title{Title: "Sound Output", Subtitle: "Final result, right channel only"}), -// charts.WithDataZoomOpts(opts.DataZoom{Type: "inside", Start: 0, End: 100}), -// charts.WithToolboxOpts(opts.Toolbox{Feature: &opts.ToolBoxFeature{DataZoom: &opts.ToolBoxFeatureDataZoom{Show: true}}}), -// ) -// line.SetXAxis(opts.XAxis{Name: "Sample #", Type: "time"}) -// d := make([]opts.LineData, len(recordingR), len(recordingR)) -// for i, v := range recordingR { -// d[i].Value = v -// } -// line.AddSeries("Right Amplitude", d) diff --git a/note.go b/note.go index be627aa..5861826 100644 --- a/note.go +++ b/note.go @@ -34,12 +34,17 @@ const ( var NoteFreqs map[string]Hertz func init() { + fmt.Printf("/n") NoteFreqs = make(map[string]Hertz) - for oct, octS := range "012345678" { + for oct, octS := range "0123456789" { f := C0freq * Hertz(math.Pow(2, float64(oct))) for n, note := range "CDEFGAB" { - NoteFreqs[string(note)+string(octS)] = f * Hertz(math.Pow(2, float64(n)*(1.0/7.0))) + ns := string(note) + string(octS) + ff := f * Hertz(math.Pow(2, float64(n)*(1.0/7.0))) + NoteFreqs[ns] = ff + fmt.Printf("%s: %3d ", ns, int(ff)) } + fmt.Printf("/n") for n, note := range "cdefgab" { NoteFreqs[string(note)+string(octS)] = f * Hertz(math.Pow(2, float64(n)*(1.0/7.0))) } @@ -57,18 +62,25 @@ func GetFreq(note string) Hertz { // Note is an instance of a voice, played with an envelope type Note struct { + Start Seconds BaseFreq Hertz Env Enveloper - Osc *Oscillator // just for now + Osc Osciller // just for now // Voice *Voicer // TODO } +// NewNote makes one +func NewNote(start Seconds, freq Hertz, env Enveloper, osc Osciller) *Note { + n := &Note{Start: start, BaseFreq: freq, Env: env, Osc: osc} + return n +} + // Length returns that of the underlying envelope -func (n Note) Length() Seconds { +func (n *Note) Length() Seconds { return n.Env.Length() } // Amplitude returns the signal strength at a given time -func (n Note) Amplitude(t Seconds) float64 { +func (n *Note) Amplitude(t Seconds) Volts { return n.Env.Amplitude(t) * n.Osc.Amplitude(t) } diff --git a/oscillators.go b/oscillators.go index b1f67df..ba0ca57 100644 --- a/oscillators.go +++ b/oscillators.go @@ -1,6 +1,8 @@ package main -import "math" +import ( + "math" +) // ██████╗ ███████╗ ██████╗██╗██╗ ██╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ // ██╔═══██╗██╔════╝██╔════╝██║██║ ██║ ██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ @@ -9,34 +11,43 @@ import "math" // ╚██████╔╝███████║╚██████╗██║███████╗███████╗██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ // ╚═════╝ ╚══════╝ ╚═════╝╚═╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ +// Osciller is an interface for oscillators +type Osciller interface { + Amplitude(t Seconds) Volts +} + // Oscillator is a periodically repeating waveform type Oscillator struct { - ν Hertz // Fundamental frequency - Phase Angle // Last known phase - PhaseAt Seconds // When that phase occurred - Wave func(a Angle) float64 // Function that describes wave shape + T0 Seconds // Global time when this osc started + ν Hertz // Fundamental frequency + Phase Angle // Last known phase + PhaseAt Seconds // When that phase occurred + Wave func(a Angle) Volts // Function that describes wave shape } // Waveform is a function that encodes the shape of the cycles of a waveform in *angle* -type Waveform func(a Angle) float64 +type Waveform func(a Angle) Volts -// Amplitude returns the strength of the waveform at a given *time* for a particular *oscillator*, which may change frequency -func (osc *Oscillator) Amplitude(t Seconds) float64 { - dT := t - osc.PhaseAt +// Amplitude returns the strength of the waveform (which may change frequency) at a given global time +func (osc *Oscillator) Amplitude(t Seconds) Volts { + ot := t - osc.T0 // local time + dT := ot - osc.PhaseAt dA := Angle(dT) * τ * Angle(osc.ν) // convert time to phase angle at new freq osc.Phase += dA - osc.PhaseAt = t + osc.PhaseAt = ot return osc.Wave(osc.Phase) } -// NewSine returns a new sine wave oscillator -func NewSine(newν Hertz) *Oscillator { +// NewSine returns a new sine wave oscillator starting at global time t +func NewSine(t Seconds, newν Hertz) *Oscillator { + // fmt.Printf("New sine osc at %f\n", t) return &Oscillator{ + T0: t, // Global start time ν: newν, Phase: 0, PhaseAt: 0, - Wave: func(a Angle) float64 { - return math.Sin(float64(a)) + Wave: func(a Angle) Volts { + return Volts(math.Sin(float64(a))) }, } } @@ -44,5 +55,4 @@ func NewSine(newν Hertz) *Oscillator { // NewFreq updates the frequency and Phase func (osc *Oscillator) NewFreq(ν Hertz) { osc.ν = ν - // syn.DeltaPhase = Angle(Seconds(f) * τ * syn.Tick) } diff --git a/scores/Rendez-vous_III_Laser_Harpe.mid b/scores/Rendez-vous_III_Laser_Harpe.mid new file mode 100644 index 0000000..01b16cd Binary files /dev/null and b/scores/Rendez-vous_III_Laser_Harpe.mid differ diff --git a/scores/readmidi.go b/scores/readmidi.go new file mode 100644 index 0000000..93fe8cd --- /dev/null +++ b/scores/readmidi.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + + "gitlab.com/gomidi/midi/reader" +) + +type printer struct{} + +func (pr printer) noteOn(p *reader.Position, channel, key, vel uint8) { + fmt.Printf("Track: %v Pos: %v NoteOn (ch %v: key %v vel: %v)\n", p.Track, p.AbsoluteTicks, channel, key, vel) +} + +func (pr printer) noteOff(p *reader.Position, channel, key, vel uint8) { + fmt.Printf("Track: %v Pos: %v NoteOff (ch %v: key %v)\n", p.Track, p.AbsoluteTicks, channel, key) +} + +func main() { + + var p printer + + // to disable logging, pass mid.NoLogger() as option + rd := reader.New(reader.NoLogger(), + // set the functions for the messages you are interested in + reader.NoteOn(p.noteOn), + reader.NoteOff(p.noteOff), + ) + + f := "Rendez-vous_III_Laser_Harpe.mid" + err := reader.ReadSMFFile(rd, f) + + if err != nil { + fmt.Printf("could not read SMF file %v\n", f) + } + +} diff --git a/scores/scores b/scores/scores new file mode 100755 index 0000000..defe6c8 Binary files /dev/null and b/scores/scores differ diff --git a/synth.go b/synth.go index c47a923..fa83f05 100644 --- a/synth.go +++ b/synth.go @@ -1,7 +1,11 @@ package main import ( + "image" + "image/draw" + "image/png" "math" + "os" "time" ) @@ -15,14 +19,15 @@ import ( // Synth is type Synth struct { T0 time.Time // When this synth started playing + SampleNo int // number of the last sample emitted Freq Hertz // Hz SR Hertz // Samples/Second Tick Seconds // Seconds/Sample DeltaPhase Angle // Radians/Sample - lastSample Angle // phase of the last sample we made. Used to avoid disconinuities during frequency changes - lastAt Seconds // When we made the last sample - lastTime time.Time // Sounds []*Sound // Sounds being considered for playing + recordingL []float64 + recordingR []float64 + recordIt bool } // Sound is a note played at a particular time @@ -33,7 +38,7 @@ type Sound struct { } // Amplitude is just that of the underlying note -func (snd Sound) Amplitude(t Seconds) float64 { +func (snd Sound) Amplitude(t Seconds) Volts { return snd.Note.Amplitude(t) } @@ -41,39 +46,39 @@ func (snd Sound) Amplitude(t Seconds) float64 { func NewSynth(t0 time.Time, f Hertz, sr Hertz) *Synth { syn := Synth{T0: t0, Freq: f, SR: sr} syn.Tick = Seconds(1 / sr) - // syn.DeltaPhase = Angle(Seconds(f) * τ * syn.Tick) - // syn.lastSample = 0.0 - syn.lastAt = 0.0 + syn.recordingL = make([]float64, 0, 1000000) + syn.recordingR = make([]float64, 0, 1000000) return &syn } -// Now returns the time the synth considers itself to be at, which is in fact the -// next time (in seconds from starting) at which a sample will be generated. +// Now is the current 'Global Time' of the synth in Seconds since starting // Sounds that wish to start immediately should do so at syn.Now() func (syn Synth) Now() Seconds { - return syn.lastAt + syn.Tick + return Seconds(float64(time.Now().Sub(syn.T0)) / float64(time.Second)) + // return syn.lastAt + syn.Tick } // AddSound adds a note to be played starting at time 'when' -func (syn *Synth) AddSound(n *Note, when Seconds) { - ns := &Sound{Note: n, Start: when, End: when + n.Length()} +func (syn *Synth) AddSound(n *Note, start Seconds) { + // fmt.Printf("Playing sound from %f to %f\n", start, start+n.Length()) + ns := &Sound{Note: n, Start: start, End: start + n.Length()} syn.Sounds = append(syn.Sounds, ns) // sort.Slice(syn.Sounds, func(i, j int) bool { return syn.Sounds[i].End < syn.Sounds[j].End }) } // Amplitude adds all the currently playing notes together, culls any that have completed -func (syn *Synth) Amplitude(t Seconds) float64 { - a := 0.0 +func (syn *Synth) Amplitude(t Seconds) Volts { + a := Volts(0.0) n := 0 for _, s := range syn.Sounds { if s.Start <= t && s.End >= t { - a += float64(s.Amplitude(t)) + a += s.Amplitude(t) n++ } } if n > 0 { - if math.Abs(a) > 1 { // clamp to +-1 - if math.Signbit(a) { + if math.Abs(float64(a)) > 1 { // clamp to +-1 + if math.Signbit(float64(a)) { return -1 } return 1 @@ -93,3 +98,83 @@ func (syn *Synth) PruneSounds(t Seconds) { } syn.Sounds = newSounds } + +// Stream satisifies beep.Streamer, computes the instantaneous amplitude for each channel. +func (syn *Synth) Stream(samples [][2]float64) (n int, ok bool) { + for i := range samples { + when := Seconds(syn.SampleNo) * syn.Tick + aR := syn.Amplitude(when) + aL := syn.Amplitude(when) + samples[i][0] = float64(aR) + samples[i][1] = float64(aL) + if syn.recordIt { + syn.recordingR = append(syn.recordingR, float64(aR)) + syn.recordingL = append(syn.recordingL, float64(aL)) + } + syn.SampleNo++ + } + return len(samples), true +} + +// Graphout draws an graphic of this synth +func (syn Synth) Graphout() { + + nSamples := len(syn.recordingR) + nSecs := 1 + (nSamples / int(syn.SR)) + colH := 200 + sideH := colH / 2 + colW := 2000 + margin := 20 + yScale := float64(colH) / 2 + totalW := margin*2 + colW + totalH := margin*2 + nSecs*(colH+margin) + totalT := Seconds(nSamples) / Seconds(syn.SR) + + xy := func(s int) (x, y int) { + stripe := s / int(syn.SR) + col := margin + int(float64(colW)*float64(s%int(syn.SR))/float64(syn.SR)) + row := margin + stripe*(colH+margin) + colH/2 + return col, row + } + + t2samp := func(t Seconds) int { + return int(t * Seconds(syn.SR)) + } + + upLeft := image.Point{0, 0} + lowRight := image.Point{totalW, totalH} + all := image.Rectangle{upLeft, lowRight} + img := image.NewRGBA(all) + bg := image.NewUniform(imWhite) + draw.Draw(img, all, bg, image.Pt(0, 0), draw.Over) + + // Output waveform + for samp := 0; samp < nSamples; samp++ { + x, y := xy(samp) + img.Set(x, y+int(syn.recordingR[samp]*yScale), imBlue) + img.Set(x, y, imBlack) + } + + // 0.1s red ticks on time axis + for t := Seconds(0); t < totalT; t += 0.1 { + x, y := xy(t2samp(t)) + for i := -sideH / 4; i < sideH/4; i++ { + img.Set(x, y+i, imRed) + } + } + + // Green lines at start of each sound + for _, s := range syn.Sounds { + x, y := xy(t2samp(s.Start)) + for i := -sideH; i < sideH; i++ { + img.Set(x, y+i, imGreen) + } + } + + // f, _ := os.Create("line.jpg") + // jpeg.Encode(f, img, &jpeg.Options{Quality: 95}) + + f, _ := os.Create("line2.png") + png.Encode(f, img) + +}