Skip to main content
Glama
emicklei

melrōse musical expression player

by emicklei
timeline.go4.87 kB
package core import ( "fmt" "sync" "time" "github.com/emicklei/melrose/notify" ) // Timeline is a chain of events that are placed in the future (playing). type Timeline struct { head *scheduledTimelineEvent // earliest tail *scheduledTimelineEvent // latest protection sync.RWMutex isPlaying bool resume chan bool } // NewTimeline creates a new Timeline. func NewTimeline() *Timeline { return &Timeline{ protection: sync.RWMutex{}, } } // TimelineEvent describes an event that can be scheduled on a Timeline. type TimelineEvent interface { Handle(tim *Timeline, when time.Time) NoteChangesDo(block func(NoteChange)) } type scheduledTimelineEvent struct { event TimelineEvent when time.Time next *scheduledTimelineEvent } var ( wait = 50 * time.Millisecond // 1/16 note @ bpm 300 ) // Len returns the current number of scheduled events. func (t *Timeline) Len() int64 { t.protection.RLock() defer t.protection.RUnlock() var count int64 here := t.head for here != nil { here = here.next count++ } return count } // Play runs a loop to handle all the events in time. This is blocking. func (t *Timeline) Play() { t.resume = make(chan bool) t.isPlaying = true for { t.protection.RLock() here := t.head t.protection.RUnlock() if here == nil { <-t.resume continue } now := time.Now() for now.After(here.when) { here.event.Handle(t, now) t.protection.Lock() t.head = t.head.next here = t.head t.protection.Unlock() if here == nil { break } } if here != nil { untilNext := here.when.Sub(now) if wait < untilNext { time.Sleep(wait) // 1/16 note } else { time.Sleep(untilNext) // < 1/16 note } } } } // Reset forgets about all scheduled calls. func (t *Timeline) Reset() { if notify.IsDebug() { notify.Debugf("core.timeline: flushing all scheduled MIDI events") } t.protection.Lock() defer t.protection.Unlock() t.head = nil t.tail = nil } // Schedule adds an event for a given time func (t *Timeline) Schedule(event TimelineEvent, when time.Time) error { now := time.Now() diff := when.Sub(now) if diff < -wait { return fmt.Errorf("core.timeline: cannot schedule in the past:%v", now.Sub(when)) } t.schedule(&scheduledTimelineEvent{ when: when, event: event, }) return nil } // schedule adds an event on the chain. // pre: event.when >= now func (t *Timeline) schedule(event *scheduledTimelineEvent) { t.protection.Lock() if t.head == nil { t.head = event t.tail = event // before resume otherwise run loop will deadlock t.protection.Unlock() if t.isPlaying { t.resume <- true } return } defer t.protection.Unlock() if event.when.After(t.tail.when) { // event is after tail, new tail t.tail.next = event t.tail = event return } if t.head.when.After(event.when) { // event is before head, new head event.next = t.head t.head = event return } if t.head.next == nil { // event on the same time as head, put it after! head t.head.next = event t.tail = event return } if t.tail.when.Equal(event.when) { // event on the same time as tail, put it after! tail t.tail.next = event t.tail = event return } // somewhere between head and tail previous := t.head here := t.head.next for event.when.After(here.when) { previous = here here = here.next } // here is after event, it must be scheduled before it previous.next = event event.next = here } // EventsDo visits all scheduled events and calls the block for each. func (t *Timeline) EventsDo(block func(event TimelineEvent, when time.Time)) { t.protection.Lock() defer t.protection.Unlock() here := t.head for here != nil { block(here.event, here.when) here = here.next } } // ZeroStarting returns a new one in which all events are shifted back in time starting at time 0. func (t *Timeline) ZeroStarting() *Timeline { if t.Len() == 0 { return t } result := NewTimeline() zero := time.Time{} t.EventsDo(func(event TimelineEvent, when time.Time) { d := when.Sub(t.head.when) result.schedule(&scheduledTimelineEvent{ when: zero.Add(d), event: event, }) }) return result } func (t *Timeline) NoteEvents() (list []NoteEvent) { activeNotes := map[int64]NoteEvent{} t.EventsDo(func(event TimelineEvent, when time.Time) { event.NoteChangesDo(func(change NoteChange) { if change.isOn { _, ok := activeNotes[change.note] if ok { // note was on ? // TODO warn? } else { // new activeNotes[change.note] = NoteEvent{Start: when, Number: change.Number(), Velocity: change.Velocity()} } } else { // note off hit, ok := activeNotes[change.note] if !ok { // note was never on ? // TODO warn? } else { list = append(list, hit.WithEnd(when)) delete(activeNotes, change.note) } } }) }) return }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/emicklei/melrose'

If you have feedback or need assistance with the MCP directory API, please join our Discord server