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
+}