Skip to content

Commit

Permalink
Mutes (#2)
Browse files Browse the repository at this point in the history
* started working on mutes mode

* handle track buttons properly in mutes mode
  • Loading branch information
briansorahan authored Apr 2, 2017
1 parent f79d020 commit 5fd545e
Show file tree
Hide file tree
Showing 2 changed files with 228 additions and 44 deletions.
247 changes: 206 additions & 41 deletions sequencer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"os"
"time"

"github.com/pkg/errors"
"github.com/scgolang/syncosc"
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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{}
Expand All @@ -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 (
Expand All @@ -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] {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -317,3 +480,5 @@ var resolutionMap = map[string]int{
"32nd": 3,
"96th": 1,
}

type loopFunc func(ctx context.Context, hits <-chan Hit) error
25 changes: 22 additions & 3 deletions sequencer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}

Expand Down

0 comments on commit 5fd545e

Please sign in to comment.