flutter_notemus 0.1.0
flutter_notemus: ^0.1.0 copied to clipboard
Professional music notation rendering for Flutter with complete SMuFL support. Features 2932 glyphs, JSON import/export, automatic validation, and advanced beaming system.
🎵 Flutter Notemus #
A powerful Flutter package for professional music notation rendering with complete SMuFL support.
Flutter Notemus provides a comprehensive solution for rendering high-quality music notation in Flutter applications. Built on the SMuFL (Standard Music Font Layout) specification, it offers precise, professional-grade music engraving.
📑 Table of Contents #
- 📸 Examples
- ✨ Features
- 📊 JSON Format Reference
- 📖 Quick Start
- ⚠️ Measure Validation System
- 🎼 Advanced Examples
- ⚙️ Advanced Customization
- 🏗️ Architecture
- 👥 Contributing
- 📄 License
📸 Examples #
Professional Music Notation Rendering #
[Ode à Alegria - Complete Score]
Complete "Ode à Alegria" with professional engraving, dynamics, and proper spacing
[Detailed Music Elements]
Dotted notes, breath marks, and precise SMuFL glyph rendering
🎯 Key Highlights #
- 🎼 2932 SMuFL Glyphs - Complete Bravura font support
- 📊 JSON Import/Export - Professional format with 12+ element types
- ✅ Auto Validation - Music theory-based measure validation
- 🎨 Professional Layout - Horizontal justification, smart spacing
- 🔄 Repeat Signs - Full ritornelo support (
:||,||:,:||:) - 📏 Precise Rendering - SMuFL anchors for pixel-perfect positioning
✨ Features #
🎼 Complete Music Notation #
- 2932 SMuFL glyphs from the Bravura font
- Professional engraving following industry standards
- Precise positioning using SMuFL anchors and bounding boxes
- Typography-aware rendering with optical centers
🎹 Musical Elements #
- Notes & Rests: All durations from whole notes to 1024th notes
- Clefs: Treble, bass, alto, tenor, percussion, and tablature
- Key Signatures: All major and minor keys with accidentals
- Time Signatures: Simple, compound, and complex meters
- Accidentals: Natural, sharp, flat, double sharp/flat, and microtones
- Articulations: Staccato, accent, tenuto, marcato, and more
- Ornaments: Trills, turns, mordents, grace notes
- Dynamics: pp to ff, crescendo, diminuendo, sforzando
- Chords: Multi-note chords with proper stem alignment
- Beams: Advanced beaming system with professional features:
- Primary Beams: Colcheias (8th notes) with automatic slope
- Secondary Beams: Semicolcheias, fusas, semifusas (up to 128th notes)
- Broken Beams: Fractional beams for dotted rhythms (♪. ♬)
- Smart Breaking: Follows "two levels above" rule (Behind Bars)
- SMuFL Precision: 0.5 SS thickness, 0.25 SS gap, perfect geometry
- Tuplets: Triplets, quintuplets, septuplets, etc.
- Slurs & Ties: Curved connectors between notes
- Ledger Lines: Automatic for notes outside the staff
- Barlines: Single, double, final, and repeat signs using SMuFL glyphs
- Breath Marks: Comma, tick, and caesura marks
- Repeat Signs: Forward (
:||), backward (||:), and both-sided (:||:) ritornelos
🏗️ Architecture #
- Single Responsibility Principle: Specialized renderers for each element
- Modular Design: Easy to extend and customize
- Staff Position Calculator: Unified pitch-to-position conversion
- Collision Detection: Smart spacing and layout
- Theme System: Customizable colors and styles
- Measure Validation: Automatic music theory-based validation
- Prevents overfilled measures
- Real-time capacity checking
- Detailed error messages
- Tuplet-aware calculations
- Intelligent Layout Engine:
- Horizontal justification (stretches measures to fill available width)
- Automatic line breaks every 4 measures
- Staff line optimization (no empty space)
- Professional measure spacing
📊 Format Support #
- JSON: Import and export music data
- Programmatic API: Build music programmatically
✨ Recent Improvements (2025-11-05) #
🎵 Professional Barlines with SMuFL Glyphs #
All barlines now use official SMuFL glyphs from the Bravura font for perfect typographic accuracy:
- Single barline (
barlineSingleU+E030) - Double barline (
barlineDoubleU+E031) - Final barline (
barlineFinalU+E032) - fina + grossa - Repeat forward (
repeatLeftU+E040) -:║▌ - Repeat backward (
repeatRightU+E041) -▌║: - Repeat both (
repeatLeftRightU+E042) -:▌▌:
// Simple usage - barlines are automatic!
measure9.add(Barline(type: BarlineType.repeatForward));
measure16.add(Barline(type: BarlineType.final_));
📏 Horizontal Justification #
Measures now stretch proportionally to fill available width, matching professional engraving standards:
BEFORE: [M1][M2][M3][M4]___________
↑ wasted space
AFTER: [ M1 ][ M2 ][ M3 ][ M4 ]
←──── full width ────→
Algorithm distributes extra space proportionally based on element positions.
🔄 Repeat Signs (Ritornelo) #
Full support for musical repeat signs with perfect positioning:
// Start of repeated section
measure.add(Barline(type: BarlineType.repeatForward));
// End of repeated section
measure.add(Barline(type: BarlineType.repeatBackward));
💨 Breath Marks #
Respiratory marks for wind and vocal music:
// Add breath mark (comma)
measure.add(Breath(type: BreathType.comma));
// Positioned 2.5 staff spaces above the staff
Supported types:
comma- Most common (,)tick- Alternative markcaesura- Double slash (//)
✂️ Optimized Staff Lines with Configurable Margins #
Staff lines now end exactly where music ends with smart detection of barline types:
BEFORE: ═══════════════════════════════
[music] [empty space]
AFTER: ═══════════╡
[music] ║▌
↑ ends here!
🎛️ Fine-tuning available via adjustable constants in StaffRenderer:
// For NORMAL barlines (single, double, dashed, etc.)
static const double systemEndMargin = -12.0;
// For FINAL barline (BarlineType.final_)
static const double finalBarlineMargin = -1.5;
How it works:
- System detects barline type at the end of each staff system
- Applies
systemEndMarginfor normal barlines (BarlineType.single,double, etc.) - Applies
finalBarlineMarginfor final barlines (BarlineType.final_) - Independent control - adjust one without affecting the other!
Why two different values?
- Normal barlines are thinner → need more negative margin (
-12.0) - Final barlines are thicker → need less negative margin (
-1.5) - Result: Perfect visual alignment for all barline types ✅
🎯 Intelligent Line Breaking #
Automatic line breaks every 4 measures with proper barlines:
System 1: [M1][M2][M3][M4] |
System 2: [M5][M6][M7][M8] |
System 3: [M9][M10][M11][M12] |
System 4: [M13][M14][M15][M16] ║▌
🔬 Technical: Musical Coordinate System #
Important Discovery: The musical coordinate system is centered on staff line 3 (B4 in treble clef):
Line 1 ═══ Y = +2 SS (above center)
Line 2 ═══ Y = +1 SS
Line 3 ═══ Y = 0 (CENTER!)
Line 4 ═══ Y = -1 SS
Line 5 ═══ Y = -2 SS (below center)
SMuFL Glyphs:
- Have origin (0,0) at baseline (typographic convention)
- Use specific anchors from metadata.json (not geometric center)
- Follow OpenType standards with Y-axis growing upward
- Flutter's Y-axis is inverted (grows downward)
This explains why barlineYOffset = -2.0 is correct:
- Positions baseline 2 staff spaces below center (line 5)
- Glyph height of 4.0 SS makes it reach line 1
- Perfect coverage of all 5 staff lines! ✅
See BARLINE_CALIBRATION_GUIDE.md for technical details.
⚠️ Current Limitations (v0.1.0) #
Flutter Notemus v0.1.0 focuses on single-staff notation with professional engraving quality. The following features are not yet supported but have their foundations implemented:
🚫 Not Supported in v0.1.0 #
- 🎹 Piano Notation (Grand Staff) - Two interconnected staves for piano/keyboard music
- 🎶 SATB (Four-Part Vocal) - Soprano, Alto, Tenor, Bass on separate staves
- 🎸 Tablature - Guitar/bass tablature notation
- 🎼 Chord Symbols - Jazz/pop chord symbols above the staff (e.g., "C7", "Am", "G#dim")
- 🎵 Grace Notes - Appoggiatura and acciaccatura (small ornamental notes)
- 🎹 Multiple Voices - Independent voices on the same staff
📅 Roadmap - January 2026 Update #
All the above features are planned for the next major release (v0.2.0) in January 2026:
✅ Foundations Already Implemented
The architectural groundwork is complete:
- Multi-staff rendering system - Ready for grand staff and SATB
- Staff grouping with brackets -
BracketRendererfully functional - Tablature clef support -
ClefType.tab4andClefType.tab6defined - Chord symbol data model -
Textelement withTextType.chord - Grace note parsing - JSON parser ready for
isGraceNotefield - Voice separation logic - Core model supports multiple voices
🔨 What's Coming in v0.2.0
-
Piano Grand Staff
- Automatic bracket rendering between treble and bass staves
- Shared barlines and system breaks
- Cross-staff beaming
-
SATB Vocal Scores
- Four independent staves with proper grouping
- Lyrics support for each voice
- Staff labels (Soprano, Alto, Tenor, Bass)
-
Tablature
- 4-string (bass) and 6-string (guitar) tablature
- Fret number rendering
- Techniques (hammer-on, pull-off, slides)
-
Chord Symbols
- Jazz/pop chord symbols with proper typography
- Symbol positioning above staves
- Chord diagram support
-
Ornaments & Grace Notes
- Appoggiatura (accented grace note)
- Acciaccatura (quick grace note with slash)
- Proper spacing and collision avoidance
-
Multiple Voices
- Independent rhythms on same staff
- Stem direction logic (voice 1 up, voice 2 down)
- Collision detection between voices
🎯 Current Focus (v0.1.0)
V0.1.0 provides production-ready single-staff notation with:
- ✅ Professional engraving quality
- ✅ 2932 SMuFL glyphs from Bravura font
- ✅ Complete JSON import/export
- ✅ Automatic measure validation
- ✅ Advanced beaming system
- ✅ Slurs, ties, dynamics, articulations
- ✅ Repeat signs and breath marks
- ✅ Perfect for: lead sheets, melodies, single-instrument scores
Recommendation: If you need grand staff or SATB right now, consider waiting for v0.2.0 in January 2026 or contributing to the development!
🚀 Installation #
Add to your pubspec.yaml:
dependencies:
flutter_notemus: ^0.1.0
Then run:
flutter pub get
📖 Quick Start #
import 'package:flutter/material.dart';
import 'package:flutter_notemus/flutter_notemus.dart';
class SimpleMusicExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Create a staff
final staff = Staff();
final measure = Measure();
// Add clef, key signature, and time signature
measure.add(Clef(clefType: ClefType.treble));
measure.add(KeySignature(fifths: 0)); // C major
measure.add(TimeSignature(numerator: 4, denominator: 4));
// Add notes: C, D, E, F
measure.add(Note(
pitch: Pitch(step: 'C', octave: 4),
duration: NoteDuration(type: DurationType.quarter),
));
measure.add(Note(
pitch: Pitch(step: 'D', octave: 4),
duration: NoteDuration(type: DurationType.quarter),
));
measure.add(Note(
pitch: Pitch(step: 'E', octave: 4),
duration: NoteDuration(type: DurationType.quarter),
));
measure.add(Note(
pitch: Pitch(step: 'F', octave: 4),
duration: NoteDuration(type: DurationType.quarter),
));
staff.add(measure);
// Render the staff
return MusicScore(
staff: staff,
theme: MusicScoreTheme(),
staffSpace: 12.0,
);
}
}
📊 JSON Format Reference #
Complete JSON Structure #
Flutter Notemus supports a professional JSON format for importing and exporting music notation. This format is compatible with the core music model and supports all musical elements.
🎼 Basic Structure
{
"measures": [
{
"elements": [
// Musical elements here
]
}
]
}
📋 Valid Element Types #
1️⃣ Clef ("type": "clef")
{"type": "clef", "clefType": "treble"}
Valid clefType values:
"treble"- Clave de Sol (G clef)"bass"- Clave de Fá (F clef)"alto"- Clave de Dó na 3ª linha (C clef on 3rd line)"tenor"- Clave de Dó na 4ª linha (C clef on 4th line)"percussion"- Clave de percussão"tab6"- Tablatura de 6 cordas"tab4"- Tablatura de 4 cordas
2️⃣ Key Signature ("type": "keySignature")
{"type": "keySignature", "count": 2}
count values:
- Positive numbers = sustenidos (sharps):
1a71= Sol Maior / Mi menor (G major / E minor)2= Ré Maior / Si menor (D major / B minor)7= Dó# Maior / Lá# menor (C# major / A# minor)
- Negative numbers = bemóis (flats):
-1a-7-1= Fá Maior / Ré menor (F major / D minor)-2= Sib Maior / Sol menor (Bb major / G minor)-7= Dób Maior / Láb menor (Cb major / Ab minor)
- Zero =
0= Dó Maior / Lá menor (C major / A minor)
3️⃣ Time Signature ("type": "timeSignature")
{"type": "timeSignature", "numerator": 4, "denominator": 4}
Fields:
"numerator": Número de tempos (beats per measure)"denominator": Valor da unidade de tempo (note value that gets one beat)
Common examples:
4/4- Compasso quaternário simples3/4- Compasso ternário (waltz)6/8- Compasso composto2/2- Alla breve
4️⃣ Note ("type": "note")
{
"type": "note",
"pitch": {
"step": "F",
"octave": 5,
"alter": 0.0
},
"duration": {
"type": "quarter",
"dots": 1
}
}
Pitch fields:
"step": Nota diatônica (diatonic note name)- Valid:
"C","D","E","F","G","A","B"
- Valid:
"octave": Oitava (octave number)- Valid:
0a9(C4 = middle C / Dó central)
- Valid:
"alter": Alteração cromática (chromatic alteration)0.0= natural1.0= sustenido (sharp) #-1.0= bemol (flat) ♭2.0= dobrado sustenido (double sharp) 𝄪-2.0= dobrado bemol (double flat) 𝄫
Duration fields:
"type": Tipo de duração (duration type)- Valid:
"whole","half","quarter","eighth","sixteenth","thirtySecond","sixtyFourth"
- Valid:
"dots": Pontos de aumento (augmentation dots) - OPTIONAL0= sem ponto (no dot)1= um ponto (single dot) - aumenta 50%2= dois pontos (double dot) - aumenta 75%
Examples:
Semínima pontuada (Dotted quarter note):
{
"type": "note",
"pitch": {"step": "E", "octave": 5, "alter": 0.0},
"duration": {"type": "quarter", "dots": 1}
}
Colcheia (Eighth note):
{
"type": "note",
"pitch": {"step": "D", "octave": 5, "alter": 0.0},
"duration": {"type": "eighth"}
}
5️⃣ Rest ("type": "rest")
{
"type": "rest",
"duration": {
"type": "quarter",
"dots": 0
}
}
Same duration fields as notes.
6️⃣ Barline ("type": "barline")
{"type": "barline", "barlineType": "final_"}
Valid barlineType values:
"single"- Barra simples (single barline)|"double"- Barra dupla (double barline)||"final_"- Barra final dupla (final double barline)||(thick)"heavy"- Barra grossa (heavy barline)"repeatForward"- Ritornelo à frente (repeat forward):||"repeatBackward"- Ritornelo atrás (repeat backward)||:"repeatBoth"- Ritornelo ambos lados (repeat both):||:"dashed"- Barra tracejada (dashed barline)"tick"- Tick barline"short_"- Barra curta (short barline)"none"- Sem barra (invisible barline)
⚠️ IMPORTANT: Barlines são OPCIONAIS no JSON!
- Se você NÃO incluir barlines, elas são adicionadas automaticamente entre compassos
- Se você INCLUIR uma barline explícita no JSON, o sistema respeita e não duplica
- Recomendação: Adicione apenas a barra final (
"final_") no último compasso
7️⃣ Dynamic ("type": "dynamic")
{"type": "dynamic", "dynamicType": "forte"}
Valid dynamicType values:
- Básicas:
"pp","p","mp","mf","f","ff" - Completas:
"pianissimo","piano","mezzoPiano","mezzoForte","forte","fortissimo" - Especiais:
"sforzando","crescendo","diminuendo"
Com hairpin (crescendo/diminuendo):
{
"type": "dynamic",
"dynamicType": "crescendo",
"isHairpin": true,
"length": 120.0
}
8️⃣ Tempo ("type": "tempo")
{
"type": "tempo",
"text": "Allegro",
"beatUnit": "quarter",
"bpm": 120
}
Fields:
"text": Texto descritivo (ex: "Allegro", "Andante")"beatUnit": Unidade de tempo (same as duration types)"bpm": Batidas por minuto (opcional)
9️⃣ Breath Mark ("type": "breath")
{"type": "breath", "breathType": "comma"}
Valid breathType values:
"comma"- Vírgula de respiração (,)"tick"- Tick mark (')"upbow"- Arco para cima"caesura"- Cesura (//)
🔟 Caesura ("type": "caesura")
{"type": "caesura"}
Marca de pausa longa entre frases (//). Similar a breath, mas mais enfático.
1️⃣1️⃣ Chord ("type": "chord")
{
"type": "chord",
"notes": [
{"step": "C", "octave": 4, "alter": 0.0},
{"step": "E", "octave": 4, "alter": 0.0},
{"step": "G", "octave": 4, "alter": 0.0}
],
"duration": {"type": "quarter", "dots": 0},
"articulations": ["staccato", "accent"]
}
Fields:
"notes": Array de pitches (notes sem duration individual)"duration": Duration aplicada a todas as notas"articulations": Array opcional de articulações
Valid articulations:
"staccato","accent","tenuto","marcato"
1️⃣2️⃣ Text ("type": "text")
{
"type": "text",
"text": "dolce",
"textType": "expression",
"placement": "above",
"fontSize": 12.0
}
Valid textType values:
"expression"- Expressões musicais (dolce, espressivo)"instruction"- Instruções técnicas (pizz., arco)"lyrics"- Letra da música"rehearsal"- Marcas de ensaio (A, B, C)"chord"- Cifras (C, Am, G7)"tempo"- Indicações de andamento"title","subtitle","composer"- Metadados
Valid placement values:
"above"- Acima da pauta"below"- Abaixo da pauta"inside"- Dentro da pauta
📄 Complete Example: Ode à Alegria (8 compassos) #
{
"measures": [
{
"elements": [
{"type": "clef", "clefType": "treble"},
{"type": "keySignature", "count": 2},
{"type": "timeSignature", "numerator": 4, "denominator": 4},
{"type": "note", "pitch": {"step": "F", "octave": 5, "alter": 0.0}, "duration": {"type": "quarter"}},
{"type": "note", "pitch": {"step": "F", "octave": 5, "alter": 0.0}, "duration": {"type": "quarter"}},
{"type": "note", "pitch": {"step": "G", "octave": 5, "alter": 0.0}, "duration": {"type": "quarter"}},
{"type": "note", "pitch": {"step": "A", "octave": 5, "alter": 0.0}, "duration": {"type": "quarter"}}
]
},
{
"elements": [
{"type": "note", "pitch": {"step": "A", "octave": 5, "alter": 0.0}, "duration": {"type": "quarter"}},
{"type": "note", "pitch": {"step": "G", "octave": 5, "alter": 0.0}, "duration": {"type": "quarter"}},
{"type": "note", "pitch": {"step": "F", "octave": 5, "alter": 0.0}, "duration": {"type": "quarter"}},
{"type": "note", "pitch": {"step": "E", "octave": 5, "alter": 0.0}, "duration": {"type": "quarter"}}
]
},
{
"elements": [
{"type": "note", "pitch": {"step": "E", "octave": 5, "alter": 0.0}, "duration": {"type": "quarter", "dots": 1}},
{"type": "note", "pitch": {"step": "D", "octave": 5, "alter": 0.0}, "duration": {"type": "eighth"}},
{"type": "note", "pitch": {"step": "D", "octave": 5, "alter": 0.0}, "duration": {"type": "half"}},
{"type": "barline", "barlineType": "final_"}
]
}
]
}
💻 Usage in Code #
import 'package:flutter_notemus/src/parsers/json_parser.dart';
// Parse JSON string to Staff
final jsonString = '{"measures": [...]}';
final staff = JsonMusicParser.parseStaff(jsonString);
// Render
MusicScore(
staff: staff,
theme: MusicScoreTheme(
noteheadColor: Colors.black,
stemColor: Colors.black,
staffLineColor: Colors.black87,
barlineColor: Colors.black,
),
staffSpace: 14.0,
)
✅ JSON Validation Rules #
-
Measure Capacity: O total de durações das notas não pode exceder a capacidade do compasso definida pela fórmula de compasso.
- Exemplo: Em 4/4, o total deve ser ≤ 1.0 (4 semínimas)
-
Required Fields:
- Cada elemento deve ter
"type" - Notes requerem
"pitch"e"duration" - Pitch requer
"step"e"octave" - Duration requer
"type"
- Cada elemento deve ter
-
Optional Fields:
"dots"na duration (padrão: 0)"alter"no pitch (padrão: 0.0)
-
Automatic Features:
- Barlines automáticas entre compassos
- Barra final dupla no último compasso (se não especificada)
- Layout inteligente com quebras de linha
🔗 Related Documentation #
- Ver também:
PARSERS_GUIDE.mdpara exemplos avançados - Exemplo completo:
example/professional_json_example.dart
📦 Database Integration - Complete Example #
🎯 Use Case: Music Library App #
Flutter Notemus JSON format is designed to work seamlessly with databases. Here's a complete example of how to store and retrieve music notation data.
📊 Database Schema Example (SQL) #
-- Main table for musical pieces
CREATE TABLE musical_pieces (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
composer TEXT,
arranger TEXT,
genre TEXT,
difficulty TEXT, -- beginner, intermediate, advanced
duration_seconds INTEGER,
time_signature TEXT, -- e.g., "4/4", "3/4", "6/8"
key_signature TEXT, -- e.g., "C major", "D minor", "2 sharps"
tempo_bpm INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- JSON notation data
notation_json TEXT NOT NULL, -- Flutter Notemus JSON format
-- Metadata
tags TEXT, -- JSON array: ["classical", "beginner", "piano"]
notes TEXT, -- Performance notes, teaching comments
UNIQUE(title, composer)
);
-- Index for faster searches
CREATE INDEX idx_composer ON musical_pieces(composer);
CREATE INDEX idx_genre ON musical_pieces(genre);
CREATE INDEX idx_difficulty ON musical_pieces(difficulty);
📝 Complete JSON Example for Database #
Here's a complete, ready-to-store JSON example representing "Ode to Joy" (first phrase):
{
"metadata": {
"title": "Ode to Joy (Excerpt)",
"composer": "Ludwig van Beethoven",
"arranger": "Simplified arrangement",
"timeSignature": "4/4",
"keySignature": "D major (2 sharps)",
"tempo": "Allegro assai (120 BPM)"
},
"measures": [
{
"number": 1,
"elements": [
{"type": "clef", "clefType": "treble"},
{"type": "keySignature", "count": 2},
{"type": "timeSignature", "numerator": 4, "denominator": 4},
{
"type": "note",
"pitch": {"step": "F", "octave": 5, "alter": 1.0},
"duration": {"type": "quarter", "dots": 0}
},
{
"type": "note",
"pitch": {"step": "F", "octave": 5, "alter": 1.0},
"duration": {"type": "quarter", "dots": 0}
},
{
"type": "note",
"pitch": {"step": "G", "octave": 5, "alter": 0.0},
"duration": {"type": "quarter", "dots": 0}
},
{
"type": "note",
"pitch": {"step": "A", "octave": 5, "alter": 0.0},
"duration": {"type": "quarter", "dots": 0}
}
]
},
{
"number": 2,
"elements": [
{
"type": "note",
"pitch": {"step": "A", "octave": 5, "alter": 0.0},
"duration": {"type": "quarter", "dots": 0}
},
{
"type": "note",
"pitch": {"step": "G", "octave": 5, "alter": 0.0},
"duration": {"type": "quarter", "dots": 0}
},
{
"type": "note",
"pitch": {"step": "F", "octave": 5, "alter": 1.0},
"duration": {"type": "quarter", "dots": 0}
},
{
"type": "note",
"pitch": {"step": "E", "octave": 5, "alter": 0.0},
"duration": {"type": "quarter", "dots": 0}
}
]
},
{
"number": 3,
"elements": [
{
"type": "note",
"pitch": {"step": "D", "octave": 5, "alter": 0.0},
"duration": {"type": "quarter", "dots": 0}
},
{
"type": "note",
"pitch": {"step": "D", "octave": 5, "alter": 0.0},
"duration": {"type": "quarter", "dots": 0}
},
{
"type": "note",
"pitch": {"step": "E", "octave": 5, "alter": 0.0},
"duration": {"type": "quarter", "dots": 0}
},
{
"type": "note",
"pitch": {"step": "F", "octave": 5, "alter": 1.0},
"duration": {"type": "quarter", "dots": 1}
},
{
"type": "note",
"pitch": {"step": "E", "octave": 5, "alter": 0.0},
"duration": {"type": "eighth", "dots": 0}
}
]
},
{
"number": 4,
"elements": [
{
"type": "note",
"pitch": {"step": "E", "octave": 5, "alter": 0.0},
"duration": {"type": "half", "dots": 0}
},
{
"type": "rest",
"duration": {"type": "quarter", "dots": 0}
},
{
"type": "breath",
"breathType": "comma"
},
{
"type": "barline",
"barlineType": "final_"
}
]
}
]
}
📱 Flutter Implementation - Complete Flow #
1️⃣ Saving to Database
import 'dart:convert';
import 'package:sqflite/sqflite.dart';
import 'package:flutter_notemus/flutter_notemus.dart';
class MusicDatabase {
static Future<Database> get database async {
return openDatabase(
'music_library.db',
version: 1,
onCreate: (db, version) {
return db.execute(
'''CREATE TABLE musical_pieces (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
composer TEXT,
time_signature TEXT,
key_signature TEXT,
tempo_bpm INTEGER,
notation_json TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)''',
);
},
);
}
/// Save a musical piece to the database
static Future<int> savePiece({
required String title,
required String composer,
required Staff staff, // Flutter Notemus Staff object
String? timeSignature,
String? keySignature,
int? tempoBpm,
}) async {
final db = await database;
// Convert Staff to JSON
final jsonMap = {
'metadata': {
'title': title,
'composer': composer,
'timeSignature': timeSignature,
'keySignature': keySignature,
'tempo': tempoBpm != null ? '$tempoBpm BPM' : null,
},
'measures': staff.measures.map((measure) => {
'elements': measure.elements.map((element) {
// Convert each element to JSON
return _elementToJson(element);
}).toList(),
}).toList(),
};
final notationJson = jsonEncode(jsonMap);
return db.insert(
'musical_pieces',
{
'title': title,
'composer': composer,
'time_signature': timeSignature,
'key_signature': keySignature,
'tempo_bpm': tempoBpm,
'notation_json': notationJson,
},
);
}
/// Convert MusicalElement to JSON Map
static Map<String, dynamic> _elementToJson(MusicalElement element) {
if (element is Note) {
return {
'type': 'note',
'pitch': {
'step': element.pitch.step,
'octave': element.pitch.octave,
'alter': element.pitch.alter,
},
'duration': {
'type': element.duration.type.name,
'dots': element.duration.dots,
},
};
} else if (element is Rest) {
return {
'type': 'rest',
'duration': {
'type': element.duration.type.name,
'dots': element.duration.dots,
},
};
} else if (element is Clef) {
return {
'type': 'clef',
'clefType': element.actualClefType.name,
};
} else if (element is KeySignature) {
return {
'type': 'keySignature',
'count': element.fifths,
};
} else if (element is TimeSignature) {
return {
'type': 'timeSignature',
'numerator': element.numerator,
'denominator': element.denominator,
};
} else if (element is Barline) {
return {
'type': 'barline',
'barlineType': element.type.name,
};
}
// Add other element types as needed
return {'type': 'unknown'};
}
/// Retrieve a musical piece from the database
static Future<Staff?> loadPiece(int id) async {
final db = await database;
final results = await db.query(
'musical_pieces',
where: 'id = ?',
whereArgs: [id],
);
if (results.isEmpty) return null;
final notationJson = results.first['notation_json'] as String;
// Parse JSON to Staff using JsonMusicParser
return JsonMusicParser.parseStaff(notationJson);
}
/// Search pieces by composer
static Future<List<Map<String, dynamic>>> searchByComposer(
String composer,
) async {
final db = await database;
return db.query(
'musical_pieces',
where: 'composer LIKE ?',
whereArgs: ['%$composer%'],
orderBy: 'title ASC',
);
}
}
2️⃣ Rendering from Database
import 'package:flutter/material.dart';
import 'package:flutter_notemus/flutter_notemus.dart';
class MusicViewer extends StatelessWidget {
final int pieceId;
const MusicViewer({required this.pieceId});
@override
Widget build(BuildContext context) {
return FutureBuilder<Staff?>(
future: MusicDatabase.loadPiece(pieceId),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError || snapshot.data == null) {
return Center(child: Text('Failed to load music'));
}
final staff = snapshot.data!;
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(16.0),
child: MusicScore(
staff: staff,
theme: MusicScoreTheme(
noteheadColor: Colors.black,
stemColor: Colors.black,
staffLineColor: Colors.black87,
barlineColor: Colors.black,
),
staffSpace: 14.0,
),
),
);
},
);
}
}
3️⃣ Complete App Example
import 'package:flutter/material.dart';
import 'package:flutter_notemus/flutter_notemus.dart';
void main() {
runApp(MusicLibraryApp());
}
class MusicLibraryApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Music Library',
home: MusicListScreen(),
);
}
}
class MusicListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('My Music Library')),
body: FutureBuilder<List<Map<String, dynamic>>>(
future: MusicDatabase.searchByComposer(''),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
final pieces = snapshot.data!;
return ListView.builder(
itemCount: pieces.length,
itemBuilder: (context, index) {
final piece = pieces[index];
return ListTile(
title: Text(piece['title'] as String),
subtitle: Text(piece['composer'] as String),
trailing: Text(piece['time_signature'] as String? ?? ''),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MusicViewer(
pieceId: piece['id'] as int,
),
),
);
},
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Add new piece
_showAddPieceDialog(context);
},
child: Icon(Icons.add),
),
);
}
void _showAddPieceDialog(BuildContext context) {
// Implementation for adding new piece
}
}
✅ Database Best Practices #
- Store JSON as TEXT - SQLite handles JSON text efficiently
- Index metadata fields - For fast searching (composer, genre, difficulty)
- Validate before saving - Use
MeasureValidatorto ensure correctness - Version your JSON schema - Add
schema_versionfield for future migrations - Compress large pieces - Use gzip for pieces with 50+ measures
- Cache parsed Staff objects - Parse JSON once, reuse Staff object
💡 Pro Tips #
- Backup strategy: Export JSON to files for user backups
- Cloud sync: Send JSON to Firebase/Supabase for multi-device sync
- Offline-first: Store everything locally, sync when online
- Search optimization: Use FTS (Full-Text Search) for title/composer searches
- Thumbnails: Generate PNG previews of first system for list views
⚠️ Measure Validation System #
IMPORTANT: Flutter Notemus includes a strict measure validation system that enforces musical correctness based on music theory rules.
🛡️ Automatic Validation #
When you add notes to a measure with a TimeSignature, the system automatically validates that the total duration doesn't exceed the measure's capacity:
final measure = Measure(
inheritedTimeSignature: TimeSignature(numerator: 4, denominator: 4),
);
// ✅ VALID: 4 quarter notes = 1.0 units (fits in 4/4)
measure.add(Note(pitch: Pitch(step: 'C', octave: 4),
duration: Duration(DurationType.quarter))); // 0.25
measure.add(Note(pitch: Pitch(step: 'D', octave: 4),
duration: Duration(DurationType.quarter))); // 0.25
measure.add(Note(pitch: Pitch(step: 'E', octave: 4),
duration: Duration(DurationType.quarter))); // 0.25
measure.add(Note(pitch: Pitch(step: 'F', octave: 4),
duration: Duration(DurationType.quarter))); // 0.25
❌ Validation Errors #
If you try to add more notes than the measure can hold, an exception will be thrown:
final measure = Measure(
inheritedTimeSignature: TimeSignature(numerator: 4, denominator: 4),
);
measure.add(Note(pitch: Pitch(step: 'C', octave: 4),
duration: Duration(DurationType.half, dots: 1))); // 0.75
measure.add(Note(pitch: Pitch(step: 'D', octave: 4),
duration: Duration(DurationType.eighth))); // 0.125
// ❌ ERROR: This will throw MeasureCapacityException!
measure.add(Note(pitch: Pitch(step: 'E', octave: 4),
duration: Duration(DurationType.whole))); // 1.0
// Total would be: 0.75 + 0.125 + 1.0 = 1.875 units
// But 4/4 capacity is only 1.0 units!
// EXCESS: 0.875 units ← BLOCKED!
Error Message:
MeasureCapacityException: Não é possível adicionar Note ao compasso!
Compasso 4/4 (capacidade: 1 unidades)
Valor atual: 0.875 unidades
Tentando adicionar: 1 unidades
Total seria: 1.875 unidades
EXCESSO: 0.8750 unidades
❌ OPERAÇÃO BLOQUEADA - Remova figuras ou crie novo compasso!
📊 How Duration Works #
The system calculates durations based on music theory:
| Figure | Base Value | With Single Dot | With Double Dot |
|---|---|---|---|
| Whole (Semibreve) | 1.0 | 1.5 | 1.75 |
| Half (Mínima) | 0.5 | 0.75 | 0.875 |
| Quarter (Semínima) | 0.25 | 0.375 | 0.4375 |
| Eighth (Colcheia) | 0.125 | 0.1875 | 0.21875 |
| Sixteenth (Semicolcheia) | 0.0625 | 0.09375 | 0.109375 |
Formula for dotted notes:
- Single dot:
duration × 1.5 - Double dot:
duration × 1.75 - Multiple dots:
duration × (2 - 2^(-dots))
🎯 Tuplets Support #
Tuplets are automatically calculated with correct proportions:
// Triplet: 3 notes in the time of 2
Tuplet(
actualNotes: 3,
normalNotes: 2,
elements: [
Note(duration: Duration(DurationType.eighth)), // 0.125
Note(duration: Duration(DurationType.eighth)), // 0.125
Note(duration: Duration(DurationType.eighth)), // 0.125
],
) // Total: (0.125 × 3) × (2/3) = 0.25 units
🔄 TimeSignature Inheritance #
Measures without explicit TimeSignature can inherit from previous measures:
final measure1 = Measure();
measure1.add(TimeSignature(numerator: 4, denominator: 4));
// ... add notes
final measure2 = Measure(
inheritedTimeSignature: TimeSignature(numerator: 4, denominator: 4),
);
// measure2 inherits 4/4 from measure1 for validation
✅ Best Practices #
- Always set TimeSignature - Either in the measure or as inherited
- Check remaining space - Use
measure.remainingValuebefore adding notes - Use try-catch - Wrap
measure.add()in try-catch for user input:
try {
measure.add(Note(
pitch: Pitch(step: 'C', octave: 4),
duration: Duration(DurationType.quarter),
));
} on MeasureCapacityException catch (e) {
print('Cannot add note: ${e.message}');
// Show error to user or handle gracefully
}
- Validate before rendering - The
MeasureValidatorprovides detailed reports:
final validation = MeasureValidator.validateWithTimeSignature(
measure,
timeSignature,
);
if (!validation.isValid) {
print('Invalid measure: ${validation.errors}');
print('Expected: ${validation.expectedCapacity}');
print('Actual: ${validation.actualDuration}');
}
🎵 Musical Correctness #
This validation system ensures your notation follows professional music engraving standards:
- ✅ No overfilled measures - Prevents rhythmic errors
- ✅ Clear error messages - Shows exactly what's wrong
- ✅ Theory-based - Follows music theory rules
- ✅ Preventive - Catches errors BEFORE rendering
- ✅ Tuplet-aware - Correctly handles complex rhythms
Remember: The validation is your friend! It prevents creating invalid musical notation that would confuse performers.
🎼 Advanced Examples #
Chords #
Chord(
notes: [
Note(
pitch: Pitch(step: 'C', octave: 4),
duration: NoteDuration(type: DurationType.quarter),
),
Note(
pitch: Pitch(step: 'E', octave: 4),
duration: NoteDuration(type: DurationType.quarter),
),
Note(
pitch: Pitch(step: 'G', octave: 4),
duration: NoteDuration(type: DurationType.quarter),
),
],
duration: NoteDuration(type: DurationType.quarter),
)
Augmentation Dots #
Note(
pitch: Pitch(step: 'C', octave: 4),
duration: NoteDuration(
type: DurationType.quarter,
dots: 2, // Double-dotted quarter note
),
)
Accidentals #
Note(
pitch: Pitch(
step: 'F',
octave: 4,
accidental: AccidentalType.sharp,
),
duration: NoteDuration(type: DurationType.quarter),
)
Articulations #
Note(
pitch: Pitch(step: 'C', octave: 4),
duration: NoteDuration(type: DurationType.quarter),
articulations: [
Articulation(type: ArticulationType.staccato),
Articulation(type: ArticulationType.accent),
],
)
Dynamics #
measure.add(Dynamic(
type: DynamicType.forte,
customText: 'f',
));
// Crescendo (hairpin)
measure.add(Dynamic(
type: DynamicType.crescendo,
isHairpin: true,
length: 120.0,
));
⚙️ Advanced Customization #
Staff Line Margins #
If you need to fine-tune where staff lines end in relation to barlines, you can adjust the constants in lib/src/rendering/staff_renderer.dart:
class StaffRenderer {
// 🎚️ MANUAL ADJUSTMENT CONSTANTS
// Margin after NORMAL barlines (single, double, dashed, etc.)
// Negative values move lines closer to the barline
// -12.0 = Lines end exactly at normal barlines ✅
static const double systemEndMargin = -12.0;
// Margin after FINAL barline (BarlineType.final_)
// -1.5 = Lines end exactly at final barline ✅
static const double finalBarlineMargin = -1.5;
}
When to adjust:
- Different font sizes may require different values
- Custom barline implementations
- Specific visual preferences
How to test:
- Modify the constant values
- Run
flutter runwith hot reload - Visually inspect barline alignment
- Adjust incrementally (0.5 pixel steps recommended)
🎨 Themes #
Flutter Notemus supports customizable themes:
MusicScore(
staff: staff,
theme: MusicScoreTheme(
noteheadColor: Colors.blue,
stemColor: Colors.blue,
staffLineColor: Colors.black,
accidentalColor: Colors.red,
ornamentColor: Colors.green,
showLedgerLines: true,
),
)
📚 Documentation #
- API Reference - Complete API documentation
- Architecture - System design and principles
- Examples - Complete working examples
- SMuFL Spec - SMuFL standard reference
🏗️ Architecture Highlights #
Flutter Notemus follows Single Responsibility Principle with specialized renderers:
NoteRenderer- Note headsStemRenderer- Note stemsFlagRenderer- Note flagsDotRenderer- Augmentation dotsLedgerLineRenderer- Ledger linesAccidentalRenderer- Accidentals (sharps, flats, etc.)ChordRenderer- Multi-note chordsDynamicRenderer- Dynamic markingsRepeatMarkRenderer- Repeat signs (coda, segno)TextRenderer- Musical text
Each renderer has a single, well-defined responsibility, making the codebase maintainable and testable.
🧪 Testing #
Run tests:
flutter test
Run example app:
cd example
flutter run
📦 What's Included #
- ✅ Complete SMuFL glyph support (Bravura font)
- ✅ Professional music engraving engine
- ✅ Specialized renderers following SRP
- ✅ Staff position calculator
- ✅ Collision detection system
- ✅ Automatic measure validation system
- ✅ Horizontal justification (proportional spacing)
- ✅ Barlines with SMuFL glyphs (all types)
- ✅ Repeat signs (ritornelo forward/backward/both)
- ✅ Breath marks (comma, tick, caesura)
- ✅ Optimized staff lines (no empty space)
- ✅ Configurable staff line margins (type-aware: normal vs final barlines)
- ✅ Intelligent line breaking (4 measures per system)
- ✅ Theme system
- ✅ JSON parser
- ✅ Comprehensive examples
- ✅ Full documentation
⚙️ Technical Notes: Flutter TextPainter & SMuFL #
🔍 Understanding Baseline Corrections #
Important for contributors and advanced users!
Flutter Notemus implements several baseline corrections to compensate for fundamental differences between Flutter's text rendering system and the SMuFL specification. Understanding these differences is crucial for maintaining and extending the library.
📐 The Core Issue #
SMuFL Coordinate System
SMuFL uses precise glyph-based coordinates:
- Baseline: Center line of the glyph
- Bounding boxes: Exact per-glyph dimensions
- Example (noteheadBlack):
bBoxSwY: -0.5 staff spaces
bBoxNeY: +0.5 staff spaces
Height: 1.0 staff space
Flutter TextPainter System
Flutter uses font-wide metrics (OpenType hhea table):
- ascent: ~2.5 staff spaces
- descent: ~2.5 staff spaces
- Total height: ~5.0 staff spaces (5× the actual glyph!)
Why? The font metrics must accommodate the largest possible glyph (clefs, ornaments, etc.), not individual noteheads.
🎯 Baseline Correction Formula #
baselineCorrection = -textPainter.height * 0.5
= -(5.0 staff spaces) * 0.5
= -2.5 staff spaces
This correction:
- ✅ Moves glyphs from Flutter's "top of box" coordinate to SMuFL's "baseline" coordinate
- ✅ Ensures noteheads align precisely with staff lines
- ✅ Maintains compatibility with SMuFL anchors (stemUpSE, stemDownNW)
📊 Impact on Components #
Noteheads
// base_glyph_renderer.dart
static const GlyphDrawOptions noteheadDefault = GlyphDrawOptions(
centerVertically: false,
disableBaselineCorrection: false, // ← Correction ENABLED
);
Result: Noteheads render at correct staff positions ✅
Augmentation Dots
// dot_renderer.dart
double _calculateDotY(double noteY, int staffPosition) {
// noteY already has -2.5 SS baseline correction applied
// Compensate to position dots correctly:
if (staffPosition.isEven) {
return noteY - (coordinates.staffSpace * 2.5); // Compensate
} else {
return noteY - (coordinates.staffSpace * 2.0); // Compensate
}
}
Result: Dots align perfectly in staff spaces ✅
🔬 Mathematical Proof #
For a note on staff line 2 (G4 in treble clef):
Without correction:
staffPosition = -2
noteY = 72.0px (baseline)
TextPainter renders at: 72.0px ← TOO LOW!
With correction:
staffPosition = -2
noteY = 72.0px
baselineCorrection = -30.0px (-2.5 SS)
Final Y = 72.0 - 30.0 = 42.0px ← CORRECT!
Dot position:
dotY = noteY - (2.5 × staffSpace)
= 72.0 - 30.0
= 42.0px
Then add 0.5 SS to move to space above line
Final dotY = 42.0 - 6.0 = 36.0px ← PERFECT!
🏗️ Design Decisions #
Why Not Modify the Font?
- ❌ Would break compatibility with standard Bravura distribution
- ❌ Would lose updates and improvements from SMuFL team
- ❌ Wouldn't solve the fundamental Flutter/SMuFL difference
Why Not Use Canvas.drawParagraph Directly?
- ❌ More complex API
- ❌ Loses Flutter's text rendering optimizations
- ❌ More difficult to maintain
Why TextPainter + Corrections? ✅
- ✅ Uses Flutter's native, optimized text rendering
- ✅ Works with any SMuFL-compliant font
- ✅ Mathematical corrections are predictable and documentable
- ✅ Well-tested and proven approach
📚 References #
- SMuFL Specification: https://w3c.github.io/smufl/latest/
- OpenType hhea Table: https://docs.microsoft.com/en-us/typography/opentype/spec/hhea
- "Behind Bars" by Elaine Gould - Music engraving best practices
- Flutter TextPainter: https://api.flutter.dev/flutter/painting/TextPainter-class.html
💡 For Contributors #
When adding new renderers or modifying existing ones:
- Understand the coordinate system - The musical staff is centered on line 3 (Y=0)
- SMuFL baseline vs geometric center - Glyphs use baseline (0,0 at bottom-left), not center!
- Check metadata.json - Use SMuFL anchors for precise positioning
- Account for Y-axis inversion - Flutter (↓) vs OpenType (↑)
- Test with multiple staff positions - Verify alignment on lines AND spaces
- Document empirical values - Explain mathematically, not just "it works"
- Refer to technical guides:
SOLUCAO_FINAL_PONTOS.md- Dot positioning case studyBARLINE_CALIBRATION_GUIDE.md- Barline positioningVISUAL_ADJUSTMENTS_FINAL.md- Stem/flag alignment
Key principles:
- Musical coordinate system: line 3 = Y:0 (center)
- SMuFL glyphs: baseline = (0,0) (typographic)
- All "magic numbers" are mathematical compensations - document them!
- Always verify against professional notation software (Finale, Sibelius, Dorico)
- Staff line margins are type-aware:
systemEndMarginfor normal barlines (thinner)finalBarlineMarginfor final barlines (thicker)- System detects barline type automatically (
BarlineType.final_vs others)
🤝 Contributing #
Contributions are welcome! Please read our Contributing Guide for details.
When contributing, please:
- Read the Technical Notes section above
- Maintain mathematical precision in positioning
- Document any empirical values with explanations
- Test visual output against professional notation software
📄 License #
This project is licensed under the MIT License - see the LICENSE file for details.
🙏 Credits #
- Bravura Font by Steinberg Media Technologies
- SMuFL Standard by W3C Music Notation Community Group
- Engraving principles from:
- "Behind Bars" by Elaine Gould
- "The Art of Music Engraving" by Ted Ross
- Technical insights:
- OpenType specification
- SMuFL metadata.json anchors
- ChatGPT for baseline/coordinate system clarification
🌟 Why Flutter Notemus? #
| Feature | Flutter Notemus | Others |
|---|---|---|
| SMuFL Compliant | ✅ Full support | ⚠️ Partial |
| Professional Engraving | ✅ Typography-aware | ❌ Basic |
| Modular Architecture | ✅ SRP-based | ❌ Monolithic |
| Collision Detection | ✅ Smart spacing | ❌ Manual |
| Customizable Themes | ✅ Full control | ⚠️ Limited |
| Active Development | ✅ Yes | ⚠️ Varies |
Flutter Notemus - Professional music notation for Flutter 🎵
Developed with dedication by Alesson Lucas Oliveira de Queiroz