diff --git a/sequencer.go b/sequencer.go index 668692c..2e7d500 100644 --- a/sequencer.go +++ b/sequencer.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "time" "github.com/pkg/errors" "github.com/scgolang/syncosc" @@ -24,6 +25,14 @@ var ( stepColor = Color{Red: Full} ) +// Mode is a +type Mode int + +const ( + ModePattern Mode = iota + ModeMutes +) + // Trig is a sequencer trigger. // It provides the track that is being triggered as well // as the value of the sequencer for that track. @@ -41,6 +50,8 @@ type Trigger interface { // Sequencer is a simple sequencer controlled by a Novation Launchpad. type Sequencer struct { + modeChan chan Mode + mutes [gridSize]bool pad *Launchpad prevStep uint8 step uint8 @@ -56,6 +67,7 @@ type Sequencer struct { // NewSequencer creates a new sequencer. func (l *Launchpad) NewSequencer(syncConnector syncosc.ConnectorFunc, syncHost string) *Sequencer { return &Sequencer{ + modeChan: make(chan Mode, 1), pad: l, syncConnector: syncConnector, syncHost: syncHost, @@ -117,6 +129,30 @@ func (seq *Sequencer) advanceLights() error { return nil } +// flashStepTriggers flashes the tracks that are triggered for the current step. +func (seq *Sequencer) flashStepTriggers() error { + hits := []Hit{} + for track, steps := range seq.tracks { + if seq.mutes[track] { + continue + } + if val := steps[seq.step]; val > 0 { + hit := stepToHit(uint8(track)) + hits = append(hits, hit) + if err := seq.pad.Light(hit.X, hit.Y, posColor); err != nil { + return err + } + } + } + time.Sleep(40 * time.Millisecond) + for _, hit := range hits { + if err := seq.pad.Light(hit.X, hit.Y, Color{}); err != nil { + return err + } + } + return nil +} + // invokeTriggers invokes the sequencer's triggers for the provided step. func (seq *Sequencer) invokeTriggers() error { trigs := []Trig{} @@ -137,6 +173,18 @@ func (seq *Sequencer) invokeTriggers() error { return nil } +// invokeTriggersTrack invokes the Track method of all the Trigger's +// that have been added to the sequencer. +func (seq *Sequencer) invokeTriggersTrack() error { + // Invoke all the trigs. + for _, trig := range seq.triggers { + if err := trig.Track(seq.track); err != nil { + return err + } + } + return nil +} + // lightCurrentTrack lights the track buttons based on the currently selected track. func (seq *Sequencer) lightCurrentTrack() error { var ( @@ -149,6 +197,19 @@ func (seq *Sequencer) lightCurrentTrack() error { return seq.pad.Light(gridX, curY, stepColor) } +// lightMutes lights the mutes. +func (seq *Sequencer) lightMutes() error { + for track, isMuted := range seq.mutes { + if isMuted { + hit := stepToHit(uint8(track)) + if err := seq.pad.Light(hit.X, hit.Y, stepColor); err != nil { + return err + } + } + } + return nil +} + // lightTrackSteps lights all the steps of the current track. func (seq *Sequencer) lightTrackSteps() error { for step, val := range seq.tracks[seq.track] { @@ -167,56 +228,142 @@ func (seq *Sequencer) lightTrackSteps() error { return nil } -// Main is the main loop of the sequencer. -// It loops forever on input from the launchpad. -// If ctx is cancelled it returns the ctx.Err(). -func (seq *Sequencer) Main(ctx context.Context) error { - hits, err := seq.pad.Hits() - if err != nil { - return err +// loopMutes is an infinite loop that the sequencer uses when it is in "Mutes" mode. +// If the mode is changed then it will be returned with a nil error. +// The only other time this func returns is when there is an error. +func (seq *Sequencer) loopMutes(ctx context.Context, hits <-chan Hit) (Mode, error) { + if err := seq.lightMutes(); err != nil { + return 0, err } - // This func could block forever - go func() { - ctx, cancel := context.WithCancel(ctx) - if err := seq.syncConnector(ctx, seq, seq.syncHost); err != nil { - cancel() - fmt.Fprintf(os.Stderr, "connecting to sync source: %s", err.Error()) + for { + select { + case <-ctx.Done(): + return 0, ctx.Err() + case hit := <-hits: + if hit.Err != nil { + return 0, hit.Err + } + if hit.X == gridX || hit.Y == gridY { + if err := seq.setCurrentTrackFrom(hit); err != nil { + return 0, err + } + if err := seq.pad.Reset(); err != nil { + return 0, err + } + if err := seq.lightCurrentTrack(); err != nil { + return 0, err + } + if err := seq.lightMutes(); err != nil { + return 0, err + } + continue + } + if err := seq.toggleMuteFrom(hit); err != nil { + return 0, err + } + case mode := <-seq.modeChan: + return mode, nil + case pulse := <-seq.tick: + if advanced := seq.advance(pulse.Count); !advanced { + continue + } + // Flash all the triggered tracks. + if err := seq.flashStepTriggers(); err != nil { + return 0, err + } + if err := seq.invokeTriggers(); err != nil { + return 0, err + } } - }() - if err := seq.lightCurrentTrack(); err != nil { - return err + } +} + +// loopPattern is an infinite loop that the sequencer uses when it is in "Pattern" mode. +// If the mode is changed then it will be returned with a nil error. +// The only other time this func returns is when there is an error. +func (seq *Sequencer) loopPattern(ctx context.Context, hits <-chan Hit) (Mode, error) { + if err := seq.lightTrackSteps(); err != nil { + return 0, err } for { select { case <-ctx.Done(): - return ctx.Err() + return 0, ctx.Err() case hit := <-hits: if hit.Err != nil { - return hit.Err + return 0, hit.Err } if hit.X == gridX || hit.Y == gridY { - if err := seq.selectTrackFrom(hit); err != nil { - return err + if err := seq.setCurrentTrackFrom(hit); err != nil { + return 0, err + } + if err := seq.selectPatternTrackFrom(hit); err != nil { + return 0, err } continue } if err := seq.toggle(hit); err != nil { - return err + return 0, err } + case mode := <-seq.modeChan: + return mode, nil case pulse := <-seq.tick: if advanced := seq.advance(pulse.Count); !advanced { continue } if err := seq.advanceLights(); err != nil { - return err + return 0, err } if err := seq.invokeTriggers(); err != nil { - return err + return 0, err } } } } +// Main is the main loop of the sequencer. +// It loops forever on input from the launchpad. +// If ctx is cancelled it returns the ctx.Err(). +func (seq *Sequencer) Main(ctx context.Context) error { + hits, err := seq.pad.Hits() + if err != nil { + return err + } + // This func could block forever. + go func() { + ctx, cancel := context.WithCancel(ctx) + if err := seq.syncConnector(ctx, seq, seq.syncHost); err != nil { + cancel() + fmt.Fprintf(os.Stderr, "connecting to sync source: %s", err.Error()) + } + }() + if err := seq.lightCurrentTrack(); err != nil { + return err + } + loop := seq.loopPattern + + for { + if err := seq.pad.Reset(); err != nil { + return err + } + if err := seq.lightCurrentTrack(); err != nil { + return err + } + mode, err := loop(ctx, hits) + if err != nil { + return err + } + switch mode { + case ModePattern: + loop = seq.loopPattern + case ModeMutes: + loop = seq.loopMutes + default: + return errors.Errorf("unrecognized mode: %d", mode) + } + } +} + // Pulse receives pulses from oscsync. func (seq *Sequencer) Pulse(pulse syncosc.Pulse) error { seq.tick <- pulse @@ -229,8 +376,22 @@ func (seq *Sequencer) ReadFrom(r io.Reader) (int64, error) { return 0, nil } -// selectTrackFrom selects a track from the provided hit. -func (seq *Sequencer) selectTrackFrom(hit Hit) error { +// selectPatternTrackFrom selects a track from the provided hit. +// This func is used in pattern mode. +func (seq *Sequencer) selectPatternTrackFrom(hit Hit) error { + // Reset the launchpad. + if err := seq.pad.Reset(); err != nil { + return errors.Wrap(err, "resetting launchpad") + } + // Light the current track. + if err := seq.lightCurrentTrack(); err != nil { + return err + } + // Light all the steps of the current track. + return seq.lightTrackSteps() +} + +func (seq *Sequencer) setCurrentTrackFrom(hit Hit) error { if hit.Y == gridY { // We hit the top row. curX := seq.track % gridX @@ -252,22 +413,13 @@ func (seq *Sequencer) selectTrackFrom(hit Hit) error { } else { return errors.New("hit is not for track selection") } - // Reset the launchpad. - if err := seq.pad.Reset(); err != nil { - return errors.Wrap(err, "resetting launchpad") - } - // Light the current track. - if err := seq.lightCurrentTrack(); err != nil { - return err - } - // Invoke all the trigs. - for _, trig := range seq.triggers { - if err := trig.Track(seq.track); err != nil { - return err - } - } - // Light all the steps of the current track. - return seq.lightTrackSteps() + return nil +} + +// SetMode sets the display mode of the sequencer. +func (seq *Sequencer) SetMode(mode Mode) error { + seq.modeChan <- mode + return nil } // SetResolution sets the clock resolution for the sequencer. @@ -295,6 +447,17 @@ func (seq *Sequencer) toggle(hit Hit) error { return seq.pad.Light(hit.X, hit.Y, Color{}) } +// toggleMuteFrom toggles the state of a mute from a hit on the launchpad. +func (seq *Sequencer) toggleMuteFrom(hit Hit) error { + track := hitToStep(hit) + if seq.mutes[track] { + seq.mutes[track] = false + return seq.pad.Light(hit.X, hit.Y, Color{}) + } + seq.mutes[track] = true + return seq.pad.Light(hit.X, hit.Y, stepColor) +} + // WriteTo writes the current sequencer data to w. // TODO func (seq *Sequencer) WriteTo(w io.Writer) (int64, error) { @@ -317,3 +480,5 @@ var resolutionMap = map[string]int{ "32nd": 3, "96th": 1, } + +type loopFunc func(ctx context.Context, hits <-chan Hit) error diff --git a/sequencer_test.go b/sequencer_test.go index 621eecb..cc9411e 100644 --- a/sequencer_test.go +++ b/sequencer_test.go @@ -5,14 +5,33 @@ import ( "testing" "time" + "github.com/scgolang/launchpad" "github.com/scgolang/syncosc" ) func TestSequencer(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() - if err := seq.Main(ctx); err != nil && err != context.DeadlineExceeded { - t.Fatal(err) + + done := make(chan struct{}) + go func() { + if err := seq.Main(ctx); err != nil && err != context.DeadlineExceeded { + t.Fatal(err) + } + close(done) + }() + time.Sleep(20 * time.Second) + seq.SetMode(launchpad.ModeMutes) + + time.Sleep(20 * time.Second) + seq.SetMode(launchpad.ModePattern) + + time.Sleep(20 * time.Second) + + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("timeout") } }