diff --git a/.gitignore b/.gitignore index 30202d1..49cf594 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,9 @@ mscz/ *.db *.zip -dist/ \ No newline at end of file +dist/ + +# audio +*.mp3 +*.mid +*.midi \ No newline at end of file diff --git a/README.md b/README.md index 6d94fe1..a9845d7 100644 --- a/README.md +++ b/README.md @@ -79,4 +79,6 @@ Development pauses occasionally when I research better automation approaches - https://www.musicca.com/dictionary/scales - https://www.gkiharapanindah.org/download/rekap-kidung-jemaat/ - https://mymusictheory.com/more-music-theory-topics/key-signatures-chart/ -- https://fontdrop.info/#/?darkmode=true \ No newline at end of file +- https://fontdrop.info/#/?darkmode=true +- Original MIDI Audio Arrangement by GEMA/SABDA (sabda.org). Modified for single-verse playback. +- Sound Effect by freesound_community from Pixabay \ No newline at end of file diff --git a/cmd/lab/audio-stream/stream/audio_stream.go b/cmd/lab/audio-stream/stream/audio_stream.go new file mode 100644 index 0000000..2d414b5 --- /dev/null +++ b/cmd/lab/audio-stream/stream/audio_stream.go @@ -0,0 +1,174 @@ +package stream + +import ( + "io" + "log" + "net/http" + "os" + "strconv" + "sync" + "time" + + "github.com/hcl/audioduration" + "github.com/julienschmidt/httprouter" +) + +type AudioStream struct { + Sig chan os.Signal +} + +func (dh *AudioStream) ServeHTTP(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + + paths := []string{ + `/home/jodiivan/go/src/github.com/jodi-ivan/numbered-notation-xml/files/audio/kj-001-opening.mp3`, + `/home/jodiivan/go/src/github.com/jodi-ivan/numbered-notation-xml/files/audio/kj-001-core.mp3`, + } + + openingFile, err := os.Open(paths[0]) + if err != nil { + log.Println("Failed to open openingFile", err.Error()) + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + + defer openingFile.Close() + + openingDuration, err := audioduration.Mp3(openingFile) + if err != nil { + log.Println("Failed to calculate mp3 opening duration", err.Error()) + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + + coreFile, err := os.Open(paths[1]) + if err != nil { + log.Println("Failed to open coreFile", err.Error()) + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + + defer coreFile.Close() + + repeatRaw := r.FormValue("repeat") + + repeat, err := strconv.Atoi(repeatRaw) + if repeatRaw != "" && err != nil { + log.Printf("[ServeHTTP] invalid verse: %v", err.Error()) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Invalid URL")) + return + } + + if repeat == 1 { + repeat = 0 + } + + w.Header().Set("Connection", "Keep-Alive") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Transfer-Encoding", "chunked") + w.Header().Set("Content-Type", "audio/mpeg") + + ctx := r.Context() + currRepeat := 1 + measure := 0 + measureMtx := sync.Mutex{} + + songMeasure := 23.0 + songTempo := 85.0 + songDuration := ((songMeasure * 4) / songTempo) * 60.0 + op := time.Duration(openingDuration * float64(time.Second)) + core := time.Duration(songDuration * float64(time.Second)) // for 1 timesignature no changes + log.Println("wait for ", op.String()) + + files := []*os.File{ + openingFile, + coreFile, + } + go func() { + + <-time.After(op) + measure = 1 + log.Println("Start: Measure ", measure) + ticker := time.NewTicker(core / 23) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + measureMtx.Lock() + measure++ + measureMtx.Unlock() + log.Println("Measure ", measure) + if measure == 23 { + return + } + case <-dh.Sig: + log.Println("os signal interrupt") + return + } + } + + }() + + for i, file := range files { + + buf := make([]byte, 4096) // 4KB chunks + + // Loop core indefinitely + for { + select { + case <-ctx.Done(): + log.Println("context timeout") + + return + case <-dh.Sig: + log.Println("os signal interrupt") + return + default: + } + + n, err := file.Read(buf) + if err != nil { + if err == io.EOF { + log.Println("Music ", file.Name(), " is finished") + // if i == 1 && currRepeat == repeat { + // break + // } + measureMtx.Lock() + measure = 0 + measureMtx.Unlock() + + if i == 0 || ((i == 1 && repeat == 0) || (i == 1 && currRepeat == repeat)) { + break + } else if currRepeat < repeat { + currRepeat++ + file.Seek(0, 0) + continue + } + } + log.Println("Failed to ", err.Error()) + return + } + + _, werr := w.Write(buf[:n]) + if werr != nil { + log.Println("Failed to ", werr.Error()) + return + } + + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + + } + } + + // wg.Wait() + + log.Println("It is done") + +} diff --git a/cmd/rest/app.go b/cmd/rest/app.go index 6d73be7..e70ad07 100644 --- a/cmd/rest/app.go +++ b/cmd/rest/app.go @@ -8,6 +8,7 @@ import ( "syscall" "github.com/jodi-ivan/numbered-notation-xml/adapter" + strmadapter "github.com/jodi-ivan/numbered-notation-xml/cmd/lab/audio-stream/stream" lab "github.com/jodi-ivan/numbered-notation-xml/cmd/rest/adapter" "github.com/jodi-ivan/numbered-notation-xml/decorator" "github.com/jodi-ivan/numbered-notation-xml/internal/renderer" @@ -63,7 +64,7 @@ func main() { Db: db, }) - sigs := make(chan os.Signal, 1) + sigs := make(chan os.Signal, 2) ws.Register("GET", "/internal/diagnostic/verse/:scope", &adapter.DiagnosticHTTP{ Usecase: usecaseMod, @@ -71,6 +72,10 @@ func main() { Repo: repo, }) + ws.Register("GET", "/internal/v1/lab", &strmadapter.AudioStream{ + Sig: sigs, + }) + err = ws.Serve(cfg.Webserver.Port) if err != nil { log.Printf("Failed to start the server. Err: %s", err.Error()) diff --git a/files/audio/.gitkeep b/files/audio/.gitkeep new file mode 100644 index 0000000..39bec3c --- /dev/null +++ b/files/audio/.gitkeep @@ -0,0 +1 @@ +put your *.mp3 file \ No newline at end of file diff --git a/files/var/www/html/stream-test.html b/files/var/www/html/stream-test.html new file mode 100644 index 0000000..7a9f7ba --- /dev/null +++ b/files/var/www/html/stream-test.html @@ -0,0 +1,242 @@ + + + + + + + FM Stream Player + + + + +
+

🎙️ FM Stream Player

+ +
+ + + +
+ +
+ +
+ + Disconnected +
+
+
+
+ + + + + \ No newline at end of file diff --git a/go.mod b/go.mod index f7de75e..1d3a665 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/jodi-ivan/numbered-notation-xml -go 1.18 +go 1.23 require ( github.com/JoshVarga/svgparser v0.0.0-20200804023048-5eaba627a7d1 @@ -8,7 +8,6 @@ require ( github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 github.com/golang/mock v1.6.0 github.com/google/uuid v1.6.0 - github.com/jarcoal/httpmock v1.3.1 github.com/jmoiron/sqlx v1.3.5 github.com/julienschmidt/httprouter v1.3.0 github.com/mattn/go-sqlite3 v1.14.18 @@ -18,10 +17,12 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hcl/audioduration v0.0.0-20221028095105-c8039191ae43 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.2 // indirect golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect golang.org/x/text v0.3.3 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 78c1ea6..9be7dd0 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= -github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/hcl/audioduration v0.0.0-20221028095105-c8039191ae43 h1:rXo3ryzLSBpdTeoDKzd1Iz9eDO33M6mcfLFIOOQCqHw= +github.com/hcl/audioduration v0.0.0-20221028095105-c8039191ae43/go.mod h1:ATjLP2ak34LrSbG6oRxL1dO+yNwZnhSPSNxnlEuVwYM= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= @@ -27,7 +27,6 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -67,6 +66,8 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= diff --git a/internal/lyric/coloring.go b/internal/lyric/coloring.go index abc0973..f72f643 100644 --- a/internal/lyric/coloring.go +++ b/internal/lyric/coloring.go @@ -16,7 +16,7 @@ func getColoringStyle(ctx context.Context, verse, totalLyric int) string { return coloringOpacity[0] } - if prm.Verse < 2 || prm.SingleVerseMode { + if prm.Verse < 2 || (totalLyric == 1 && prm.Verse >= 2 && verse == 0) || prm.SingleVerseMode { return "" } diff --git a/internal/musicxml/entity.go b/internal/musicxml/entity.go index d866876..f9b3bdd 100644 --- a/internal/musicxml/entity.go +++ b/internal/musicxml/entity.go @@ -45,6 +45,9 @@ type MusicXML struct { Credit []Credit `xml:"credit"` Part Part `xml:"part"` Work Work `xml:"work"` + + TotalMeasure int `xml:"-"` + Tempo int `xml:"-"` } type CreditType string @@ -201,9 +204,14 @@ type Tie struct { LineType NoteSlurLineType `xml:"line-type,attr"` } +type Sound struct { + Tempo int `xml:"tempo,attr"` +} + type Direction struct { Placement string `xml:"placement,attr"` DirectionType []DirectionType `xml:"direction-type"` + Sound *Sound `xml:"sound"` } type DirectionDashesType string diff --git a/internal/musicxml/measure.go b/internal/musicxml/measure.go index 8df5475..0d09c99 100644 --- a/internal/musicxml/measure.go +++ b/internal/musicxml/measure.go @@ -39,7 +39,8 @@ type Measure struct { // FIXME: one centralized place for the measured text RightMeasureText *MeasureText `xml:"-"` PrefixHeader map[int]string `xml:"-"` - RepeatInfo *RepeatInfo + RepeatInfo *RepeatInfo `xml:"-"` + Tempo int } func (m *Measure) Build() error { @@ -73,6 +74,10 @@ func (m *Measure) Build() error { if err != nil { return err } + + if d.Sound != nil && d.Sound.Tempo != 0 { + m.Tempo = d.Sound.Tempo + } if len(d.DirectionType) == 0 { continue } diff --git a/internal/playback/playback.go b/internal/playback/playback.go new file mode 100644 index 0000000..7b2d071 --- /dev/null +++ b/internal/playback/playback.go @@ -0,0 +1,74 @@ +package playback + +import ( + "time" + + "github.com/jodi-ivan/numbered-notation-xml/internal/entity" +) + +func CalculateDuration(steps *[]Step, tempo int, totalBeat map[int][]float64, rect map[int][][2]entity.Coordinate) time.Duration { + if tempo == 0 { + tempo = 85 + } + totalOverallBeat := 0.0 + for i := 0; i < len(*steps); i++ { + dStep := *steps + currStep := dStep[i] + + totalOverallBeat += totalBeat[currStep.MeasureNumber][0] + if len(totalBeat[currStep.MeasureNumber]) > 1 { + for i := 1; i < len(totalBeat[currStep.MeasureNumber]); i++ { + totalOverallBeat += totalBeat[currStep.MeasureNumber][i] + } + } + } + + overallDuration := (totalOverallBeat / float64(tempo)) * 60.0 + durationEachBeat := (overallDuration / totalOverallBeat) * float64(time.Second) + totalSteps := len(*steps) + for i := 0; i < totalSteps; i++ { + dStep := *steps + currStep := dStep[i] + if len(rect[currStep.MeasureNumber]) == 1 { + + currStep.TotalBeat = totalBeat[currStep.MeasureNumber][0] + currStep.Duration = time.Duration(durationEachBeat * currStep.TotalBeat) + currStep.DurationStr = currStep.Duration.String() + currStep.Rect = rect[currStep.MeasureNumber][0] + + dStep[i] = currStep + *steps = dStep + continue + } else { + + firstStep := currStep + firstStep.TotalBeat = totalBeat[currStep.MeasureNumber][0] + firstStep.Duration = time.Duration(durationEachBeat * firstStep.TotalBeat) + firstStep.DurationStr = firstStep.Duration.String() + firstStep.Rect = rect[currStep.MeasureNumber][0] + + newSteps := []Step{} + + for i := 1; i < len(totalBeat[currStep.MeasureNumber]); i++ { + dur := time.Duration(durationEachBeat * totalBeat[currStep.MeasureNumber][i]) + newSteps = append(newSteps, Step{ + TotalBeat: totalBeat[currStep.MeasureNumber][i], + Duration: dur, + DurationStr: dur.String(), + Rect: rect[currStep.MeasureNumber][i], + MeasureNumber: currStep.MeasureNumber, + LyricPart: firstStep.LyricPart, + }) + } + + dStep[i] = firstStep + dStep = append(dStep[:i+1], append(newSteps, dStep[i+1:]...)...) + i += len(newSteps) + totalSteps += len(newSteps) + *steps = dStep + } + + } + + return time.Duration(overallDuration * float64(time.Second)) +} diff --git a/internal/playback/steps.go b/internal/playback/steps.go new file mode 100644 index 0000000..e94038a --- /dev/null +++ b/internal/playback/steps.go @@ -0,0 +1,49 @@ +package playback + +func GenerateSteps(hasFine, lastMeasure int, repeat [][3]int) []Step { + + result := []Step{} + + lastPart := lastMeasure + if len(repeat) > 0 { + lastPart = repeat[0][0] + } + + for i := 1; i <= lastPart && lastPart != 1; i++ { + result = append(result, Step{MeasureNumber: i, LyricPart: 1}) + } + + for _, r := range repeat { + start, end, jump := r[0], r[1], r[2] + for part := 1; part <= 2; part++ { + for i := start; i <= end; i++ { + measureNumber := i + if part == 2 && i == end && jump > 0 { + measureNumber += jump + } + result = append(result, Step{MeasureNumber: measureNumber, LyricPart: part}) + + } + } + + // TODO: fill gap between repeats. + } + + if len(repeat) > 0 { + for i := repeat[len(repeat)-1][1] + 1; i <= lastMeasure; i++ { + result = append(result, Step{MeasureNumber: i, LyricPart: 1}) + } + } + + if hasFine > 0 { + jump := len(repeat) > 0 && (repeat[0][1] == hasFine-repeat[0][2]) + + for i := 1; i <= hasFine; i++ { + if jump && i == hasFine-1 { + continue + } + result = append(result, Step{MeasureNumber: i, LyricPart: 1}) + } + } + return result +} diff --git a/internal/playback/type.go b/internal/playback/type.go new file mode 100644 index 0000000..6e7e3fd --- /dev/null +++ b/internal/playback/type.go @@ -0,0 +1,17 @@ +package playback + +import ( + "time" + + "github.com/jodi-ivan/numbered-notation-xml/internal/entity" +) + +type Step struct { + Duration time.Duration `json:"duration"` + DurationStr string `json:"duration_str"` + LyricPart int `json:"lyric_part"` + MeasureNumber int `json:"measure_number"` + TotalBeat float64 `json:"total_beat"` + + Rect [2]entity.Coordinate `json:"rect"` +} diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go index 6504db6..c327875 100644 --- a/internal/renderer/renderer.go +++ b/internal/renderer/renderer.go @@ -47,7 +47,7 @@ func NewRenderer() Renderer { } func (ir *rendererInteractor) Render(ctx context.Context, music musicxml.MusicXML, canv canvas.Canvas, metadata *entity.HymnMetaData) { - canvHeight := 3000 + canvHeight := 3000 ns := []string{} param, _ := params.GetParamFromContext(ctx) if param.Render != nil && param.Render.WhiteBackground { diff --git a/internal/staff/align.go b/internal/staff/align.go index 4438fc8..1a3524f 100644 --- a/internal/staff/align.go +++ b/internal/staff/align.go @@ -1,9 +1,11 @@ package staff import ( + "cmp" "context" "fmt" "math" + "slices" "github.com/jodi-ivan/numbered-notation-xml/internal/barline" "github.com/jodi-ivan/numbered-notation-xml/internal/breathpause" @@ -20,6 +22,7 @@ import ( "github.com/jodi-ivan/numbered-notation-xml/internal/staff/toping" "github.com/jodi-ivan/numbered-notation-xml/internal/timesig" "github.com/jodi-ivan/numbered-notation-xml/utils/canvas" + "github.com/jodi-ivan/numbered-notation-xml/utils/params" ) type RenderStaffWithAlign interface { @@ -176,6 +179,32 @@ func (rsa *renderStaffAlign) RenderWithAlign(ctx context.Context, canv canvas.Ca offsetLyric := 0 for mi, measure := range noteRenderer { + param, _ := params.GetParamFromContext(ctx) + if param != nil && param.Playback != nil { + max := slices.MaxFunc(measure, func(a, b *entity.NoteRenderer) int { + return cmp.Compare(len(a.Lyric), len(b.Lyric)) + }) + + rectX := measure[0].PositionX - 5 + if mi > 0 { + rectX = noteRenderer[mi-1][len(noteRenderer[mi-1])-1].PositionX + } + rectX1 := measure[len(measure)-1].PositionX + hasNewLine := slices.ContainsFunc(measure, func(mc *entity.NoteRenderer) bool { return mc.IsNewLine }) + if hasNewLine { // last measure + rectX1 = constant.LAYOUT_WIDTH - constant.LAYOUT_INDENT_LENGTH + 8 + } + rectY := (y + lyric.DISTANCE_NOTE_TO_LYRIC + (len(max.Lyric) * lyric.LINE_BETWEEN_LYRIC)) + + measureNo := measure[0].MeasureNumber + + param.Playback.Rect[measureNo] = append(param.Playback.Rect[measureNo], + [2]entity.Coordinate{ + entity.NewCoordinate(float64(rectX), float64(y)), + entity.NewCoordinate(float64(rectX1-rectX), float64((rectY+(yPos-y))-y)), + }) + } + canv.Group("class='measure-align'", fmt.Sprintf("number='%d'", measure[0].MeasureNumber)) prev := []*entity.NoteRenderer{} diff --git a/internal/staff/lines/lines.go b/internal/staff/lines/lines.go index 8c2de2c..30b008c 100644 --- a/internal/staff/lines/lines.go +++ b/internal/staff/lines/lines.go @@ -3,6 +3,7 @@ package lines import ( "cmp" "context" + "fmt" "unicode" "github.com/jodi-ivan/numbered-notation-xml/internal/constant" @@ -127,6 +128,11 @@ func (ls *LineStaff) Render(canv canvas.Canvas, y int, measureNo int, inclTimesi x := float64(constant.LAYOUT_INDENT_LENGTH) initialY := ls.Lines[0] canv.Group(`class="staff-markings"`) + + if measureNo > 1 && ls.TimeSig.Signatures[0].Beat != 1 { + measureMarking := fmt.Sprintf("%d", measureNo) + canv.Text(constant.LAYOUT_INDENT_LENGTH-len(measureMarking)*4, ls.Lines[0]-15, measureMarking) + } // clef key := ls.Keysig.GetKeyOnMeasure(context.Background(), measureNo) accidentalSet := key.GetAccidentals() diff --git a/internal/staff/render.go b/internal/staff/render.go index 73a6994..ca875f8 100644 --- a/internal/staff/render.go +++ b/internal/staff/render.go @@ -12,6 +12,7 @@ import ( "github.com/jodi-ivan/numbered-notation-xml/internal/staff/lines" "github.com/jodi-ivan/numbered-notation-xml/internal/timesig" "github.com/jodi-ivan/numbered-notation-xml/utils/canvas" + "github.com/jodi-ivan/numbered-notation-xml/utils/params" ) func (si *staffInteractor) Render(ctx context.Context, canv canvas.Canvas, part musicxml.Part, keySignature keysig.KeySignature, timeSignature timesig.TimeSignature, metadata *entity.HymnMetaData) int { @@ -27,6 +28,7 @@ func (si *staffInteractor) Render(ctx context.Context, canv canvas.Canvas, part info := StaffInfo{ NextLineRenderer: []*entity.NoteRenderer{}, SyllableOffset: map[int]int{}, + TotalBeat: map[int][]float64{}, } oldMarginButtom := 0 for i, st := range staffes { @@ -40,6 +42,7 @@ func (si *staffInteractor) Render(ctx context.Context, canv canvas.Canvas, part RepeatInfo: info.RepeatInfo, SyllableOffset: info.SyllableOffset, + TotalBeat: info.TotalBeat, } info = si.RenderStaff(ctx, canv, x, relativeY, i, metadata, st, data) info.RepeatInfo = append(data.RepeatInfo, info.RepeatInfo...) @@ -77,6 +80,7 @@ func (si *staffInteractor) Render(ctx context.Context, canv canvas.Canvas, part RepeatInfo: info.RepeatInfo, SyllableOffset: info.SyllableOffset, + TotalBeat: info.TotalBeat, // TODO: send this to result useacase } x = staffLines.GetLeftIndent(info.NextLineRenderer[0].MeasureNumber) idx := len(staffes) - 1 @@ -87,5 +91,10 @@ func (si *staffInteractor) Render(ctx context.Context, canv canvas.Canvas, part relativeY += info.MarginBottom + STAFF_LINE_DISTANCE + 70 } + param, _ := params.GetParamFromContext(ctx) + if param != nil && param.Playback != nil { + param.Playback.TotalBeat = info.TotalBeat + } + return relativeY } diff --git a/internal/staff/staff.go b/internal/staff/staff.go index 76fab60..91ef3f3 100644 --- a/internal/staff/staff.go +++ b/internal/staff/staff.go @@ -77,10 +77,12 @@ func (si *staffInteractor) RenderStaff(ctx context.Context, canv canvas.Canvas, align, staffInfo = ProcessPreviousLines(data.PrevNotes, data.KeySig, y) pos = data.PrevNotes[len(data.PrevNotes)-1].IndexPosition + 1 } + + staffInfo.TotalBeat = data.TotalBeat + for mi, measure := range measures { mSyllcount := 0 - measure.Build() notes := []*entity.NoteRenderer{} @@ -112,7 +114,6 @@ func (si *staffInteractor) RenderStaff(ctx context.Context, canv canvas.Canvas, n, octave, strikethrough := moveabledo.GetNumberedNotation(currKeySig, note) noteLength := data.TimeSig.GetNoteLength(ctx, measure.Number, note) - if rhythm.HasTies(note) && (notePos+1 < len(measure.Notes)) && currTimesig.IsCommonTime() { if mergedLength, mergedNote := rhythm.MergeNotes(ctx, note, measure.Notes[notePos+1], currTimesig); mergedLength > noteLength && mergedLength < 3 { note, noteLength = mergedNote, mergedLength @@ -336,6 +337,17 @@ func (si *staffInteractor) RenderStaff(ctx context.Context, canv canvas.Canvas, } } + totalBeat := map[int]float64{} + for _, notes := range align { + for _, note := range notes { + totalBeat[note.MeasureNumber] += note.NoteValue + } + } + + for tb, b := range totalBeat { + staffInfo.TotalBeat[tb] = append(staffInfo.TotalBeat[tb], b) + } + staffInfo.MarginBottom += si.RenderAlign.RenderWithAlign(ctx, canv, staffPos, y, data.TimeSig, data.KeySig, align) staffInfo.SyllableCount += startSyllable diff --git a/internal/staff/text/text.go b/internal/staff/text/text.go index 5f066a2..3672703 100644 --- a/internal/staff/text/text.go +++ b/internal/staff/text/text.go @@ -21,6 +21,7 @@ type Text interface { MeasureHasText(measure musicxml.Measure, t string) bool SetMeasureTextRenderer(ctx context.Context, noteRenderer *entity.NoteRenderer, note musicxml.Note, isLastNote bool) bool RenderMeasureText(ctx context.Context, y int, canv canvas.Canvas, notes []*entity.NoteRenderer, linestaff ...lines.LineStaff) + FindMeasureByText(measures []musicxml.Measure, t string) int } func NewText(l lyric.Lyric) Text { @@ -33,6 +34,23 @@ type textInteractor struct { Lyric lyric.Lyric } +func (ti *textInteractor) FindMeasureByText(measures []musicxml.Measure, t string) int { + hasFine := -1 + for _, m := range measures { + if ti.MeasureHasText(m, t) { + return m.Number + } + + for _, n := range m.Notes { + if ti.NoteHasText(n.MeasureText, t) { + return m.Number + } + } + } + return hasFine + +} + func (ti *textInteractor) NoteHasText(measureText []musicxml.MeasureText, t ...string) bool { if len(t) == 0 { return len(measureText) > 0 diff --git a/internal/staff/text/text_mock.go b/internal/staff/text/text_mock.go index 0235610..4b16a34 100644 --- a/internal/staff/text/text_mock.go +++ b/internal/staff/text/text_mock.go @@ -41,6 +41,63 @@ func (_m *MockText) EXPECT() *MockText_Expecter { return &MockText_Expecter{mock: &_m.Mock} } +// FindMeasureByText provides a mock function for the type MockText +func (_mock *MockText) FindMeasureByText(measures []musicxml.Measure, t string) int { + ret := _mock.Called(measures, t) + + if len(ret) == 0 { + panic("no return value specified for FindMeasureByText") + } + + var r0 int + if returnFunc, ok := ret.Get(0).(func([]musicxml.Measure, string) int); ok { + r0 = returnFunc(measures, t) + } else { + r0 = ret.Get(0).(int) + } + return r0 +} + +// MockText_FindMeasureByText_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindMeasureByText' +type MockText_FindMeasureByText_Call struct { + *mock.Call +} + +// FindMeasureByText is a helper method to define mock.On call +// - measures []musicxml.Measure +// - t string +func (_e *MockText_Expecter) FindMeasureByText(measures interface{}, t interface{}) *MockText_FindMeasureByText_Call { + return &MockText_FindMeasureByText_Call{Call: _e.mock.On("FindMeasureByText", measures, t)} +} + +func (_c *MockText_FindMeasureByText_Call) Run(run func(measures []musicxml.Measure, t string)) *MockText_FindMeasureByText_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 []musicxml.Measure + if args[0] != nil { + arg0 = args[0].([]musicxml.Measure) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockText_FindMeasureByText_Call) Return(n int) *MockText_FindMeasureByText_Call { + _c.Call.Return(n) + return _c +} + +func (_c *MockText_FindMeasureByText_Call) RunAndReturn(run func(measures []musicxml.Measure, t string) int) *MockText_FindMeasureByText_Call { + _c.Call.Return(run) + return _c +} + // MeasureHasText provides a mock function for the type MockText func (_mock *MockText) MeasureHasText(measure musicxml.Measure, t string) bool { ret := _mock.Called(measure, t) diff --git a/internal/staff/type.go b/internal/staff/type.go index 64eedf7..cc53606 100644 --- a/internal/staff/type.go +++ b/internal/staff/type.go @@ -19,6 +19,7 @@ type StaffInfo struct { SyllableCount int RepeatInfo []*musicxml.RepeatInfo SyllableOffset map[int]int + TotalBeat map[int][]float64 } type StaffData struct { @@ -31,6 +32,7 @@ type StaffData struct { RepeatInfo []*musicxml.RepeatInfo SyllableOffset map[int]int + TotalBeat map[int][]float64 } const ( diff --git a/internal/verse/firstverse.go b/internal/verse/firstverse.go index 88834bf..5ae0bc1 100644 --- a/internal/verse/firstverse.go +++ b/internal/verse/firstverse.go @@ -36,7 +36,6 @@ func BuildContent(music musicxml.MusicXML, metadata *entity.HymnMetaData) [][]en prevTotalLyric := -1 wordVerses := map[int]entity.LyricWordVerse{} for _, measure := range music.Part.Measures { - measure.Build() for _, note := range measure.Notes { if len(note.Lyric) == 0 { continue diff --git a/protobuf/hymn.pb.go b/protobuf/hymn.pb.go new file mode 100644 index 0000000..b1e3a81 --- /dev/null +++ b/protobuf/hymn.pb.go @@ -0,0 +1,428 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.21.12 +// source: protobuf/hymn.proto + +package hymn + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Coordinate struct { + state protoimpl.MessageState `protogen:"open.v1"` + X float64 `protobuf:"fixed64,1,opt,name=x,proto3" json:"x,omitempty"` + Y float64 `protobuf:"fixed64,2,opt,name=y,proto3" json:"y,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Coordinate) Reset() { + *x = Coordinate{} + mi := &file_protobuf_hymn_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Coordinate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Coordinate) ProtoMessage() {} + +func (x *Coordinate) ProtoReflect() protoreflect.Message { + mi := &file_protobuf_hymn_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Coordinate.ProtoReflect.Descriptor instead. +func (*Coordinate) Descriptor() ([]byte, []int) { + return file_protobuf_hymn_proto_rawDescGZIP(), []int{0} +} + +func (x *Coordinate) GetX() float64 { + if x != nil { + return x.X + } + return 0 +} + +func (x *Coordinate) GetY() float64 { + if x != nil { + return x.Y + } + return 0 +} + +type RectangularArea struct { + state protoimpl.MessageState `protogen:"open.v1"` + TopLeftPos *Coordinate `protobuf:"bytes,1,opt,name=top_left_pos,json=topLeftPos,proto3" json:"top_left_pos,omitempty"` + Width int32 `protobuf:"varint,2,opt,name=width,proto3" json:"width,omitempty"` + Height int32 `protobuf:"varint,3,opt,name=height,proto3" json:"height,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RectangularArea) Reset() { + *x = RectangularArea{} + mi := &file_protobuf_hymn_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RectangularArea) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RectangularArea) ProtoMessage() {} + +func (x *RectangularArea) ProtoReflect() protoreflect.Message { + mi := &file_protobuf_hymn_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RectangularArea.ProtoReflect.Descriptor instead. +func (*RectangularArea) Descriptor() ([]byte, []int) { + return file_protobuf_hymn_proto_rawDescGZIP(), []int{1} +} + +func (x *RectangularArea) GetTopLeftPos() *Coordinate { + if x != nil { + return x.TopLeftPos + } + return nil +} + +func (x *RectangularArea) GetWidth() int32 { + if x != nil { + return x.Width + } + return 0 +} + +func (x *RectangularArea) GetHeight() int32 { + if x != nil { + return x.Height + } + return 0 +} + +type Step struct { + state protoimpl.MessageState `protogen:"open.v1"` + DurationMillisecond int32 `protobuf:"varint,1,opt,name=duration_millisecond,json=durationMillisecond,proto3" json:"duration_millisecond,omitempty"` + DurationStr string `protobuf:"bytes,2,opt,name=duration_str,json=durationStr,proto3" json:"duration_str,omitempty"` + LyricPart int32 `protobuf:"varint,3,opt,name=lyric_part,json=lyricPart,proto3" json:"lyric_part,omitempty"` + MeasureNumber int32 `protobuf:"varint,4,opt,name=measure_number,json=measureNumber,proto3" json:"measure_number,omitempty"` + TotalBeat float64 `protobuf:"fixed64,5,opt,name=total_beat,json=totalBeat,proto3" json:"total_beat,omitempty"` + Rect *RectangularArea `protobuf:"bytes,6,opt,name=rect,proto3" json:"rect,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Step) Reset() { + *x = Step{} + mi := &file_protobuf_hymn_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Step) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Step) ProtoMessage() {} + +func (x *Step) ProtoReflect() protoreflect.Message { + mi := &file_protobuf_hymn_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Step.ProtoReflect.Descriptor instead. +func (*Step) Descriptor() ([]byte, []int) { + return file_protobuf_hymn_proto_rawDescGZIP(), []int{2} +} + +func (x *Step) GetDurationMillisecond() int32 { + if x != nil { + return x.DurationMillisecond + } + return 0 +} + +func (x *Step) GetDurationStr() string { + if x != nil { + return x.DurationStr + } + return "" +} + +func (x *Step) GetLyricPart() int32 { + if x != nil { + return x.LyricPart + } + return 0 +} + +func (x *Step) GetMeasureNumber() int32 { + if x != nil { + return x.MeasureNumber + } + return 0 +} + +func (x *Step) GetTotalBeat() float64 { + if x != nil { + return x.TotalBeat + } + return 0 +} + +func (x *Step) GetRect() *RectangularArea { + if x != nil { + return x.Rect + } + return nil +} + +type Metadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + TotalVerse int32 `protobuf:"varint,1,opt,name=total_verse,json=totalVerse,proto3" json:"total_verse,omitempty"` + Variant []string `protobuf:"bytes,2,rep,name=variant,proto3" json:"variant,omitempty"` + PlaybackStep []*Step `protobuf:"bytes,3,rep,name=playback_step,json=playbackStep,proto3" json:"playback_step,omitempty"` + OverallDurationMill int64 `protobuf:"varint,4,opt,name=overall_duration_mill,json=overallDurationMill,proto3" json:"overall_duration_mill,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Metadata) Reset() { + *x = Metadata{} + mi := &file_protobuf_hymn_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Metadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Metadata) ProtoMessage() {} + +func (x *Metadata) ProtoReflect() protoreflect.Message { + mi := &file_protobuf_hymn_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Metadata.ProtoReflect.Descriptor instead. +func (*Metadata) Descriptor() ([]byte, []int) { + return file_protobuf_hymn_proto_rawDescGZIP(), []int{3} +} + +func (x *Metadata) GetTotalVerse() int32 { + if x != nil { + return x.TotalVerse + } + return 0 +} + +func (x *Metadata) GetVariant() []string { + if x != nil { + return x.Variant + } + return nil +} + +func (x *Metadata) GetPlaybackStep() []*Step { + if x != nil { + return x.PlaybackStep + } + return nil +} + +func (x *Metadata) GetOverallDurationMill() int64 { + if x != nil { + return x.OverallDurationMill + } + return 0 +} + +type HymnSVG struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + SvgBody string `protobuf:"bytes,2,opt,name=svg_body,json=svgBody,proto3" json:"svg_body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HymnSVG) Reset() { + *x = HymnSVG{} + mi := &file_protobuf_hymn_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HymnSVG) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HymnSVG) ProtoMessage() {} + +func (x *HymnSVG) ProtoReflect() protoreflect.Message { + mi := &file_protobuf_hymn_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HymnSVG.ProtoReflect.Descriptor instead. +func (*HymnSVG) Descriptor() ([]byte, []int) { + return file_protobuf_hymn_proto_rawDescGZIP(), []int{4} +} + +func (x *HymnSVG) GetMetadata() *Metadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *HymnSVG) GetSvgBody() string { + if x != nil { + return x.SvgBody + } + return "" +} + +var File_protobuf_hymn_proto protoreflect.FileDescriptor + +const file_protobuf_hymn_proto_rawDesc = "" + + "\n" + + "\x13protobuf/hymn.proto\x12\x04hymn\"(\n" + + "\n" + + "Coordinate\x12\f\n" + + "\x01x\x18\x01 \x01(\x01R\x01x\x12\f\n" + + "\x01y\x18\x02 \x01(\x01R\x01y\"s\n" + + "\x0fRectangularArea\x122\n" + + "\ftop_left_pos\x18\x01 \x01(\v2\x10.hymn.CoordinateR\n" + + "topLeftPos\x12\x14\n" + + "\x05width\x18\x02 \x01(\x05R\x05width\x12\x16\n" + + "\x06height\x18\x03 \x01(\x05R\x06height\"\xec\x01\n" + + "\x04Step\x121\n" + + "\x14duration_millisecond\x18\x01 \x01(\x05R\x13durationMillisecond\x12!\n" + + "\fduration_str\x18\x02 \x01(\tR\vdurationStr\x12\x1d\n" + + "\n" + + "lyric_part\x18\x03 \x01(\x05R\tlyricPart\x12%\n" + + "\x0emeasure_number\x18\x04 \x01(\x05R\rmeasureNumber\x12\x1d\n" + + "\n" + + "total_beat\x18\x05 \x01(\x01R\ttotalBeat\x12)\n" + + "\x04rect\x18\x06 \x01(\v2\x15.hymn.RectangularAreaR\x04rect\"\xaa\x01\n" + + "\bMetadata\x12\x1f\n" + + "\vtotal_verse\x18\x01 \x01(\x05R\n" + + "totalVerse\x12\x18\n" + + "\avariant\x18\x02 \x03(\tR\avariant\x12/\n" + + "\rplayback_step\x18\x03 \x03(\v2\n" + + ".hymn.StepR\fplaybackStep\x122\n" + + "\x15overall_duration_mill\x18\x04 \x01(\x03R\x13overallDurationMill\"P\n" + + "\aHymnSVG\x12*\n" + + "\bmetadata\x18\x01 \x01(\v2\x0e.hymn.MetadataR\bmetadata\x12\x19\n" + + "\bsvg_body\x18\x02 \x01(\tR\asvgBodyB:Z8github.com/jodi-ivan/numbered-notation-xml/protobuf/hymnb\x06proto3" + +var ( + file_protobuf_hymn_proto_rawDescOnce sync.Once + file_protobuf_hymn_proto_rawDescData []byte +) + +func file_protobuf_hymn_proto_rawDescGZIP() []byte { + file_protobuf_hymn_proto_rawDescOnce.Do(func() { + file_protobuf_hymn_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_protobuf_hymn_proto_rawDesc), len(file_protobuf_hymn_proto_rawDesc))) + }) + return file_protobuf_hymn_proto_rawDescData +} + +var file_protobuf_hymn_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_protobuf_hymn_proto_goTypes = []any{ + (*Coordinate)(nil), // 0: hymn.Coordinate + (*RectangularArea)(nil), // 1: hymn.RectangularArea + (*Step)(nil), // 2: hymn.Step + (*Metadata)(nil), // 3: hymn.Metadata + (*HymnSVG)(nil), // 4: hymn.HymnSVG +} +var file_protobuf_hymn_proto_depIdxs = []int32{ + 0, // 0: hymn.RectangularArea.top_left_pos:type_name -> hymn.Coordinate + 1, // 1: hymn.Step.rect:type_name -> hymn.RectangularArea + 2, // 2: hymn.Metadata.playback_step:type_name -> hymn.Step + 3, // 3: hymn.HymnSVG.metadata:type_name -> hymn.Metadata + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_protobuf_hymn_proto_init() } +func file_protobuf_hymn_proto_init() { + if File_protobuf_hymn_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_protobuf_hymn_proto_rawDesc), len(file_protobuf_hymn_proto_rawDesc)), + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_protobuf_hymn_proto_goTypes, + DependencyIndexes: file_protobuf_hymn_proto_depIdxs, + MessageInfos: file_protobuf_hymn_proto_msgTypes, + }.Build() + File_protobuf_hymn_proto = out.File + file_protobuf_hymn_proto_goTypes = nil + file_protobuf_hymn_proto_depIdxs = nil +} diff --git a/protobuf/hymn.proto b/protobuf/hymn.proto new file mode 100644 index 0000000..e2f54bb --- /dev/null +++ b/protobuf/hymn.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package hymn; + +option go_package = "github.com/jodi-ivan/numbered-notation-xml/protobuf/hymn"; +message Coordinate { + double x = 1; + double y = 2; +} + +message RectangularArea { + Coordinate top_left_pos = 1; + int32 width = 2; + int32 height = 3; +} +message Step { + int32 duration_millisecond = 1; + string duration_str = 2; + int32 lyric_part = 3; + int32 measure_number = 4; + double total_beat = 5; + + RectangularArea rect = 6; +} + +message Metadata { + int32 total_verse = 1; + repeated string variant = 2; + repeated Step playback_step = 3; + int64 overall_duration_mill = 4; +} + +message HymnSVG { + Metadata metadata = 1; + string svg_body = 2; +} \ No newline at end of file diff --git a/svc/repository/repository.go b/svc/repository/repository.go index 54f8095..a6ca35b 100644 --- a/svc/repository/repository.go +++ b/svc/repository/repository.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/xml" "io" + "log" "os" "github.com/jmoiron/sqlx" @@ -138,6 +139,15 @@ func (r *repository) GetMusicXML(ctx context.Context, filepath string) (musicxml return musicxml.MusicXML{}, err } + for i := range music.Part.Measures { + m := &music.Part.Measures[i] + err = m.Build() + if err != nil { + log.Println("[GetMusicXML] Failed to parsing the measure, ", m.Number, ". Err: ", err.Error()) + return music, err + } + } + return music, err } diff --git a/svc/usecase/usecase.go b/svc/usecase/usecase.go index 78cc3f6..82b78ae 100644 --- a/svc/usecase/usecase.go +++ b/svc/usecase/usecase.go @@ -1,17 +1,22 @@ package usecase import ( + "cmp" "context" "encoding/json" "fmt" "log" + "slices" "strconv" "github.com/jodi-ivan/numbered-notation-xml/internal/barline" "github.com/jodi-ivan/numbered-notation-xml/internal/constant" "github.com/jodi-ivan/numbered-notation-xml/internal/entity" + "github.com/jodi-ivan/numbered-notation-xml/internal/lyric" "github.com/jodi-ivan/numbered-notation-xml/internal/musicxml" + "github.com/jodi-ivan/numbered-notation-xml/internal/playback" "github.com/jodi-ivan/numbered-notation-xml/internal/renderer" + "github.com/jodi-ivan/numbered-notation-xml/internal/staff/text" "github.com/jodi-ivan/numbered-notation-xml/svc/repository" "github.com/jodi-ivan/numbered-notation-xml/utils/canvas" "github.com/jodi-ivan/numbered-notation-xml/utils/config" @@ -26,6 +31,7 @@ type interactor struct { config config.Config repo repository.Repository renderer renderer.Renderer + text text.Text } func New(config config.Config, repo repository.Repository, renderer renderer.Renderer) Usecase { @@ -33,40 +39,47 @@ func New(config config.Config, repo repository.Repository, renderer renderer.Ren config: config, repo: repo, renderer: renderer, + text: text.NewText(lyric.NewLyric()), } } -func collectRepeat(measures []musicxml.Measure) [][2]int { +func collectRepeat(measures []musicxml.Measure) [][3]int { - result := [][2]int{} + result := [][3]int{} // check if there is any repeat at all for i := 0; i < len(measures); i++ { for _, b := range measures[i].Barline { if b.Repeat != nil { switch b.Repeat.Direction { case musicxml.BarLineRepeatDirectionForward: - result = append(result, [2]int{measures[i].Number}) + result = append(result, [3]int{measures[i].Number}) case musicxml.BarLineRepeatDirectionBackward: // closing if len(result) == 0 { - result = append(result, [2]int{1}) // beginning of the measure + result = append(result, [3]int{1}) // beginning of the measure } lastKnown := result[len(result)-1] lastKnown[1] = measures[i].Number result[len(result)-1] = lastKnown } + + } + if b.Ending != nil && b.Ending.Type == musicxml.BarlineEndingTypeDiscontinue && len(result) > 0 { + lastKnown := result[len(result)-1] + lastKnown[2] = measures[i].Number - lastKnown[1] + result[len(result)-1] = lastKnown } } } return result } -func ProcessRepeats(music *musicxml.MusicXML) { +func ProcessRepeats(music *musicxml.MusicXML) [][3]int { repeats := collectRepeat(music.Part.Measures) if len(repeats) == 0 { - return + return nil } bli := barline.NewBarline() @@ -148,6 +161,8 @@ func ProcessRepeats(music *musicxml.MusicXML) { } } + return repeats + } func (i *interactor) RenderHymn(ctx context.Context, canv canvas.Canvas, hymnNum int, variant ...string) error { @@ -163,7 +178,16 @@ func (i *interactor) RenderHymn(ctx context.Context, canv canvas.Canvas, hymnNum } } - ProcessRepeats(&music) + hasRepeats := ProcessRepeats(&music) + + // detect Fine in the whole music + hasFine := i.text.FindMeasureByText(music.Part.Measures, text.DEFAULT_TEXT_FINE) + + slices.SortFunc(hasRepeats, func(i, j [3]int) int { + return cmp.Compare(i[0], j[0]) + }) + + music.Tempo = music.Part.Measures[0].Tempo metaData, err := i.repo.GetHymnMetaData(ctx, hymnNum, variant...) if err != nil { @@ -197,10 +221,21 @@ func (i *interactor) RenderHymn(ctx context.Context, canv canvas.Canvas, hymnNum metaWithParsedVerse.ParsedVerse[i] = whole } + lastMeasure := music.Part.Measures[len(music.Part.Measures)-1].Number + steps := playback.GenerateSteps(hasFine, lastMeasure, hasRepeats) + music.TotalMeasure = len(steps) + param.Playback = ¶ms.PlaybackParams{Rect: map[int][][2]entity.Coordinate{}} + + for _, s := range steps { + param.Playback.Rect[s.MeasureNumber] = [][2]entity.Coordinate{} + } + + rctx := params.NewParamContext(ctx, param) canv.Delegator().OnBeforeStartWrite() - i.renderer.Render(ctx, music, canv, metaWithParsedVerse) + i.renderer.Render(rctx, music, canv, metaWithParsedVerse) + playback.CalculateDuration(&steps, music.Tempo, param.Playback.TotalBeat, param.Playback.Rect) return nil } diff --git a/utils/params/paramctx.go b/utils/params/paramctx.go index 7fed22a..1d32c81 100644 --- a/utils/params/paramctx.go +++ b/utils/params/paramctx.go @@ -15,6 +15,7 @@ type Param struct { Diagnostic *DiagParam Render *RenderParam DirectReport *DirectReport + Playback *PlaybackParams } type paramCtx struct { diff --git a/utils/params/playback.go b/utils/params/playback.go new file mode 100644 index 0000000..60d5128 --- /dev/null +++ b/utils/params/playback.go @@ -0,0 +1,8 @@ +package params + +import "github.com/jodi-ivan/numbered-notation-xml/internal/entity" + +type PlaybackParams struct { + Rect map[int][][2]entity.Coordinate + TotalBeat map[int][]float64 +}