From f0f7685498bbb6c9cbb71e222f7e25029b5667bf Mon Sep 17 00:00:00 2001 From: Jody Ivan Lumbantoruan Date: Thu, 25 Jun 2026 19:14:35 +0700 Subject: [PATCH 01/15] feat(stream): stream audio files --- .gitignore | 7 +- cmd/lab/audio-stream/stream/audio_stream.go | 70 +++++++ cmd/rest/app.go | 7 +- files/audio/.gitkeep | 1 + files/var/www/html/stream-test.html | 208 ++++++++++++++++++++ go.mod | 1 - go.sum | 3 - 7 files changed, 291 insertions(+), 6 deletions(-) create mode 100644 cmd/lab/audio-stream/stream/audio_stream.go create mode 100644 files/audio/.gitkeep create mode 100644 files/var/www/html/stream-test.html 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/cmd/lab/audio-stream/stream/audio_stream.go b/cmd/lab/audio-stream/stream/audio_stream.go new file mode 100644 index 0000000..e7bed2d --- /dev/null +++ b/cmd/lab/audio-stream/stream/audio_stream.go @@ -0,0 +1,70 @@ +package stream + +import ( + "io" + "log" + "net/http" + "os" + + "github.com/julienschmidt/httprouter" +) + +type AudioStream struct { + Sig chan os.Signal +} + +func (dh *AudioStream) ServeHTTP(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + + fileLocation := `/home/jodiivan/go/src/github.com/jodi-ivan/numbered-notation-xml/files/audio/kj-001.mp3` + + file, err := os.Open(fileLocation) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + + defer file.Close() + + 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") + + // Create a buffer for streaming data chunks + buf := make([]byte, 4096) // 4KB chunks + + for { + + select { + case <-dh.Sig: + log.Println("os signal interrupt") + return + + default: + + } + + // Read a chunk from the MP3 file + n, err := file.Read(buf) + if err != nil { + if err == io.EOF { + // Loop the audio by seeking back to the start + return + } + // Stop streaming if the connection drops or error occurs + break + } + + // Write the chunk to the HTTP response + w.Write(buf[:n]) + + // Flush the buffer to send data to the client immediately + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + + } + +} 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..329c476 --- /dev/null +++ b/files/var/www/html/stream-test.html @@ -0,0 +1,208 @@ + + + + + + FM Stream Player + + + +
+

🎙️ FM Stream Player

+ +
+ + +
+ +
+
+ + Disconnected +
+
+
+
+ + + + \ No newline at end of file diff --git a/go.mod b/go.mod index f7de75e..ce9d157 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 78c1ea6..a1df8c2 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,6 @@ 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/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 +25,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= From a08f8546cce03f459b20dc36925745c52987efd5 Mon Sep 17 00:00:00 2001 From: Jody Ivan Lumbantoruan Date: Fri, 26 Jun 2026 06:07:20 +0700 Subject: [PATCH 02/15] feat(follow-along): stream the music by separating op and core with follow along --- cmd/lab/audio-stream/stream/audio_stream.go | 156 ++++++++++++++++---- files/var/www/html/stream-test.html | 40 ++++- go.mod | 1 + go.sum | 2 + 4 files changed, 170 insertions(+), 29 deletions(-) diff --git a/cmd/lab/audio-stream/stream/audio_stream.go b/cmd/lab/audio-stream/stream/audio_stream.go index e7bed2d..2d414b5 100644 --- a/cmd/lab/audio-stream/stream/audio_stream.go +++ b/cmd/lab/audio-stream/stream/audio_stream.go @@ -5,7 +5,11 @@ import ( "log" "net/http" "os" + "strconv" + "sync" + "time" + "github.com/hcl/audioduration" "github.com/julienschmidt/httprouter" ) @@ -15,16 +19,52 @@ type AudioStream struct { func (dh *AudioStream) ServeHTTP(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - fileLocation := `/home/jodiivan/go/src/github.com/jodi-ivan/numbered-notation-xml/files/audio/kj-001.mp3` + 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`, + } - file, err := os.Open(fileLocation) + 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 file.Close() + 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", "*") @@ -32,39 +72,103 @@ func (dh *AudioStream) ServeHTTP(w http.ResponseWriter, r *http.Request, ps http w.Header().Set("Transfer-Encoding", "chunked") w.Header().Set("Content-Type", "audio/mpeg") - // Create a buffer for streaming data chunks - buf := make([]byte, 4096) // 4KB chunks + 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 { + }() - select { - case <-dh.Sig: - log.Println("os signal interrupt") - return + for i, file := range files { - default: + buf := make([]byte, 4096) // 4KB chunks - } + // Loop core indefinitely + for { + select { + case <-ctx.Done(): + log.Println("context timeout") - // Read a chunk from the MP3 file - n, err := file.Read(buf) - if err != nil { - if err == io.EOF { - // Loop the audio by seeking back to the start + 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 } - // Stop streaming if the connection drops or error occurs - break - } - // Write the chunk to the HTTP response - w.Write(buf[:n]) + _, werr := w.Write(buf[:n]) + if werr != nil { + log.Println("Failed to ", werr.Error()) + return + } - // Flush the buffer to send data to the client immediately - if f, ok := w.(http.Flusher); ok { - f.Flush() - } + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + } } + // wg.Wait() + + log.Println("It is done") + } diff --git a/files/var/www/html/stream-test.html b/files/var/www/html/stream-test.html index 329c476..7a9f7ba 100644 --- a/files/var/www/html/stream-test.html +++ b/files/var/www/html/stream-test.html @@ -1,5 +1,6 @@ + @@ -14,6 +15,7 @@ margin: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } + .container { background: white; padding: 40px; @@ -22,16 +24,19 @@ text-align: center; min-width: 300px; } + h1 { color: #333; margin: 0 0 30px 0; } + .controls { display: flex; gap: 10px; justify-content: center; margin-bottom: 20px; } + button { padding: 12px 30px; font-size: 16px; @@ -41,36 +46,46 @@ transition: all 0.3s; font-weight: bold; } + .play-btn { background: #667eea; color: white; } + .play-btn:hover { background: #5568d3; } + .play-btn:disabled { background: #ccc; cursor: not-allowed; } + .stop-btn { background: #f56565; color: white; } + .stop-btn:hover { background: #e53e3e; } + .stop-btn:disabled { background: #ccc; cursor: not-allowed; } + .status { margin: 20px 0; font-size: 14px; } + .status-text { color: #666; margin-bottom: 10px; + margin-top: 20px; } + .status-indicator { display: inline-block; width: 12px; @@ -79,14 +94,24 @@ background: #ccc; margin-right: 8px; } + .status-indicator.connected { background: #48bb78; animation: pulse 1s infinite; } + @keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.5; + } } + .error { color: #f56565; margin-top: 10px; @@ -94,16 +119,19 @@ } +

🎙️ FM Stream Player

- +
+
+
Disconnected @@ -122,6 +150,8 @@

🎙️ FM Stream Player

const statusText = document.getElementById('statusText'); const statusIndicator = document.getElementById('statusIndicator'); const errorMsg = document.getElementById('errorMsg'); + const repeatInput = document.getElementById("repeat"); + function updateStatus(connected, message) { statusText.textContent = message; @@ -150,6 +180,9 @@

🎙️ FM Stream Player

audio.crossOrigin = 'anonymous'; audio.src = STREAM_URL; + if (repeatInput.value) { + audio.src += ("?repeat=" + repeatInput.value) + } // Handle audio events audio.addEventListener('play', () => { isPlaying = true; @@ -205,4 +238,5 @@

🎙️ FM Stream Player

}); + \ No newline at end of file diff --git a/go.mod b/go.mod index ce9d157..ed59b3e 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ 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 diff --git a/go.sum b/go.sum index a1df8c2..ad20a0c 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +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/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= From 7ea7784c384965585681c1b1dab1d281bc2d400c Mon Sep 17 00:00:00 2001 From: Jody Ivan Lumbantoruan Date: Tue, 30 Jun 2026 02:17:57 +0700 Subject: [PATCH 03/15] docs(credits): add midi source --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 From 9245074f6dff53905585b0f7d563862be8f42b31 Mon Sep 17 00:00:00 2001 From: Jody Ivan Lumbantoruan Date: Tue, 30 Jun 2026 02:37:21 +0700 Subject: [PATCH 04/15] fix(lyric): multi-verse coloring --- internal/lyric/coloring.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 "" } From f76155d0c77ab96722f5e537525ee672dcd2df78 Mon Sep 17 00:00:00 2001 From: Jody Ivan Lumbantoruan Date: Tue, 30 Jun 2026 10:07:14 +0700 Subject: [PATCH 05/15] feat(duration): measure counting and tempo marker --- internal/musicxml/entity.go | 8 ++++++ internal/musicxml/measure.go | 7 +++++- internal/staff/staff.go | 1 - internal/verse/firstverse.go | 1 - svc/repository/repository.go | 10 ++++++++ svc/usecase/usecase.go | 49 +++++++++++++++++++++++++++++++++--- 6 files changed, 70 insertions(+), 6 deletions(-) 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/staff/staff.go b/internal/staff/staff.go index 76fab60..44914d7 100644 --- a/internal/staff/staff.go +++ b/internal/staff/staff.go @@ -80,7 +80,6 @@ func (si *staffInteractor) RenderStaff(ctx context.Context, canv canvas.Canvas, for mi, measure := range measures { mSyllcount := 0 - measure.Build() notes := []*entity.NoteRenderer{} 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/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..a45201f 100644 --- a/svc/usecase/usecase.go +++ b/svc/usecase/usecase.go @@ -10,8 +10,10 @@ import ( "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/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 +28,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,6 +36,7 @@ func New(config config.Config, repo repository.Repository, renderer renderer.Ren config: config, repo: repo, renderer: renderer, + text: text.NewText(lyric.NewLyric()), } } @@ -61,12 +65,12 @@ func collectRepeat(measures []musicxml.Measure) [][2]int { return result } -func ProcessRepeats(music *musicxml.MusicXML) { +func ProcessRepeats(music *musicxml.MusicXML) [][2]int { repeats := collectRepeat(music.Part.Measures) if len(repeats) == 0 { - return + return nil } bli := barline.NewBarline() @@ -148,6 +152,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 +169,44 @@ func (i *interactor) RenderHymn(ctx context.Context, canv canvas.Canvas, hymnNum } } - ProcessRepeats(&music) + totalMeasure := len(music.Part.Measures) + hasRepeats := ProcessRepeats(&music) + + // count the measure + if len(hasRepeats) > 0 { + // case 2: it has repeat + for _, v := range hasRepeats { + totalMeasure += (v[1] - v[0]) + 1 + } + } + + // detect Fine in the whole music + hasFine := -1 + for _, m := range music.Part.Measures { + if i.text.MeasureHasText(m, text.DEFAULT_TEXT_FINE) { + hasFine = m.Number + break + } + + for _, n := range m.Notes { + if err != nil && i.text.NoteHasText(n.MeasureText, text.DEFAULT_TEXT_FINE) { + hasFine = m.Number + break + } + + } + if hasFine > -1 { + break + } + } + + if hasFine > -1 { + // so far, all fine is added from beginning of the music and the beginning also a refrein + totalMeasure += hasFine + } + + music.TotalMeasure = totalMeasure + music.Tempo = music.Part.Measures[0].Tempo metaData, err := i.repo.GetHymnMetaData(ctx, hymnNum, variant...) if err != nil { From 61af6693e34a8618f0f5897b35455f78468a0b94 Mon Sep 17 00:00:00 2001 From: Jody Ivan Lumbantoruan Date: Tue, 30 Jun 2026 11:42:52 +0700 Subject: [PATCH 06/15] chore(beat-counter): add beat counter for each measure --- internal/staff/render.go | 3 +++ internal/staff/staff.go | 5 ++++- internal/staff/type.go | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/staff/render.go b/internal/staff/render.go index 73a6994..24f60e3 100644 --- a/internal/staff/render.go +++ b/internal/staff/render.go @@ -27,6 +27,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 +41,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 +79,7 @@ func (si *staffInteractor) Render(ctx context.Context, canv canvas.Canvas, part RepeatInfo: info.RepeatInfo, SyllableOffset: info.SyllableOffset, + TotalBeat: info.TotalBeat, } x = staffLines.GetLeftIndent(info.NextLineRenderer[0].MeasureNumber) idx := len(staffes) - 1 diff --git a/internal/staff/staff.go b/internal/staff/staff.go index 44914d7..6a8144d 100644 --- a/internal/staff/staff.go +++ b/internal/staff/staff.go @@ -77,6 +77,9 @@ 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 @@ -111,7 +114,7 @@ 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) - + staffInfo.TotalBeat[measure.Number] += noteLength 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 diff --git a/internal/staff/type.go b/internal/staff/type.go index 64eedf7..f616b65 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 ( From b2377e0b84ecfce9aa9e74c0d6be2fc01512b55c Mon Sep 17 00:00:00 2001 From: Jody Ivan Lumbantoruan Date: Tue, 30 Jun 2026 12:06:17 +0700 Subject: [PATCH 07/15] feat(measure): print measure number at the start of the staff --- internal/staff/lines/lines.go | 6 ++++++ 1 file changed, 6 insertions(+) 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() From c306d0acf60d1aabf984ae35ddb5356e2ddfa545 Mon Sep 17 00:00:00 2001 From: Jody Ivan Lumbantoruan Date: Tue, 30 Jun 2026 18:51:28 +0700 Subject: [PATCH 08/15] feat(playback): measure steps generator --- internal/playback/steps.go | 49 +++++++++++++++++++++++++++++++++++ internal/playback/type.go | 9 +++++++ internal/renderer/renderer.go | 2 +- internal/staff/render.go | 2 +- svc/usecase/usecase.go | 41 +++++++++++++++-------------- 5 files changed, 81 insertions(+), 22 deletions(-) create mode 100644 internal/playback/steps.go create mode 100644 internal/playback/type.go 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..fd0758f --- /dev/null +++ b/internal/playback/type.go @@ -0,0 +1,9 @@ +package playback + +type Step struct { + // Duration time.Duration `json:"duration,omitempty"` + LyricPart int `json:"lyric_part,omitempty"` + MeasureNumber int `json:"measure_number"` + + // Rect [2]entity.Coordinate `json:"rect,omitempty"` +} 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/render.go b/internal/staff/render.go index 24f60e3..35f82d8 100644 --- a/internal/staff/render.go +++ b/internal/staff/render.go @@ -79,7 +79,7 @@ func (si *staffInteractor) Render(ctx context.Context, canv canvas.Canvas, part RepeatInfo: info.RepeatInfo, SyllableOffset: info.SyllableOffset, - TotalBeat: info.TotalBeat, + TotalBeat: info.TotalBeat, // TODO: send this to result useacase } x = staffLines.GetLeftIndent(info.NextLineRenderer[0].MeasureNumber) idx := len(staffes) - 1 diff --git a/svc/usecase/usecase.go b/svc/usecase/usecase.go index a45201f..852fe01 100644 --- a/svc/usecase/usecase.go +++ b/svc/usecase/usecase.go @@ -1,10 +1,12 @@ package usecase import ( + "cmp" "context" "encoding/json" "fmt" "log" + "slices" "strconv" "github.com/jodi-ivan/numbered-notation-xml/internal/barline" @@ -12,6 +14,7 @@ import ( "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" @@ -40,32 +43,38 @@ func New(config config.Config, repo repository.Repository, renderer renderer.Ren } } -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) [][2]int { +func ProcessRepeats(music *musicxml.MusicXML) [][3]int { repeats := collectRepeat(music.Part.Measures) @@ -169,17 +178,8 @@ func (i *interactor) RenderHymn(ctx context.Context, canv canvas.Canvas, hymnNum } } - totalMeasure := len(music.Part.Measures) hasRepeats := ProcessRepeats(&music) - // count the measure - if len(hasRepeats) > 0 { - // case 2: it has repeat - for _, v := range hasRepeats { - totalMeasure += (v[1] - v[0]) + 1 - } - } - // detect Fine in the whole music hasFine := -1 for _, m := range music.Part.Measures { @@ -189,7 +189,7 @@ func (i *interactor) RenderHymn(ctx context.Context, canv canvas.Canvas, hymnNum } for _, n := range m.Notes { - if err != nil && i.text.NoteHasText(n.MeasureText, text.DEFAULT_TEXT_FINE) { + if i.text.NoteHasText(n.MeasureText, text.DEFAULT_TEXT_FINE) { hasFine = m.Number break } @@ -200,12 +200,10 @@ func (i *interactor) RenderHymn(ctx context.Context, canv canvas.Canvas, hymnNum } } - if hasFine > -1 { - // so far, all fine is added from beginning of the music and the beginning also a refrein - totalMeasure += hasFine - } + slices.SortFunc(hasRepeats, func(i, j [3]int) int { + return cmp.Compare(i[0], j[0]) + }) - music.TotalMeasure = totalMeasure music.Tempo = music.Part.Measures[0].Tempo metaData, err := i.repo.GetHymnMetaData(ctx, hymnNum, variant...) @@ -240,6 +238,9 @@ 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) canv.Delegator().OnBeforeStartWrite() From 7d8befb79eae2bc0fed3fe68c02360495e5a6e8a Mon Sep 17 00:00:00 2001 From: Jody Ivan Lumbantoruan Date: Tue, 30 Jun 2026 19:07:51 +0700 Subject: [PATCH 09/15] refactor(measure-text): find measure by text --- internal/staff/text/text.go | 18 ++++++++++++++++++ svc/usecase/usecase.go | 19 +------------------ 2 files changed, 19 insertions(+), 18 deletions(-) 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/svc/usecase/usecase.go b/svc/usecase/usecase.go index 852fe01..cc3a1af 100644 --- a/svc/usecase/usecase.go +++ b/svc/usecase/usecase.go @@ -181,24 +181,7 @@ func (i *interactor) RenderHymn(ctx context.Context, canv canvas.Canvas, hymnNum hasRepeats := ProcessRepeats(&music) // detect Fine in the whole music - hasFine := -1 - for _, m := range music.Part.Measures { - if i.text.MeasureHasText(m, text.DEFAULT_TEXT_FINE) { - hasFine = m.Number - break - } - - for _, n := range m.Notes { - if i.text.NoteHasText(n.MeasureText, text.DEFAULT_TEXT_FINE) { - hasFine = m.Number - break - } - - } - if hasFine > -1 { - break - } - } + 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]) From 540827e70ad7312d254e519b5bd6fd33f146233e Mon Sep 17 00:00:00 2001 From: Jody Ivan Lumbantoruan Date: Tue, 30 Jun 2026 19:34:24 +0700 Subject: [PATCH 10/15] test(measure-text): regen mock --- internal/staff/text/text_mock.go | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) 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) From 1c02909f0a1f5033a162de7a1a73f5395dfab415 Mon Sep 17 00:00:00 2001 From: Jody Ivan Lumbantoruan Date: Tue, 30 Jun 2026 23:28:07 +0700 Subject: [PATCH 11/15] chore(playback): rect for measure --- internal/playback/type.go | 14 ++++++++++---- internal/staff/align.go | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/internal/playback/type.go b/internal/playback/type.go index fd0758f..f1869c3 100644 --- a/internal/playback/type.go +++ b/internal/playback/type.go @@ -1,9 +1,15 @@ package playback +import ( + "time" + + "github.com/jodi-ivan/numbered-notation-xml/internal/entity" +) + type Step struct { - // Duration time.Duration `json:"duration,omitempty"` - LyricPart int `json:"lyric_part,omitempty"` - MeasureNumber int `json:"measure_number"` + Duration time.Duration `json:"duration"` + LyricPart int `json:"lyric_part"` + MeasureNumber int `json:"measure_number"` - // Rect [2]entity.Coordinate `json:"rect,omitempty"` + Rect [2]entity.Coordinate `json:"rect"` } diff --git a/internal/staff/align.go b/internal/staff/align.go index 4438fc8..d2279da 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" @@ -176,6 +178,25 @@ func (rsa *renderStaffAlign) RenderWithAlign(ctx context.Context, canv canvas.Ca offsetLyric := 0 for mi, measure := range noteRenderer { + 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)) + canv.Rect(rectX, y, + rectX1-rectX, + ((rectY)+(yPos-y))-y, + "fill:none;stroke:#FF0000;stroke-linecap:round;stroke-width:0.9") + canv.Group("class='measure-align'", fmt.Sprintf("number='%d'", measure[0].MeasureNumber)) prev := []*entity.NoteRenderer{} From a4bdc3f9475ce08697866f84d5fd48c6858c7f61 Mon Sep 17 00:00:00 2001 From: Jody Ivan Lumbantoruan Date: Wed, 1 Jul 2026 11:49:42 +0700 Subject: [PATCH 12/15] feat(playback): send back the needed data to usecase --- internal/staff/align.go | 40 ++++++++++++++++++++++++---------------- internal/staff/render.go | 6 ++++++ svc/usecase/usecase.go | 11 ++++++++++- utils/params/paramctx.go | 1 + utils/params/playback.go | 8 ++++++++ 5 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 utils/params/playback.go diff --git a/internal/staff/align.go b/internal/staff/align.go index d2279da..1a3524f 100644 --- a/internal/staff/align.go +++ b/internal/staff/align.go @@ -22,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 { @@ -178,24 +179,31 @@ func (rsa *renderStaffAlign) RenderWithAlign(ctx context.Context, canv canvas.Ca offsetLyric := 0 for mi, measure := range noteRenderer { - max := slices.MaxFunc(measure, func(a, b *entity.NoteRenderer) int { - return cmp.Compare(len(a.Lyric), len(b.Lyric)) - }) + 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 + 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)), + }) } - rectY := (y + lyric.DISTANCE_NOTE_TO_LYRIC + (len(max.Lyric) * lyric.LINE_BETWEEN_LYRIC)) - canv.Rect(rectX, y, - rectX1-rectX, - ((rectY)+(yPos-y))-y, - "fill:none;stroke:#FF0000;stroke-linecap:round;stroke-width:0.9") canv.Group("class='measure-align'", fmt.Sprintf("number='%d'", measure[0].MeasureNumber)) diff --git a/internal/staff/render.go b/internal/staff/render.go index 35f82d8..040c1df 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 { @@ -90,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/svc/usecase/usecase.go b/svc/usecase/usecase.go index cc3a1af..f49cb10 100644 --- a/svc/usecase/usecase.go +++ b/svc/usecase/usecase.go @@ -224,10 +224,19 @@ func (i *interactor) RenderHymn(ctx context.Context, canv canvas.Canvas, hymnNum 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) 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..bfc7031 --- /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 +} From 40ed8dcd4d4545e1cfc6f20c3fb29222c464a9d7 Mon Sep 17 00:00:00 2001 From: Jody Ivan Lumbantoruan Date: Wed, 1 Jul 2026 14:33:56 +0700 Subject: [PATCH 13/15] feat(playback): split staff beat --- internal/playback/playback.go | 74 +++++++++++++++++++++++++++++++++++ internal/playback/type.go | 2 + internal/renderer/renderer.go | 2 +- internal/staff/render.go | 2 +- internal/staff/staff.go | 12 +++++- internal/staff/type.go | 4 +- svc/usecase/usecase.go | 11 ++++-- utils/params/playback.go | 2 +- 8 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 internal/playback/playback.go 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/type.go b/internal/playback/type.go index f1869c3..6e7e3fd 100644 --- a/internal/playback/type.go +++ b/internal/playback/type.go @@ -8,8 +8,10 @@ import ( 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 c327875..e9cfab8 100644 --- a/internal/renderer/renderer.go +++ b/internal/renderer/renderer.go @@ -91,7 +91,7 @@ func (ir *rendererInteractor) Render(ctx context.Context, music musicxml.MusicXM ir.Footnote.RenderTitleFootnotes(canv, relativeY, metadata.HymnData) } canv.Start(constant.LAYOUT_WIDTH, canvHeight) - canv.End() + // canv.End() } diff --git a/internal/staff/render.go b/internal/staff/render.go index 040c1df..ca875f8 100644 --- a/internal/staff/render.go +++ b/internal/staff/render.go @@ -28,7 +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{}, + TotalBeat: map[int][]float64{}, } oldMarginButtom := 0 for i, st := range staffes { diff --git a/internal/staff/staff.go b/internal/staff/staff.go index 6a8144d..91ef3f3 100644 --- a/internal/staff/staff.go +++ b/internal/staff/staff.go @@ -114,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) - staffInfo.TotalBeat[measure.Number] += noteLength 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 @@ -338,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/type.go b/internal/staff/type.go index f616b65..cc53606 100644 --- a/internal/staff/type.go +++ b/internal/staff/type.go @@ -19,7 +19,7 @@ type StaffInfo struct { SyllableCount int RepeatInfo []*musicxml.RepeatInfo SyllableOffset map[int]int - TotalBeat map[int]float64 + TotalBeat map[int][]float64 } type StaffData struct { @@ -32,7 +32,7 @@ type StaffData struct { RepeatInfo []*musicxml.RepeatInfo SyllableOffset map[int]int - TotalBeat map[int]float64 + TotalBeat map[int][]float64 } const ( diff --git a/svc/usecase/usecase.go b/svc/usecase/usecase.go index f49cb10..aef8123 100644 --- a/svc/usecase/usecase.go +++ b/svc/usecase/usecase.go @@ -224,9 +224,7 @@ func (i *interactor) RenderHymn(ctx context.Context, canv canvas.Canvas, hymnNum 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{}, - } + param.Playback = ¶ms.PlaybackParams{Rect: map[int][][2]entity.Coordinate{}} for _, s := range steps { param.Playback.Rect[s.MeasureNumber] = [][2]entity.Coordinate{} @@ -237,6 +235,13 @@ func (i *interactor) RenderHymn(ctx context.Context, canv canvas.Canvas, hymnNum canv.Delegator().OnBeforeStartWrite() i.renderer.Render(rctx, music, canv, metaWithParsedVerse) + t := playback.CalculateDuration(&steps, music.Tempo, param.Playback.TotalBeat, param.Playback.Rect) + canv.Text(constant.LAYOUT_INDENT_LENGTH, 100, t.String()) + for _, rect := range steps { + canv.Rect(int(rect.Rect[0].X), int(rect.Rect[0].Y), int(rect.Rect[1].X), int(rect.Rect[1].Y), "fill:none;stroke:#FF0000;stroke-linecap:round;stroke-width:0.9") + canv.Text(int(rect.Rect[0].X), int(rect.Rect[0].Y), rect.DurationStr) + } + canv.End() return nil } diff --git a/utils/params/playback.go b/utils/params/playback.go index bfc7031..60d5128 100644 --- a/utils/params/playback.go +++ b/utils/params/playback.go @@ -4,5 +4,5 @@ import "github.com/jodi-ivan/numbered-notation-xml/internal/entity" type PlaybackParams struct { Rect map[int][][2]entity.Coordinate - TotalBeat map[int]float64 + TotalBeat map[int][]float64 } From 4ddf1d3b9d96cef7a563af38e71b40bbfdf321e4 Mon Sep 17 00:00:00 2001 From: Jody Ivan Lumbantoruan Date: Wed, 1 Jul 2026 18:16:25 +0700 Subject: [PATCH 14/15] chore(clean up): clean up debugger --- internal/renderer/renderer.go | 2 +- svc/usecase/usecase.go | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go index e9cfab8..c327875 100644 --- a/internal/renderer/renderer.go +++ b/internal/renderer/renderer.go @@ -91,7 +91,7 @@ func (ir *rendererInteractor) Render(ctx context.Context, music musicxml.MusicXM ir.Footnote.RenderTitleFootnotes(canv, relativeY, metadata.HymnData) } canv.Start(constant.LAYOUT_WIDTH, canvHeight) - // canv.End() + canv.End() } diff --git a/svc/usecase/usecase.go b/svc/usecase/usecase.go index aef8123..82b78ae 100644 --- a/svc/usecase/usecase.go +++ b/svc/usecase/usecase.go @@ -235,13 +235,7 @@ func (i *interactor) RenderHymn(ctx context.Context, canv canvas.Canvas, hymnNum canv.Delegator().OnBeforeStartWrite() i.renderer.Render(rctx, music, canv, metaWithParsedVerse) - t := playback.CalculateDuration(&steps, music.Tempo, param.Playback.TotalBeat, param.Playback.Rect) - canv.Text(constant.LAYOUT_INDENT_LENGTH, 100, t.String()) - for _, rect := range steps { - canv.Rect(int(rect.Rect[0].X), int(rect.Rect[0].Y), int(rect.Rect[1].X), int(rect.Rect[1].Y), "fill:none;stroke:#FF0000;stroke-linecap:round;stroke-width:0.9") - canv.Text(int(rect.Rect[0].X), int(rect.Rect[0].Y), rect.DurationStr) - } - canv.End() + playback.CalculateDuration(&steps, music.Tempo, param.Playback.TotalBeat, param.Playback.Rect) return nil } From eee137164dc2eaaefee82f09134143a80a4188c8 Mon Sep 17 00:00:00 2001 From: Jody Ivan Lumbantoruan Date: Fri, 3 Jul 2026 01:10:14 +0700 Subject: [PATCH 15/15] chore(dll): protobuf file --- go.mod | 3 +- go.sum | 2 + protobuf/hymn.pb.go | 428 ++++++++++++++++++++++++++++++++++++++++++++ protobuf/hymn.proto | 36 ++++ 4 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 protobuf/hymn.pb.go create mode 100644 protobuf/hymn.proto diff --git a/go.mod b/go.mod index ed59b3e..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 @@ -22,6 +22,7 @@ require ( 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 ad20a0c..9be7dd0 100644 --- a/go.sum +++ b/go.sum @@ -66,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/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