Skip to main content
Glama
emicklei

melrōse musical expression player

by emicklei
output_device.go6.25 kB
package midi import ( "time" "github.com/emicklei/melrose/core" "github.com/emicklei/melrose/midi/transport" "github.com/emicklei/melrose/notify" ) type OutputDevice struct { id int name string stream transport.MIDIOut defaultChannel int echo bool timeline *core.Timeline } func NewOutputDevice(id int, out transport.MIDIOut, ch int, line *core.Timeline) *OutputDevice { return &OutputDevice{ id: id, stream: out, defaultChannel: ch, echo: false, timeline: line, } } func (d *OutputDevice) Start() { go d.timeline.Play() } func (d *OutputDevice) Reset() { defer func() { if err := recover(); err != nil { notify.Warnf("reset failed for device:%v", d.id) } }() d.timeline.Reset() if notify.IsDebug() { notify.Debugf("device.%d: sending Note OFF to all 16 channels", d.id) } if d.stream != nil { // send note off all to all channels for current device for c := 1; c <= 16; c++ { if err := d.stream.WriteShort(controlChange|int64(c-1), noteAllOff, 0); err != nil { notify.Console.Errorf("device.%d: midi write error:%v", d.id, err) } } } } func (d *OutputDevice) handledPedalChange(condition core.Condition, channel int, timeline *core.Timeline, moment time.Time, group []core.Note) bool { if len(group) == 0 || len(group) > 1 { return false } note := group[0] switch { case note.IsPedalUp(): timeline.Schedule(pedalEvent{ goingDown: false, channel: channel, out: d.stream, mustHandle: condition}, moment) return true case note.IsPedalUpDown(): timeline.Schedule(pedalEvent{ goingDown: false, channel: channel, out: d.stream, mustHandle: condition}, moment) timeline.Schedule(pedalEvent{ goingDown: true, channel: channel, out: d.stream, mustHandle: condition}, moment) return true case note.IsPedalDown(): timeline.Schedule(pedalEvent{ goingDown: true, channel: channel, out: d.stream, mustHandle: condition}, moment) return true } return false } func (d *OutputDevice) Play(condition core.Condition, seq core.Sequenceable, bpm float64, beginAt time.Time) time.Time { // which channel? channel := d.defaultChannel if sel, ok := seq.(core.ChannelSelector); ok { channel = sel.Channel() seq = sel.Unwrap() } // schedule all notes of the sequenceable wholeNoteDuration := core.WholeNoteDuration(bpm) moment := beginAt for _, eachGroup := range seq.S().Notes { if len(eachGroup) == 0 { continue } // pedal if d.handledPedalChange(condition, channel, d.timeline, moment, eachGroup) { continue } // one note if len(eachGroup) == 1 { moment = scheduleOneNote(d, condition, channel, eachGroup[0], wholeNoteDuration, moment) continue } // more than one note if canCombineEvent(eachGroup) { event := combinedMidiEvent(d.id, channel, eachGroup, d.stream) if d.echo { event.echoString = core.StringFromNoteGroup(eachGroup) } actualDuration := durationOfGroup(eachGroup, wholeNoteDuration) event.mustHandle = condition moment = scheduleOnOffEvents(d, event, actualDuration, moment) continue } // not combinable group of more than one note earliest := moment.Add(1 * time.Hour) for _, each := range eachGroup { endTime := scheduleOneNote(d, condition, channel, each, wholeNoteDuration, moment) if endTime.Before(earliest) { earliest = endTime } } moment = earliest } return moment } // returns the longest TODO in core? func durationOfGroup(notes []core.Note, whole time.Duration) time.Duration { longest := time.Duration(0) for _, each := range notes { eachDuration := time.Duration(float32(whole) * each.DurationFactor()) if eachDuration > longest { longest = eachDuration } } return longest } func scheduleOneNote(device *OutputDevice, condition core.Condition, channel int, note core.Note, whole time.Duration, moment time.Time) time.Time { if note.IsRest() { event := restEvent{mustHandle: condition} if device.echo { event.echoString = note.String() } device.timeline.Schedule(event, moment) actualDuration := time.Duration(float32(whole) * note.DurationFactor()) return moment.Add(actualDuration) } // midi variable length note? if fixed, ok := note.NonFractionBasedDuration(); ok { event := midiEvent{ which: []int64{int64(note.MIDI())}, onoff: noteOn, device: device.id, channel: channel, velocity: int64(note.Velocity), out: device.stream, mustHandle: condition, } if device.echo { event.echoString = note.String() } return scheduleOnOffEvents(device, event, fixed, moment) } // normal note event := midiEvent{ which: []int64{int64(note.MIDI())}, onoff: noteOn, device: device.id, channel: channel, velocity: int64(note.Velocity), out: device.stream, mustHandle: condition, } if device.echo { event.echoString = note.String() } actualDuration := time.Duration(float32(whole) * note.DurationFactor()) return scheduleOnOffEvents(device, event, actualDuration, moment) } func scheduleOnOffEvents(device *OutputDevice, event midiEvent, duration time.Duration, at time.Time) time.Time { device.timeline.Schedule(event, at) moment := at.Add(duration) device.timeline.Schedule(event.asNoteoff(), moment) return moment } func canCombineEvent(notes []core.Note) bool { if len(notes) <= 1 { return true } dur, vel := notes[0].DurationFactor(), notes[0].Velocity for n := 1; n < len(notes); n++ { d, v := notes[n].DurationFactor(), notes[n].Velocity if d != dur || v != vel { return false } } return true } // Pre: notes not empty func combinedMidiEvent(deviceID int, channel int, notes []core.Note, stream transport.MIDIOut) midiEvent { // first note makes fraction and velocity velocity := notes[0].Velocity if velocity > 127 { velocity = 127 } if velocity < 1 { velocity = core.Normal } nrs := []int64{} for _, each := range notes { nrs = append(nrs, int64(each.MIDI())) } return midiEvent{ which: nrs, onoff: noteOn, device: deviceID, channel: channel, velocity: int64(velocity), out: stream, } }

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