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
19 changes: 19 additions & 0 deletions JianpuEditor.Tests/Rendering/AccidentalRenderingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,25 @@ public void RenderToBitmap_BreathMark_DoesNotThrowInCompactOrDefaultLayout()
}
}

[Fact]
public void RenderToBitmap_SegnoAndCoda_DoesNotThrowInCompactOrDefaultLayout()
{
var measure = ScoreTestHelper.Measure(ScoreTestHelper.Note(1), ScoreTestHelper.Note(2));
measure.Ornaments = new List<JianpuOrnament>
{
new JianpuOrnament { Type = OrnamentType.Segno, NoteIndex = 0 },
new JianpuOrnament { Type = OrnamentType.Coda, NoteIndex = 1 }
};
OrnamentService.NormalizeMeasure(measure);

var score = ScoreTestHelper.CreateScore(measure);
using (var renderer = new JianpuRenderer())
{
Assert.NotNull(renderer.RenderToBitmap(score, 1280, ScoreLayoutOptions.Editor));
Assert.NotNull(renderer.RenderToBitmap(score, 1280, ScoreLayoutOptions.Default));
}
}

[Fact]
public void GetAccidentalMark_ReturnsSharpOrFlat()
{
Expand Down
2 changes: 2 additions & 0 deletions JianpuEditor.Tests/Services/OrnamentServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ public void GetPlaceholderGlyph_ReturnsToolbarLabel()
Assert.Equal("stac", OrnamentService.GetPlaceholderGlyph(OrnamentType.Staccato));
Assert.Equal("acc", OrnamentService.GetPlaceholderGlyph(OrnamentType.Accent));
Assert.Equal("ten", OrnamentService.GetPlaceholderGlyph(OrnamentType.Tenuto));
Assert.Equal("segno", OrnamentService.GetPlaceholderGlyph(OrnamentType.Segno));
Assert.Equal("coda", OrnamentService.GetPlaceholderGlyph(OrnamentType.Coda));
}

[Fact]
Expand Down
2 changes: 2 additions & 0 deletions JianpuEditor/Controls/RibbonIcon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public enum RibbonIcon
Staccato,
Accent,
Tenuto,
Segno,
Coda,
Duplicate,
Delete,
Library,
Expand Down
10 changes: 10 additions & 0 deletions JianpuEditor/Controls/RibbonIconRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,16 @@ private static void DrawIcon(Graphics g, RibbonIcon icon, Pen pen, Brush brush)
g.DrawLine(tenutoPen, 4f, 10f, 16f, 10f);
}

return;
case RibbonIcon.Segno:
g.DrawLine(pen, 5f, 15f, 15f, 5f);
g.FillEllipse(brush, 5f, 5f, 2.6f, 2.6f);
g.FillEllipse(brush, 12.4f, 12.4f, 2.6f, 2.6f);
return;
case RibbonIcon.Coda:
g.DrawEllipse(pen, 5f, 5f, 10f, 10f);
g.DrawLine(pen, 10f, 2f, 10f, 18f);
g.DrawLine(pen, 2f, 10f, 18f, 10f);
return;
case RibbonIcon.Duplicate:
g.DrawRectangle(pen, 3f, 6f, 10f, 10f);
Expand Down
14 changes: 13 additions & 1 deletion JianpuEditor/MainForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,14 @@ private void PopulateMenuStrip(MenuStrip menu)
"Tenuto",
Keys.None,
(s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Tenuto))));
ornamentMenu.DropDownItems.Add(CreateMenuItem(
"Segno",
Keys.None,
(s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Segno))));
ornamentMenu.DropDownItems.Add(CreateMenuItem(
"Coda",
Keys.None,
(s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Coda))));
editMenu.DropDownItems.Add(ornamentMenu);
editMenu.DropDownItems.Add(CreateMenuItem("Clear Score", Keys.None, OnClearScore));

Expand Down Expand Up @@ -822,7 +830,9 @@ private FlowLayoutPanel BuildToolbarPanel()
ornaments.AddRow(
CreateRibbonButton(RibbonIcon.Staccato, "Staccato", () => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Staccato)), compact: true),
CreateRibbonButton(RibbonIcon.Accent, "Accent", () => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Accent)), compact: true),
CreateRibbonButton(RibbonIcon.Tenuto, "Tenuto", () => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Tenuto)), compact: true));
CreateRibbonButton(RibbonIcon.Tenuto, "Tenuto", () => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Tenuto)), compact: true),
CreateRibbonButton(RibbonIcon.Segno, "Segno", () => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Segno)), compact: true),
CreateRibbonButton(RibbonIcon.Coda, "Coda", () => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Coda)), compact: true));
panel.Controls.Add(ornaments);

var measures = new RibbonGroup("Measures");
Expand Down Expand Up @@ -1176,6 +1186,8 @@ private void AddNoteContextMenuItems(ContextMenuStrip menu)
ornamentsMenu.DropDownItems.Add("Staccato", null, (s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Staccato)));
ornamentsMenu.DropDownItems.Add("Accent", null, (s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Accent)));
ornamentsMenu.DropDownItems.Add("Tenuto", null, (s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Tenuto)));
ornamentsMenu.DropDownItems.Add("Segno", null, (s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Segno)));
ornamentsMenu.DropDownItems.Add("Coda", null, (s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Coda)));
menu.Items.Add(ornamentsMenu);

AddPasteItemIfAvailable(menu);
Expand Down
4 changes: 3 additions & 1 deletion JianpuEditor/Rendering/JianpuRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1368,7 +1368,9 @@ private static bool UsesLatinOrnamentFont(OrnamentType type)
|| type == OrnamentType.BreathMark
|| type == OrnamentType.Staccato
|| type == OrnamentType.Accent
|| type == OrnamentType.Tenuto;
|| type == OrnamentType.Tenuto
|| type == OrnamentType.Segno
|| type == OrnamentType.Coda;
}

private static float GetOrnamentAnchorX(OrnamentType type, int noteX, int noteWidth)
Expand Down
2 changes: 2 additions & 0 deletions JianpuEditor/Rendering/NoteTopAnnotationPlanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ private static void AnalyzeOrnaments(IReadOnlyList<JianpuOrnament> ornaments, No
case OrnamentType.Staccato:
case OrnamentType.Accent:
case OrnamentType.Tenuto:
case OrnamentType.Segno:
case OrnamentType.Coda:
layout.HasCenterOrnament = true;
break;
}
Expand Down
4 changes: 4 additions & 0 deletions JianpuEditor/Services/OrnamentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ public static string GetPlaceholderGlyph(OrnamentType type)
return "acc";
case OrnamentType.Tenuto:
return "ten";
case OrnamentType.Segno:
return "segno";
case OrnamentType.Coda:
return "coda";
default:
return type.ToString();
}
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ A Jianpu (numbered musical notation) editing tool built on C# WinForms, supporti
- Menu **Edit → Undo** (**Ctrl+Z**) / **Redo** (**Ctrl+Y**) steps backward or forward through score edits
- Covers note editing, deletion, measures, transposition, header/lyric/chord inline editing, ties, ornaments, bulk lyric editing, etc.; the undo/redo stack is cleared after creating, opening, or loading a score
- **Ornaments**
- Toolbar "Ornaments" section: **Grace note** / **Trill** / **Turn** / **Mordent** / **Fermata** / **Breath Mark** / **Staccato** / **Accent** / **Tenuto**; the menu **Edit → Ornaments** provides the same options
- Toolbar "Ornaments" section: **Grace note** / **Trill** / **Turn** / **Mordent** / **Fermata** / **Breath Mark** / **Staccato** / **Accent** / **Tenuto** / **Segno** / **Coda**; the menu **Edit → Ornaments** provides the same options
- Select one or more notes first, then click an ornament button; with multiple selection, ornaments are added in batch
- Clicking the same button again removes that type of ornament from the note (other types are kept)
- Delete / "Delete" removes ornaments on the selected note first
- The canvas and PDF export draw placeholder symbols above the note (grace / tr / turn / fermata / stac / acc / ten); a breath mark draws just after the note instead, matching where it's placed in notasi angka/jianpu sheet music
- Score playback and MIDI export expand grace notes, trills, turns, mordents, and fermata durations; staccato shortens the sounding duration, accent and tenuto boost velocity (accent more than tenuto); a breath mark is visual-only and doesn't affect playback/export
- The canvas and PDF export draw placeholder symbols above the note (grace / tr / turn / fermata / stac / acc / ten / segno / coda); a breath mark draws just after the note instead, matching where it's placed in notasi angka/jianpu sheet music
- Score playback and MIDI export expand grace notes, trills, turns, mordents, and fermata durations; staccato shortens the sounding duration, accent and tenuto boost velocity (accent more than tenuto); a breath mark, Segno, and Coda are visual-only and don't affect playback/export -- Segno/Coda mark where a D.S./D.C. jump would go, but the jump itself isn't performed during playback yet
- **Ties**
- Click "Tie" → select the start note → select the end note; Esc to cancel
- The end note must be the same pitch as the start note (a tie sustains one pitch); picking a
Expand Down Expand Up @@ -185,7 +185,7 @@ You can also manually specify a version number in GitHub under **Actions → Rel
| Split / Merge | Splits or merges the duration of selected notes |
| Toolbar "From / To" + Copy Measures | Copies measures within the specified range |
| Tie | Click "Tie" → select the start/end note; click the arc to select it, Delete to remove |
| Ornaments | Select a note, then click "Grace Note / Trill / Turn / Mordent / Fermata / Breath Mark / Staccato / Accent / Tenuto" in the toolbar; click the same button again to remove it |
| Ornaments | Select a note, then click "Grace Note / Trill / Turn / Mordent / Fermata / Breath Mark / Staccato / Accent / Tenuto / Segno / Coda" in the toolbar; click the same button again to remove it |
| Undo / Redo | **Edit → Undo / Redo** or **Ctrl+Z** / **Ctrl+Y** |
| Play / Stop | Plays the score according to BPM; drag the blue progress bar to seek |
| Transpose | Menu "Edit → Chord Transpose..."; transposes chord markers only |
Expand Down
36 changes: 23 additions & 13 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@ Until now, playback always used whatever General MIDI patch 0 (Acoustic Grand Pi

## Indonesian notasi angka completeness (gap analysis + plan)

**Status: phase 3's bug-fix half, phase 4 (all of it except Glissando), phase 6 (breath marks), and
phase 10 (pickup measure verification) are done. Everything else below is still not started**,
including phases 1-2 (`NotationStyle` + the accidental slash convention) -- see the note under
phase 2 for why that one turned out to be more involved than it looked, and the new note there
about the natural sign specifically. Phase 10 also surfaced a separate, real gap in MIDI import
(pickup measures aren't preserved) -- see the note under phase 10.
**Status: phase 3's bug-fix half, phase 4 (all of it except Glissando), phase 6 (breath marks),
the Segno/Coda half of phase 7, and phase 10 (pickup measure verification) are done. Everything
else below is still not started**, including phases 1-2 (`NotationStyle` + the accidental slash
convention) -- see the note under phase 2 for why that one turned out to be more involved than it
looked, and the new note there about the natural sign specifically. Phase 10 also surfaced a
separate, real gap in MIDI import (pickup measures aren't preserved) -- see the note under phase
10.

Researched Indonesian *notasi angka* (Indonesian numbered/jianpu notation — rules, symbols,
conventions) against what `JianpuEditor` actually implements. Full gap list and phased plan below.
Expand Down Expand Up @@ -153,13 +154,22 @@ here and intentionally excluded.*
participate in `NoteTopAnnotationPlanner`'s accidental/octave-dot collision math at all, so it
carries none of the layout risk flagged under phase 2. Visual-only, no playback/MIDI-export
effect (a version that inserts a micro-rest is a possible follow-up, not v1).
7. **Repeat bar lines, volta brackets, and D.C./D.S./Coda/Segno navigation.** Two sub-parts with
very different risk:
- **Visual-only** (lower risk): a `BarLineType` field per measure boundary (single/double/
final/repeat-start/repeat-end), a volta-bracket span (which measures, which ending number)
for 1st/2nd endings, and `Segno`/`Coda` as measure-anchored markers using the `OrnamentType`
values that already exist. Playback/MIDI export keep playing straight through once, same as
today, with a status message noting repeat structure isn't performed.
7. **Repeat bar lines, volta brackets, and D.C./D.S./Coda/Segno navigation.** Three sub-parts:
- **Segno/Coda markers — done.** Wired up via the exact same ribbon/Edit-menu/context-menu/
glyph pattern as Staccato/Accent/Tenuto, using the `OrnamentType.Segno`/`Coda` values that
already existed. These two are genuinely note/beat-anchored point symbols in real notation
(unlike a repeat bar line, which decorates the barline itself, spanning the staff height) —
see the note below on why `RepeatStart`/`RepeatEnd` were deliberately *not* wired the same
way. Visual-only: no playback/MIDI-export effect, since jumping to a Segno/Coda isn't
performed (see the third bullet below).
- **Repeat bar lines and volta brackets — not started.** A `BarLineType` field per measure
boundary (single/double/final/repeat-start/repeat-end) and a volta-bracket span (which
measures, which ending number) for 1st/2nd endings. `OrnamentType.RepeatStart`/`RepeatEnd`
exist in the enum but are deliberately being left unwired rather than reused for this — a
repeat bar line isn't a note-anchored ornament the way Segno/Coda are, it's a property of the
measure boundary itself (a thick double bar with dots, spanning the full staff height), so
modeling it as a `BarLineType` field (as originally planned) is the right shape, not a UI
wiring exercise on the existing enum values.
- **Actually performing the repeat/jump/volta-skip during playback and MIDI export** (higher
risk): needs real changes to `ScoreMidiSchedule`/`ScorePlaybackService`/`MidiExportService`'s
scheduling, which today assumes one linear pass through the measures. Worth scoping and
Expand Down