Skip to main content
Glama
emicklei

melrōse musical expression player

by emicklei
write.go5.33 kB
package file import ( "bufio" "encoding/binary" "fmt" "io" "math" "os" "time" "github.com/emicklei/melrose/core" "github.com/emicklei/melrose/notify" "github.com/emicklei/melrose/op" "github.com/Try431/EasyMIDI/smf" "github.com/Try431/EasyMIDI/smfio" ) const ticksPerBeat uint16 = 960 func Export(fileName string, m any, bpm float64, biab int) error { // Save to new midi source file outputMidi, err := os.Create(fileName) if err != nil { return err } defer outputMidi.Close() notify.Infof("exporting multi-track to [%s] ...", fileName) return ExportOn(outputMidi, m, bpm, biab) } // Export creates (overwrites) a SMF multi-track Midi file func ExportOn(w io.Writer, m any, bpm float64, biab int) error { if mt, ok := m.(core.MultiTrack); ok { return exportMultiTrack(w, mt, bpm, biab) } if seq, ok := m.(core.Sequenceable); ok { return exportSequence(seq, w, bpm, biab) } if lp, ok := m.(*core.Loop); ok { return exportSequence(lp.ToSequence(4), w, bpm, biab) // TODO 4 can be setting } return fmt.Errorf("cannot MIDI export a (%T)", m) } func exportSequence(seq core.Sequenceable, w io.Writer, bpm float64, biab int) error { t := core.NewTrack("melrōse-track", 1) t.Add(core.NewSequenceOnTrack(core.On(1), seq)) mt := core.MultiTrack{Tracks: []core.HasValue{core.On(t)}} return exportMultiTrack(w, mt, bpm, biab) } func createMidiTrack(t *core.Track, bpm float64, biab int) (*smf.Track, error) { // Create track struct track := new(smf.Track) // https://www.recordingblogs.com/wiki/midi-set-tempo-meta-message // time = 10000 * (500ms / 960) ~ 5.2 sec quarterMS := quarterUSFromBPM(bpm) tempoData := make([]byte, 4) binary.BigEndian.PutUint32(tempoData, quarterMS) tempo, err := smf.NewMetaEvent(0, smf.MetaSetTempo, tempoData[1:]) // take 3 bytes only if err != nil { return nil, err } err = track.AddEvent(tempo) if err != nil { return nil, err } // All the notes wholeNoteDuration := time.Duration(int(math.Round(4*60*1000/bpm))) * time.Millisecond // 4 = signature TODO create func var moment time.Duration var lastTicks uint32 = 0 for _, group := range buildSequenceFromTrack(t, biab).Notes { if len(group) == 0 { continue } channel := uint8(0x00) actualDuration := time.Duration(float32(wholeNoteDuration) * group[0].DurationFactor()) if group[0].IsRest() { //log.Println("rest", moment) moment = moment + actualDuration continue } absoluteTicks := ticksFromDuration(moment, quarterMS) //log.Println("on", moment) for i, each := range group { var deltaTicks uint32 = 0 if i == 0 { deltaTicks = absoluteTicks - lastTicks } noteOn, err := smf.NewMIDIEvent(deltaTicks, smf.NoteOnStatus, channel, uint8(each.MIDI()), uint8(each.Velocity)) if err != nil { return nil, err } err = track.AddEvent(noteOn) if err != nil { return nil, err } } lastTicks = absoluteTicks moment = moment + actualDuration //log.Println("off", moment) absoluteTicks = ticksFromDuration(moment, quarterMS) for i, each := range group { var deltaTicks uint32 = 0 if i == 0 { deltaTicks = absoluteTicks - lastTicks } noteOff, err := smf.NewMIDIEvent(deltaTicks, smf.NoteOffStatus, channel, uint8(each.MIDI()), 0x00) // zero velocity if err != nil { return nil, err } err = track.AddEvent(noteOff) if err != nil { return nil, err } } lastTicks = absoluteTicks } // Track end endTrack, err := smf.NewMetaEvent(0, smf.MetaEndOfTrack, []byte{}) if err != nil { return nil, err } err = track.AddEvent(endTrack) if err != nil { return nil, err } notify.Infof("wrote track [%s] of [%v] with [%d] MIDI events", t.Title, moment, track.Len()) return track, nil } func exportMultiTrack(w io.Writer, m core.MultiTrack, bpm float64, biab int) error { // Create division // https://www.recordingblogs.com/wiki/time-division-of-a-midi-file division, err := smf.NewDivision(ticksPerBeat, smf.NOSMTPE) if err != nil { return err } // Create new midi struct midi, err := smf.NewSMF(smf.Format1, *division) if err != nil { return err } for i, eachVal := range m.Tracks { if each, ok := eachVal.Value().(*core.Track); ok { // Create track struct track, err := createMidiTrack(each, bpm, biab) if err != nil { return err } // Add track to new midi struct err = midi.AddTrack(track) if err != nil { return err } } else { return fmt.Errorf("multi track contains non-track at [%d] (%T)", i+1, eachVal.Value()) } } // Create buffering stream writer := bufio.NewWriter(w) if err := smfio.Write(writer, midi); err != nil { return err } return writer.Flush() } func ticksFromDuration(dur time.Duration, quarterUSFromBPM uint32) uint32 { us := dur.Microseconds() f := float64(us) / float64(quarterUSFromBPM) * float64(ticksPerBeat) return uint32(math.Round(f)) } // duration in microseconds of one quarter note func quarterUSFromBPM(bpm float64) uint32 { // 120 bpm -> 500000 usec/quarter note return uint32(60000000.0 / bpm) } func buildSequenceFromTrack(t *core.Track, biab int) core.Sequence { target := []core.Sequenceable{} for bar, seq := range t.Content { each := core.RestSequence(bar-1, biab).SequenceJoin(seq.S()) target = append(target, each) } return op.Merge{Target: target}.S() }

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