- commit
- ba0b731
- parent
- f751184
- author
- Neale Pickett
- date
- 2026-02-17 21:22:12 -0700 MST
bellows + unit tests
9 files changed,
+251,
-65
+33,
-0
1@@ -0,0 +1,33 @@
2+package main
3+
4+import (
5+ "tinygo.org/x/drivers"
6+ "tinygo.org/x/drivers/vl6180x"
7+)
8+
9+// BellowsSlop defines the coarseness of the bellows action.
10+// Set this lower to make the bellows more "twitchy"
11+const BellowsSlop = 2
12+
13+type Bellows struct {
14+ vl6180x.Device
15+ prev uint16
16+}
17+
18+func NewBellows(i2c drivers.I2C) *Bellows {
19+ b := &Bellows{
20+ Device: vl6180x.New(i2c),
21+ }
22+ b.Device.Configure(false) // this argument is ignored!
23+ return b
24+}
25+
26+// Read the direction of the bellows.
27+//
28+// Negative means draw, because that makes more sense to me.
29+func (b *Bellows) Direction() int16 {
30+ distance := b.Read()
31+ dir := int16(b.prev -distance) / BellowsSlop
32+ b.prev = distance
33+ return dir
34+}
1@@ -0,0 +1,36 @@
2+package main
3+
4+import "git.woozle.org/neale/concertina/pkg/layouts"
5+import "git.woozle.org/neale/concertina/internal/midi"
6+
7+// XXX: figure out where to put this documentation.
8+//
9+// Buttons are read from four (4) 16-bit GPIO expanders, two per side.
10+// Each row of buttons goes to one byte in the expander,
11+// and there are four rows (three finger rows and one thumb row).
12+//
13+// I spent a long time wringing my hands about wasting all those GPIO ports,
14+// but the GPIO expander is $2.50, so I'm going to try to not worry about it.
15+// Anyway, nobody makes a 17-port GPIO expander.
16+
17+// ButtonMap maps buttons to MIDI notes.
18+type ButtonMap struct {
19+ Push [4][16]midi.Note
20+ Draw [4][16]midi.Note
21+}
22+
23+// MapLayout generates a ButtonMapping from a layouts.Layout.
24+func MapLayout(l *layouts.Layout) *ButtonMap {
25+ m := new(ButtonMap)
26+ for side := range layouts.Sides {
27+ for row := range layouts.Rows {
28+ for col := range layouts.Columns {
29+ expander := (side*2) + row/2
30+ bit := ((row % 2) * 8) + col
31+ m.Push[expander][bit] = midi.Note(l.Notes[side][row][col][0])
32+ m.Draw[expander][bit] = midi.Note(l.Notes[side][row][col][1])
33+ }
34+ }
35+ }
36+ return m
37+}
+39,
-16
1@@ -6,6 +6,7 @@ import (
2 "machine"
3 "time"
4
5+ "git.woozle.org/neale/concertina/internal/midi"
6 "git.woozle.org/neale/concertina/pkg/layouts"
7 "git.woozle.org/neale/concertina/pkg/led"
8 "git.woozle.org/neale/concertina/pkg/pcf8575"
9@@ -22,8 +23,10 @@ var Black = color.RGBAModel.Convert(color.Black).(color.RGBA)
10
11 type Concertina struct {
12 display *ssd1306.Device
13- tonegen MidiWriter
14+ tonegen midi.Writer
15+ bellows *Bellows
16 expanders [4]pcf8575.Device
17+ buttonMap *ButtonMap
18 layouts []*layouts.Layout
19 currentLayout int
20 }
21@@ -47,7 +50,7 @@ func NewConcertina(i2c drivers.I2C) (*Concertina, error) {
22 }
23
24 c := &Concertina{
25- tonegen: MidiWriter{Writer: machine.DefaultUART},
26+ tonegen: midi.Writer{Writer: machine.DefaultUART},
27 }
28
29 for i := range c.expanders {
30@@ -57,13 +60,12 @@ func NewConcertina(i2c drivers.I2C) (*Concertina, error) {
31 }
32 }
33
34- if false {
35- if l, err := layouts.DefaultLayouts(); err != nil {
36- return nil, err
37- } else {
38- c.layouts = l
39- }
40+ if l, err := layouts.DefaultLayouts(); err != nil {
41+ return nil, err
42+ } else {
43+ c.layouts = l
44 }
45+ c.loadLayout(0)
46
47 c.display = ssd1306.NewI2C(i2c)
48 c.display.Configure(
49@@ -74,9 +76,11 @@ func NewConcertina(i2c drivers.I2C) (*Concertina, error) {
50 },
51 )
52 c.display.ClearDisplay()
53- tinyfont.WriteLine(c.display, &proggy.TinySZ8pt7b, 0, 20, "Ruby", White)
54+ tinyfont.WriteLine(c.display, &proggy.TinySZ8pt7b, 0, 10, "Ruby", White)
55 c.display.Display()
56
57+ c.bellows = NewBellows(i2c)
58+
59 <-resetTimer // Wait until this timer has elapsed
60 VS1053_RESET.High()
61
62@@ -85,24 +89,43 @@ func NewConcertina(i2c drivers.I2C) (*Concertina, error) {
63 return c, nil
64 }
65
66+func (c *Concertina) loadLayout(n int) {
67+ c.buttonMap = MapLayout(c.layouts[n])
68+ c.currentLayout = n
69+}
70+
71 func (c *Concertina) Loop() {
72 var buttons [4]uint16
73+ //toPlay := new(Polyphony)
74+
75+ // get the bellows direction
76+ c.display.FillRectangle(0, 16, 100, 4, Black)
77+ dir := c.bellows.Direction()
78+ if (dir < 0) {
79+ c.display.FillRectangle(50+dir, 16, -dir, 4, White)
80+ } else if (dir > 0) {
81+ c.display.FillRectangle(50, 16, dir, 4, White)
82+ }
83
84- c.display.FillRectangle(124, 0, 4, 16, Black)
85- for x := range 4 {
86+ // Read Everybody
87+ for x := range buttons {
88 buttons[x] = c.expanders[x].Get()
89+ }
90+
91+ // Draw what was pressed
92+ c.display.FillRectangle(124, 0, 4, 16, Black)
93+ for x, bits := range buttons {
94 for bit := range 16 {
95- pc := White
96- if buttons[x]&(1<<bit) != 0 {
97- pc = Black
98+ if bits&(1<<bit) != 0 {
99+ c.display.SetPixel(int16(124+x), int16(bit), White)
100 }
101- c.display.SetPixel(int16(124+x), int16(bit), pc)
102 }
103 }
104+
105 c.display.Display()
106
107 //c.tonegen.NoteOn(byte(x))
108- time.Sleep(200 * time.Millisecond)
109+ //time.Sleep(200 * time.Millisecond)
110 //c.tonegen.NoteOff(byte(x))
111 }
112
+0,
-45
1@@ -1,45 +0,0 @@
2-package main
3-
4-import "io"
5-
6-type MidiWriter struct {
7- io.Writer
8- Channel byte
9- Playing *MidiNotes
10-}
11-
12-func (w MidiWriter) NoteOnVelocity(note byte, velocity byte) {
13- w.Write([]byte{0x90 + w.Channel, note, velocity})
14- w.Playing.On(note)
15-}
16-
17-func (w MidiWriter) NoteOn(note byte) {
18- w.NoteOnVelocity(note, 127)
19-}
20-
21-func (w MidiWriter) NoteOffVelocity(note byte, velocity byte) {
22- w.Write([]byte{0x80 + w.Channel, note, velocity})
23- w.Playing.Off(note)
24-}
25-
26-func (w MidiWriter) NoteOff(note byte) {
27- w.NoteOffVelocity(note, 127)
28-}
29-
30-func (w MidiWriter) SetPatch(patch byte) {
31- w.Write([]byte{0xC0 + w.Channel, patch})
32-}
33-
34-type MidiNotes [2]uint64
35-
36-func (n *MidiNotes) Equal(a MidiNotes) bool {
37- return (n[0] == a[0]) && (n[1] == a[1])
38-}
39-
40-func (n *MidiNotes) On(note byte) {
41- n[note/64] |= 1 << (note % 64)
42-}
43-
44-func (n *MidiNotes) Off(note byte) {
45- n[note/64] &= ^(1 << (note % 64))
46-}
+2,
-0
1@@ -0,0 +1,2 @@
2+I can't figure out how to make `tinygo test` work.
3+By putting things in here, I can use plain ol' `go test`.
+62,
-0
1@@ -0,0 +1,62 @@
2+package midi
3+
4+import "io"
5+
6+// Note is a MIDI note, between 0 and 127.
7+// Any value <0 means no note.
8+type Note int8
9+
10+// Writer sends note on/off events to a MIDI device,
11+// and keeps track of what's currently playing.
12+type Writer struct {
13+ io.Writer
14+ Channel byte
15+ Playing *Polyphony
16+}
17+
18+func NewWriter(w io.Writer, channel byte) (*Writer) {
19+ return &Writer{
20+ w,
21+ channel,
22+ &Polyphony{},
23+ }
24+}
25+
26+func (w *Writer) NoteOnVelocity(note byte, velocity byte) {
27+ w.Write([]byte{0x90 + w.Channel, note, velocity})
28+ w.Playing.On(note)
29+}
30+
31+func (w *Writer) NoteOn(note byte) {
32+ w.NoteOnVelocity(note, 127)
33+}
34+
35+func (w *Writer) NoteOffVelocity(note byte, velocity byte) {
36+ w.Write([]byte{0x80 + w.Channel, note, velocity})
37+ w.Playing.Off(note)
38+}
39+
40+func (w *Writer) NoteOff(note byte) {
41+ w.NoteOffVelocity(note, 127)
42+}
43+
44+func (w *Writer) SetPatch(patch byte) {
45+ w.Write([]byte{0xC0 + w.Channel, patch})
46+}
47+
48+// Polyphony tracks what notes are currently playing.
49+type Polyphony struct {
50+ playing [2]uint64
51+}
52+
53+func (n *Polyphony) Equal(a Polyphony) bool {
54+ return (n.playing[0] == a.playing[0]) && (n.playing[1] == a.playing[1])
55+}
56+
57+func (n *Polyphony) On(note byte) {
58+ n.playing[note/64] |= 1 << (note % 64)
59+}
60+
61+func (n *Polyphony) Off(note byte) {
62+ n.playing[note/64] &= ^(1 << (note % 64))
63+}
+63,
-0
1@@ -0,0 +1,63 @@
2+package midi
3+
4+import (
5+ "math/bits"
6+ "testing"
7+)
8+
9+// WriteCounter just counts how many bytes were written
10+type WriteCounter struct {
11+ len int
12+}
13+func (w *WriteCounter) Write(p []byte) (int, error) {
14+ w.len += len(p)
15+ return len(p), nil
16+}
17+
18+func (n *Polyphony) OnesCount() int {
19+ return bits.OnesCount64(n.playing[0]) + bits.OnesCount64(n.playing[1])
20+}
21+
22+func TestWriter(t *testing.T) {
23+ counter := new(WriteCounter)
24+ w := NewWriter(counter, 0)
25+
26+ if w.Playing.OnesCount() != 0 {
27+ t.Error("polyphony tracking is busted")
28+ }
29+
30+ w.NoteOn(12)
31+ if w.Playing.OnesCount() != 1 {
32+ t.Error("polyphony tracking is busted")
33+ }
34+
35+ w.NoteOn(12)
36+ if w.Playing.OnesCount() != 1 {
37+ t.Error("polyphony tracking is busted")
38+ }
39+
40+ w.NoteOn(20)
41+ if w.Playing.OnesCount() != 2 {
42+ t.Error("polyphony tracking is busted")
43+ }
44+
45+ w.NoteOn(20)
46+ if w.Playing.OnesCount() != 2 {
47+ t.Error("polyphony tracking is busted")
48+ }
49+
50+ w.NoteOff(12)
51+ if w.Playing.OnesCount() != 1 {
52+ t.Error("polyphony tracking is busted")
53+ }
54+
55+ w.NoteOff(12)
56+ if w.Playing.OnesCount() != 1 {
57+ t.Error("polyphony tracking is busted")
58+ }
59+
60+ w.NoteOff(20)
61+ if w.Playing.OnesCount() != 0 {
62+ t.Error("polyphony tracking is busted")
63+ }
64+}
+4,
-4
1@@ -47,10 +47,6 @@ func ParseLayouts(s string) ([]*Layout, error) {
2 continue
3 }
4
5- if row >= Rows {
6- return nil, &ParseError{lineno, "too many rows"}
7- }
8-
9 // Split the line up into fields
10 fields := strings.Fields(line)
11
12@@ -64,6 +60,10 @@ func ParseLayouts(s string) ([]*Layout, error) {
13 continue
14 }
15
16+ if row >= Rows {
17+ return nil, &ParseError{lineno, "too many rows: " + line}
18+ }
19+
20 // We can skip the middle | now
21 fields = append(fields[:5], fields[6:]...)
22 for col, button := range fields {
+12,
-0
1@@ -40,3 +40,15 @@ func TestParser(t *testing.T) {
2 t.Error("wrong note")
3 }
4 }
5+
6+func TestDefaultParser(t *testing.T) {
7+ layouts, err := DefaultLayouts()
8+ if err != nil {
9+ t.Fatal(err)
10+ }
11+
12+ // If you add or remove a layout, you'll have to change this constant :)
13+ if l := len(layouts); l != 4 {
14+ t.Errorf("wrong number of layouts: %d", l)
15+ }
16+}