Skip to main content
Glama
emicklei

melrōse musical expression player

by emicklei
note.go10.4 kB
package core import ( "bytes" "fmt" "io" "strings" "time" ) // Note represents a musical note. // Notations: // // 2.C#3 = half duration, pitch C, sharp, octave 3, velocity default // D = quarter duration, pitch D, octave 4, no accidental // 8B_ = eighth duration, pitch B, octave 4, flat // = = quarter rest // -/+ = velocity number // // http://en.wikipedia.org/wiki/Musical_Note type Note struct { Name string // {C D E F G A B = ^ < >} Octave int Accidental int // -1 Flat, +1 Sharp, 0 Normal Dotted bool // if true then fraction is increased by half Velocity int // 1..127 fraction float32 // {0.03175,0.0625,0.125,0.25,0.5,1} duration time.Duration // if set then this overrides Dotted and fraction tied []Note // succeeding identical notes that are tied to this ; mostly empty } func (n Note) Equals(o Note) bool { return n.Name == o.Name && n.Octave == o.Octave && n.Accidental == o.Accidental && n.Dotted == o.Dotted && n.Velocity == o.Velocity && n.fraction == o.fraction && n.duration == o.duration && n.HasEqualTied(o) } func (n Note) HasEqualTied(o Note) bool { if len(n.tied) != len(o.tied) { return false } for t := range n.tied { if !n.tied[t].Equals(o.tied[t]) { return false } } return true } func (n Note) Storex() string { return fmt.Sprintf("note('%s')", n.String()) } // ToNote() is part of NoteConvertable func (n Note) ToNote() (Note, error) { return n, nil } func (n Note) Fraction() float32 { return n.fraction } func (n Note) ToRest() Note { return Note{ Name: "=", Octave: n.Octave, Accidental: n.Accidental, Dotted: n.Dotted, Velocity: n.Velocity, fraction: n.fraction, duration: n.duration, } } // Replaced is part of Replaceable func (n Note) Replaced(from, to Sequenceable) Sequenceable { if IsIdenticalTo(from, n) { return to } return n } var ( Rest4 = Note{Name: "=", fraction: 0.25} PedalUpDown = Note{Name: "^", fraction: 0} PedalDown = Note{Name: ">", fraction: 0} PedalUp = Note{Name: "<", fraction: 0} ZeroDuration = time.Duration(0) ) func NewNote(name string, octave int, frac float32, accidental int, dot bool, velocity int) (Note, error) { if len(name) != 1 { return Rest4, fmt.Errorf("note must be one character, got [%s]", name) } // pedal check switch name { case "^": return PedalUpDown, nil case ">": return PedalDown, nil case "<": return PedalUp, nil } if !strings.Contains(allowedNoteNames, name) { return Rest4, fmt.Errorf("invalid note name [%s]:%s", allowedNoteNames, name) } switch frac { case 0.03175: case 0.0625: case 0.125: case 0.25: case 0.5: case 1: default: return Rest4, fmt.Errorf("invalid fraction [1,0.5,0.25,0.125,0.0625,0.03175]:%v", frac) } if accidental != 0 && accidental != -1 && accidental != 1 { return Rest4, fmt.Errorf("invalid accidental: %d", accidental) } return Note{Name: name, Octave: octave, fraction: frac, Accidental: accidental, Dotted: dot, Velocity: velocity}, nil } func MakeNote(name string, octave int, frac float32, accidental int, dot bool, velocity int) Note { return Note{Name: name, Octave: octave, fraction: frac, Accidental: accidental, Dotted: dot, Velocity: velocity} } func (n Note) IsRest() bool { return Rest4.Name == n.Name } func (n Note) IsPedalUp() bool { return PedalUp.Name == n.Name } func (n Note) IsPedalDown() bool { return PedalDown.Name == n.Name } func (n Note) IsPedalUpDown() bool { return PedalUpDown.Name == n.Name } func (n Note) IsPedal() bool { return PedalUpDown.Name == n.Name || PedalDown.Name == n.Name || PedalUp.Name == n.Name } // DurationFactor is the actual duration time factor // Only correct if n.duration is 0 and also for each tied note ; use DurationAt otherwise func (n Note) DurationFactor() float32 { f := n.fraction if n.Dotted { f *= 1.5 } for _, each := range n.tied { f += each.DurationFactor() } return f } func (n Note) DurationAt(bpm float64) time.Duration { if n.duration > 0 { sum := n.duration for _, each := range n.tied { sum += each.DurationAt(bpm) } return sum } return time.Duration(float32(WholeNoteDuration(bpm)) * n.DurationFactor()) } func (n Note) S() Sequence { return BuildSequence([]Note{n}) } func (n Note) WithDynamic(emphasis string) Note { return n.WithVelocity(ParseVelocity(emphasis)) } func (n Note) WithoutDynamic() Note { return n.WithVelocity(Normal) } func (n Note) WithVelocity(v int) Note { n.Velocity = v if len(n.tied) == 0 { return n } // handle tied notes t := make([]Note, len(n.tied)) for i := 0; i < len(n.tied); i++ { t[i] = n.tied[i].WithVelocity(v) } n.tied = t return n } func (n Note) WithFraction(f float32, dotted bool) Note { // TODO if f == 0.5*1.5 { n.fraction = 0.5 n.Dotted = true return n } if f == 0.25*1.5 { n.fraction = 0.25 n.Dotted = true return n } if f == 0.125*1.5 { n.fraction = 0.125 n.Dotted = true return n } if f == 0.0625*1.5 { n.fraction = 0.0625 n.Dotted = true return n } if f == 0.03175*1.5 { n.fraction = 0.03175 n.Dotted = true return n } n.fraction = f n.Dotted = dotted if len(n.tied) == 0 { return n } // handle tied notes t := make([]Note, len(n.tied)) for i := 0; i < len(n.tied); i++ { t[i] = n.tied[i].WithFraction(f, dotted) } n.tied = t return n } func (n Note) WithTiedNote(t Note) Note { n.tied = append(n.tied, t) return n } func (n Note) IsHearable() bool { return strings.ContainsAny(n.Name, "ABCDEFG") } // MustParseNote returns a Note by parsing the input. Panic if it fails. func MustParseNote(input string) Note { n, err := ParseNote(input) if err != nil { panic("MustParseNote failed:" + err.Error()) } return n } var N = MustParseNote // ParseNote reads the format <(inverse-)duration?>[CDEFGA=<^>]<accidental?><dot?><octave?> func ParseNote(input string) (Note, error) { return newFormatParser(input).parseNote() } func ParseVelocity(plusmin string) (velocity int) { switch plusmin { case "--": velocity = VelocityP case "---": velocity = VelocityPP case "----": velocity = VelocityPPP case "-----": velocity = VelocityPPPP case "++": velocity = VelocityF case "+++": velocity = VelocityFF case "++++": velocity = VelocityFFF case "+++++": velocity = VelocityFFFF case "o": velocity = Normal case "-": velocity = VelocityMP case "+": velocity = VelocityMF default: // invalid velocity = -1 } return } // Formatting func (n Note) accidentalf(encoded bool) string { if n.Accidental == -1 { if encoded { return "b" } else { return "_" } } if n.Accidental == 1 { if encoded { return "#" } else { return "#" } } return "" } func (n Note) NonFractionBasedDuration() (time.Duration, bool) { if n.duration > 0 { return n.duration, true } return ZeroDuration, false } func FractionToString(f float32) string { switch f { case 0.03175: return "32" case 0.0625: return "16" case 0.125: return "8" case 0.25: return "4" case 0.5: return "2" case 1.0: return "1" } return "" } func (n Note) CheckTieableTo(t Note) error { if n.Name != t.Name { return fmt.Errorf("note name mismatch, got [%s] want [%s]", t.Name, n.Name) } if n.Octave != t.Octave { return fmt.Errorf("note octave mismatch, got [%d] want [%d]", t.Octave, n.Octave) } if n.Accidental != t.Accidental { return fmt.Errorf("note accidental mismatch, got [%d] want [%d]", t.Accidental, n.Accidental) } if n.Velocity != t.Velocity { return fmt.Errorf("note velocity mismatch, got [%d] want [%d]", t.Velocity, n.Velocity) } return nil } func (n Note) Inspect(i Inspection) { i.Properties["length"] = n.DurationFactor() i.Properties["midi"] = n.MIDI() i.Properties["velocity"] = n.Velocity i.Properties["duration"] = n.DurationAt(i.Context.Control().BPM()) } func (n Note) String() string { var buf bytes.Buffer n.printOn(&buf, PrintAsSpecified) return buf.String() } func (n Note) printOn(buf *bytes.Buffer, sharpOrFlatKey int) { if n.IsPedalUp() { buf.WriteString(PedalUp.Name) return } if n.IsPedalDown() { buf.WriteString(PedalDown.Name) return } if n.IsPedalUpDown() { buf.WriteString(PedalUpDown.Name) return } if n.fraction != 0.25 { buf.WriteString(FractionToString(n.fraction)) } if n.Dotted { buf.WriteString(".") } if n.IsRest() { buf.WriteString(n.Name) return } if Sharp == sharpOrFlatKey && n.Accidental == -1 { // want Sharp, specified in Flat buf.WriteString(n.Pitched(-1).Name) buf.WriteString("#") } else if Flat == sharpOrFlatKey && n.Accidental == 1 { // want Flat, specified in Sharp buf.WriteString(n.Pitched(1).Name) buf.WriteString("_") } else { // PrintAsSpecified buf.WriteString(n.Name) if n.Accidental != 0 { buf.WriteString(n.accidentalf(false)) } } if n.Octave != 4 { fmt.Fprintf(buf, "%d", n.Octave) } if n.Velocity != Normal { io.WriteString(buf, VelocityToDynamic(n.Velocity)) } if len(n.tied) > 0 { for _, each := range n.tied { io.WriteString(buf, "~") each.printOn(buf, sharpOrFlatKey) } } } func VelocityToDynamic(v int) string { if v == Normal { return "" } switch { case v <= VelocityPPPP: return "-----" case v <= VelocityPPP: return "----" case v <= VelocityPP: return "---" case v <= VelocityP: return "--" case v <= VelocityMP: return "-" case v <= Normal: case v <= VelocityMF: return "+" case v <= VelocityF: return "++" case v <= VelocityFF: return "+++" case v <= VelocityFFF: return "++++" case v > VelocityFFF: return "+++++" } return "" } var fractionRanges = []struct { fraction float32 dotted bool }{ {0.03175, false}, // 1/32 {0.03175 * 1.5, true}, {0.0625, false}, // 1/16 {0.09375, true}, {0.125, false}, {0.1875, true}, {0.25, false}, {0.375, true}, {0.5, false}, {0.75, true}, {1.0, false}, {1.5, true}, {2.0, false}, // non-exist } func QuantizeFraction(durationFactor float32) (fraction float32, dotted bool, ok bool) { last := float32(0.0) for i := 0; i < len(fractionRanges); i++ { next := fractionRanges[i] halfway := (last + next.fraction) / 2.0 if durationFactor <= halfway { if i == 0 { return 0.0, false, false } prev := fractionRanges[i-1] return prev.fraction, prev.dotted, true } last = next.fraction } return 0.0, false, false }

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