Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
462 changes: 378 additions & 84 deletions internal/jazz/lilypond.go

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions internal/jazz/musicxml_read.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
"unicode/utf8"
Expand Down Expand Up @@ -421,6 +422,21 @@ func ParseMusicXML(r io.Reader) (*Tune, error) {

numHarmonies := len(harmonyEvents)
if numHarmonies > 0 {
sort.SliceStable(harmonyEvents, func(i, j int) bool {
return harmonyEvents[i].division < harmonyEvents[j].division
})

// Deduplicate harmonies that fall on the exact same division
var deduped []harmonyAt
for _, h := range harmonyEvents {
if len(deduped) > 0 && deduped[len(deduped)-1].division == h.division {
continue
}
deduped = append(deduped, h)
}
harmonyEvents = deduped
numHarmonies = len(harmonyEvents)

hasNoteTiming := maxCursor > 0 && divisions > 0

for i, h := range harmonyEvents {
Expand Down
9 changes: 8 additions & 1 deletion internal/server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,22 @@ func (s *Server) handleAnalyze(w http.ResponseWriter, r *http.Request) {
// Run tonal center analysis and device detection
jazz.AnalyzeTonalCenters(tune)
devices := jazz.DetectHarmonicDevices(tune)
if devices == nil {
devices = []jazz.HarmonicDevice{}
}
journeySpans := jazz.GetHarmonicJourneySpans(tune)
if journeySpans == nil {
journeySpans = []jazz.JourneySpan{}
}
journeyStr := jazz.HarmonicJourney(tune)

var measuresDTO []MeasureDTO
measuresDTO := make([]MeasureDTO, 0, len(tune.Measures))
pairIdx := 0

for _, m := range tune.Measures {
mDTO := MeasureDTO{
Number: m.Number,
Chords: make([]ChordDTO, 0, len(m.Chords)),
}
for _, tc := range m.Chords {
var p jazz.GuideTonePair
Expand Down
14 changes: 14 additions & 0 deletions internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,18 @@ func TestAnalyzeEndpoint_StandardAutumnLeaves(t *testing.T) {
if len(resp.Measures) == 0 {
t.Errorf("expected measures in Autumn Leaves, got 0")
}

rawJSON := rec.Body.String()
if strings.Contains(rawJSON, `"chords":null`) {
t.Errorf("expected non-null chords in JSON, but found '\"chords\":null'")
}
if resp.Measures[0].Chords == nil {
t.Errorf("expected Measure[0].Chords to be non-nil empty slice, got nil")
}
if resp.Devices == nil {
t.Errorf("expected Devices to be non-nil slice, got nil")
}
if resp.JourneySpans == nil {
t.Errorf("expected JourneySpans to be non-nil slice, got nil")
}
}

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion web/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎷</text></svg>" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>jazz-tools - Harmonic Analysis & Companion Generator</title>
<script type="module" crossorigin src="/assets/index-BU2UpVYr.js"></script>
<script type="module" crossorigin src="/assets/index-BEh0cyCH.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DK9tBSLp.css">
</head>
<body class="bg-slate-50 text-slate-900 min-h-screen">
Expand Down
2 changes: 1 addition & 1 deletion web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ export const App: React.FC = () => {
Key: {analysis.key || 'Modal/C'}
</span>
<span className="px-2.5 py-1 rounded-md bg-slate-100 text-slate-700 text-xs font-semibold">
Time: {analysis.timeSignature[0]}/{analysis.timeSignature[1]}
Time: {analysis.timeSignature ? `${analysis.timeSignature[0]}/${analysis.timeSignature[1]}` : '4/4'}
</span>
<span className="px-2.5 py-1 rounded-md bg-slate-100 text-slate-700 text-xs font-semibold">
{analysis.measureCount} Measures
Expand Down
5 changes: 4 additions & 1 deletion web/src/components/ChordsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { MeasureDTO } from '../types';
export const ChordsTable: React.FC<{ measures: MeasureDTO[] }> = ({ measures }) => {
if (!measures || measures.length === 0) return null;

const hasAnyChords = measures.some((m) => m.chords && m.chords.length > 0);
if (!hasAnyChords) return null;

return (
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="px-5 py-3 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center">
Expand All @@ -25,7 +28,7 @@ export const ChordsTable: React.FC<{ measures: MeasureDTO[] }> = ({ measures })
</thead>
<tbody className="divide-y divide-slate-100 font-mono">
{measures.flatMap((m) =>
m.chords.map((c, cIdx) => (
(m.chords || []).map((c, cIdx) => (
<tr key={`${m.number}-${cIdx}`} className="hover:bg-slate-50/80 transition-colors">
<td className="py-2 px-4 font-semibold text-slate-700">
{cIdx === 0 ? m.number : ''}
Expand Down
15 changes: 10 additions & 5 deletions web/src/components/ScoreViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,16 @@ export const ScoreViewer: React.FC<Props> = ({ xmlContent, hasMelody }) => {
});
}

osmdRef.current.load(xmlContent).then(() => {
osmdRef.current?.render();
}).catch(err => {
console.error("OSMD render error:", err);
});
try {
osmdRef.current.clear();
osmdRef.current.load(xmlContent).then(() => {
osmdRef.current?.render();
}).catch(err => {
console.error("OSMD render error:", err);
});
} catch (err) {
console.error("OSMD load error:", err);
}
}, [xmlContent]);

return (
Expand Down
2 changes: 1 addition & 1 deletion web/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export interface ChordDTO {

export interface MeasureDTO {
number: number;
chords: ChordDTO[];
chords?: ChordDTO[];
melody?: MelodyNoteDTO[];
}

Expand Down
Loading