Skip to main content
Glama
emicklei

melrōse musical expression player

by emicklei
sequence_builder.go4.12 kB
package core import ( "math" "time" ) type NoteChangeEvent struct { When int64 `json:"when"` IsOn bool `json:"ison"` Note int64 `json:"note"` Velocity int64 `json:"velocity"` } func (t *Timeline) toNoteChangeEvents() (changes []NoteChangeEvent) { t.EventsDo(func(each TimelineEvent, when time.Time) { change, ok := each.(NoteChange) if !ok { return } store := NoteChangeEvent{ When: when.UnixNano(), IsOn: change.isOn, Note: change.note, Velocity: change.velocity, } changes = append(changes, store) }) return } func (t *Timeline) BuildNotePeriods() []NotePeriod { events := t.toNoteChangeEvents() periods := ConvertToNotePeriods(events) return periods } type NotePeriod struct { startMs, endMs int64 number int velocity int } func (p NotePeriod) Start() time.Time { return time.Unix(0, p.startMs*1e6) // ms -> nano } func (p NotePeriod) End() time.Time { return time.Unix(0, p.endMs*1e6) // ms -> nano } func (p NotePeriod) Number() int { return p.number } func (p NotePeriod) Velocity() int { return p.velocity } func (p NotePeriod) Note(bpm float64) Note { // TODO assume duration is <= whole note sixteenth := 4 * 60 * 1000 / bpm / 16 times := float64(p.endMs-p.startMs) / sixteenth fraction, dotted := FractionToDurationParts(times * 0.0625) // 1/16 name, octave, accidental := MIDIToNoteParts(p.number) n, _ := NewNote(name, octave, fraction, accidental, dotted, p.velocity) return n } func (p NotePeriod) Quantized(bpm float64) NotePeriod { // snap the start to a multiple of 16th note duration for bpm // snap the length too sixteenth := 4 * 60 * 1000 / bpm / 16 startMs := nearest(p.startMs, sixteenth) endMs := nearest(p.endMs, sixteenth) return NotePeriod{startMs: startMs, endMs: endMs, number: p.number, velocity: p.velocity} } func nearest(value int64, delta float64) int64 { vf := float64(value) times := math.Round(vf / delta) return int64(times * delta) } // TODO move inside sequencebuilder ? func ConvertToNotePeriods(changes []NoteChangeEvent) (events []NotePeriod) { noteOn := map[int64]NoteChangeEvent{} // which note started when var begin int64 = 0 for _, each := range changes { if each.IsOn { noteOn[each.Note] = each } else { on, ok := noteOn[each.Note] if !ok { continue } delete(noteOn, each.Note) if begin == 0 { begin = on.When } start := (on.When - begin) / 1e6 // to milliseconds event := NotePeriod{ startMs: start, endMs: (each.When - begin) / 1e6, // to milliseconds number: int(each.Note), velocity: int(on.Velocity), } events = append(events, event) } } return } type SequenceBuilder struct { periods []NotePeriod // sorted by startMS, ascending noteGroups [][]Note bpm float64 // max 300 } func NewSequenceBuilder(periods []NotePeriod, bpm float64) *SequenceBuilder { return &SequenceBuilder{ periods: periods, noteGroups: [][]Note{}, bpm: bpm, } } func (s *SequenceBuilder) Build() Sequence { quantized := []NotePeriod{} for _, each := range s.periods { quantized = append(quantized, each.Quantized(s.bpm)) } whole := WholeNoteDuration(s.bpm).Milliseconds() group := []Note{} lastStartMs := int64(-1) lastEndMs := int64(-1) for _, each := range quantized { if lastStartMs == -1 { lastStartMs = each.startMs lastEndMs = each.endMs group = append(group, each.Note(s.bpm)) continue } if lastStartMs == each.startMs { group = append(group, each.Note(s.bpm)) if each.endMs > lastEndMs { lastEndMs = each.endMs } continue } s.noteGroups = append(s.noteGroups, group) // add zero or more rest notes for the gap fraction, dotted := FractionToDurationParts(float64(each.startMs-lastEndMs) / float64(whole)) rest, _ := NewNote("=", 4, fraction, 0, dotted, 0) s.noteGroups = append(s.noteGroups, []Note{rest}) group = []Note{each.Note(s.bpm)} lastStartMs = each.startMs lastEndMs = each.endMs } if len(group) > 0 { s.noteGroups = append(s.noteGroups, group) } return Sequence{ Notes: s.noteGroups, } }

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