Skip to content

Commit abef511

Browse files
committed
fix problem with shutdown and open notes
1 parent 2c2004e commit abef511

7 files changed

Lines changed: 98 additions & 22 deletions

File tree

core/timeline.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,17 @@ func (t *Timeline) Len() int64 {
5757
func (t *Timeline) Play() {
5858
t.resume = make(chan bool)
5959
t.isPlaying = true
60-
for {
60+
for t.isPlaying {
6161
t.protection.RLock()
6262
here := t.head
6363
t.protection.RUnlock()
6464
if here == nil {
65-
<-t.resume
66-
continue
65+
// Wait for a signal (new event or shutdown signal)
66+
select {
67+
case <-t.resume:
68+
// Continue to check isPlaying at top of loop
69+
continue
70+
}
6771
}
6872
now := time.Now()
6973
for now.After(here.when) {
@@ -100,6 +104,25 @@ func (t *Timeline) Reset() {
100104
t.tail = nil
101105
}
102106

107+
// Stop signals the Play loop to exit gracefully.
108+
// Should be called during shutdown to clean up the timeline goroutine.
109+
func (t *Timeline) Stop() {
110+
t.protection.Lock()
111+
wasPlaying := t.isPlaying
112+
t.isPlaying = false
113+
t.protection.Unlock()
114+
115+
// If we were playing, wake up the goroutine so it can check isPlaying and exit
116+
if wasPlaying && t.resume != nil {
117+
select {
118+
case t.resume <- true:
119+
// Signal sent successfully
120+
case <-time.After(100 * time.Millisecond):
121+
// Timeout in case goroutine is already stopped
122+
}
123+
}
124+
}
125+
103126
// Schedule adds an event for a given time
104127
func (t *Timeline) Schedule(event TimelineEvent, when time.Time) error {
105128
now := time.Now()

midi/midi_message.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ const (
1313
noteOn int64 = 0x90 // 10010000 , 144
1414
noteOff int64 = 0x80 // 10000000 , 128
1515
controlChange int64 = 0xB0 // 10110000 , 176
16-
noteAllOff int64 = 0x78 // 01111000 , 120 (not 123 because sustain)
17-
sustainPedal int64 = 0x40
16+
allSoundOff int64 = 0x78 // CC120
17+
allNotesOff int64 = 0x7B // CC123
18+
sustainPedal int64 = 0x40 // CC64
19+
sustainOff int64 = 0x00
1820
)
1921

2022
type Message struct {

midi/output_device.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,49 @@ func (d *OutputDevice) Reset() {
3838
notify.Warnf("reset failed for device:%v", d.id)
3939
}
4040
}()
41+
// Stop the timeline goroutine first, then clear the queue
42+
d.timeline.Stop()
4143
d.timeline.Reset()
44+
// Give timeline time to stop processing any pending events
45+
time.Sleep(10 * time.Millisecond)
46+
4247
if notify.IsDebug() {
4348
notify.Debugf("device.%d: sending Note OFF to all 16 channels", d.id)
4449
}
4550
if d.stream != nil {
46-
// send note off all to all channels for current device
51+
// Send a robust panic sequence for each channel.
52+
// Some devices respond better to CC123, others to CC120, and sustained notes
53+
// require a sustain-off first.
4754
for c := 1; c <= 16; c++ {
48-
if err := d.stream.WriteShort(controlChange|int64(c-1), noteAllOff, 0); err != nil {
55+
status := controlChange | int64(c-1)
56+
notify.Debugf("reset: sending off events to [channel %d]", c)
57+
if err := d.stream.WriteShort(status, sustainPedal, sustainOff); err != nil {
58+
notify.Console.Errorf("device.%d: midi write error:%v", d.id, err)
59+
}
60+
if err := d.stream.WriteShort(status, allNotesOff, 0); err != nil {
61+
notify.Console.Errorf("device.%d: midi write error:%v", d.id, err)
62+
}
63+
if err := d.stream.WriteShort(status, allSoundOff, 0); err != nil {
4964
notify.Console.Errorf("device.%d: midi write error:%v", d.id, err)
5065
}
66+
// Small delay between channels to allow receiver to process messages.
67+
time.Sleep(5 * time.Millisecond)
68+
}
69+
70+
// Send explicit note-off messages for all notes on all channels.
71+
// This is more direct than relying on CC123/CC120 alone and handles
72+
// devices/DAWs that may not respond to control changes during shutdown.
73+
noteOff := int64(0x80) // MIDI Note Off status
74+
for c := 1; c <= 16; c++ {
75+
status := noteOff | int64(c-1)
76+
notify.Debugf("reset: sending explicit note-off for all notes on channel %d", c)
77+
for n := 0; n <= 127; n++ {
78+
if err := d.stream.WriteShort(status, int64(n), 0); err != nil {
79+
notify.Console.Errorf("device.%d: note-off write error:%v", d.id, err)
80+
}
81+
}
82+
// Small delay between channels
83+
time.Sleep(5 * time.Millisecond)
5184
}
5285
}
5386
}

midi/registry_device.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,21 @@ func (r *DeviceRegistry) DefaultDeviceIDs() (inputDeviceID, outputDeviceID int)
4747
}
4848

4949
func (r *DeviceRegistry) Reset() {
50+
r.mutex.RLock()
51+
outs := make([]*OutputDevice, 0, len(r.out))
5052
for _, each := range r.out {
51-
each.Reset()
53+
outs = append(outs, each)
5254
}
55+
ins := make([]*InputDevice, 0, len(r.in))
5356
for _, each := range r.in {
57+
ins = append(ins, each)
58+
}
59+
r.mutex.RUnlock()
60+
61+
for _, each := range outs {
62+
each.Reset()
63+
}
64+
for _, each := range ins {
5465
each.stopListener()
5566
}
5667
}
@@ -178,7 +189,14 @@ func (r *DeviceRegistry) initInputs() error {
178189
}
179190

180191
func (r *DeviceRegistry) Close() error {
192+
r.mutex.RLock()
193+
ins := make([]*InputDevice, 0, len(r.in))
181194
for _, each := range r.in {
195+
ins = append(ins, each)
196+
}
197+
r.mutex.RUnlock()
198+
199+
for _, each := range ins {
182200
each.stopListener()
183201
}
184202
return r.streamRegistry.close()

midi/transport/rt.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,8 @@ func (l *RtListener) handleRtEvent(m rtmidi.MIDIIn, data []byte, delta float64)
140140
func (l *RtListener) Stop() {
141141
l.mutex.Lock()
142142
defer l.mutex.Unlock()
143-
if l.listening {
144-
if err := l.midiIn.CancelCallback(); err != nil {
145-
notify.Warnf("failed to cancel listener callback")
146-
}
147-
}
143+
// Keep the native callback installed and only gate delivery in handleRtEvent.
144+
// Calling CancelCallback can race with in-flight native callbacks and cause
145+
// a nil dereference inside the upstream rtmidi binding.
148146
l.listening = false
149147
}

system/tear_down.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ import (
77
)
88

99
func TearDown(ctx core.Context) error {
10+
// Stop timing/scheduling first to minimize new events during shutdown.
1011
dsl.StopAllPlayables(ctx)
11-
ctx.Control().Reset()
12-
ctx.Device().Close()
12+
ctx.Control().Stop()
13+
// Force immediate silence before closing MIDI streams.
14+
ctx.Device().Reset()
15+
if err := ctx.Device().Close(); err != nil {
16+
return err
17+
}
1318
notify.PrintBye()
1419
return nil
1520
}

ui/cli/app.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,15 @@ func StartREPL(ctx core.Context) {
4242
// start REPL
4343
line := liner.NewLiner()
4444
defer line.Close()
45-
defer tearDown(line, ctx)
45+
defer tearDown(line)
4646
// TODO liner catches control+c
4747
//setupCloseHandler(line)
4848
ctx.Device().Report()
4949
setup(line)
5050
repl(line, ctx)
5151
}
5252

53-
func tearDown(line *liner.State, ctx core.Context) {
54-
ctx.Control().Reset()
55-
ctx.Device().Reset()
53+
func tearDown(line *liner.State) {
5654
if f, err := os.Create(history); err != nil {
5755
notify.Print(notify.NewErrorf("error writing history file:%v", err))
5856
} else {
@@ -77,7 +75,6 @@ func repl(line *liner.State, ctx core.Context) {
7775
entry, err := line.Prompt(notify.Prompt())
7876
if err != nil {
7977
notify.Print(notify.NewError(err))
80-
tearDown(line, ctx)
8178
goto exit
8279
}
8380
entry:
@@ -134,13 +131,13 @@ exit:
134131
// setupCloseHandler creates a 'listener' on a new goroutine which will notify the
135132
// program if it receives an interrupt from the OS. We then handle this by calling
136133
// our clean up procedure and exiting the program.
137-
func setupCloseHandler(line *liner.State, ctx core.Context) {
134+
func setupCloseHandler(line *liner.State) {
138135
c := make(chan os.Signal, 1)
139136
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
140137
go func() {
141138
<-c
142139
fmt.Println("\r- Ctrl+C pressed in Terminal")
143-
tearDown(line, ctx)
140+
tearDown(line)
144141
os.Exit(0)
145142
}()
146143
}

0 commit comments

Comments
 (0)