flutter_notemus 0.1.0 copy "flutter_notemus: ^0.1.0" to clipboard
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 #

Flutter Dart SMuFL License: MIT

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 #

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 (barlineSingle U+E030)
  • Double barline (barlineDouble U+E031)
  • Final barline (barlineFinal U+E032) - fina + grossa
  • Repeat forward (repeatLeft U+E040) - :║▌
  • Repeat backward (repeatRight U+E041) - ▌║:
  • Repeat both (repeatLeftRight U+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 mark
  • caesura - 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 systemEndMargin for normal barlines (BarlineType.single, double, etc.)
  • Applies finalBarlineMargin for 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 - BracketRenderer fully functional
  • Tablature clef support - ClefType.tab4 and ClefType.tab6 defined
  • Chord symbol data model - Text element with TextType.chord
  • Grace note parsing - JSON parser ready for isGraceNote field
  • Voice separation logic - Core model supports multiple voices

🔨 What's Coming in v0.2.0

  1. Piano Grand Staff

    • Automatic bracket rendering between treble and bass staves
    • Shared barlines and system breaks
    • Cross-staff beaming
  2. SATB Vocal Scores

    • Four independent staves with proper grouping
    • Lyrics support for each voice
    • Staff labels (Soprano, Alto, Tenor, Bass)
  3. Tablature

    • 4-string (bass) and 6-string (guitar) tablature
    • Fret number rendering
    • Techniques (hammer-on, pull-off, slides)
  4. Chord Symbols

    • Jazz/pop chord symbols with proper typography
    • Symbol positioning above staves
    • Chord diagram support
  5. Ornaments & Grace Notes

    • Appoggiatura (accented grace note)
    • Acciaccatura (quick grace note with slash)
    • Proper spacing and collision avoidance
  6. 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): 1 a 7
    • 1 = 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): -1 a -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 simples
  • 3/4 - Compasso ternário (waltz)
  • 6/8 - Compasso composto
  • 2/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"
  • "octave": Oitava (octave number)
    • Valid: 0 a 9 (C4 = middle C / Dó central)
  • "alter": Alteração cromática (chromatic alteration)
    • 0.0 = natural
    • 1.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"
  • "dots": Pontos de aumento (augmentation dots) - OPTIONAL
    • 0 = 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 #

  1. 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)
  2. Required Fields:

    • Cada elemento deve ter "type"
    • Notes requerem "pitch" e "duration"
    • Pitch requer "step" e "octave"
    • Duration requer "type"
  3. Optional Fields:

    • "dots" na duration (padrão: 0)
    • "alter" no pitch (padrão: 0.0)
  4. Automatic Features:

    • Barlines automáticas entre compassos
    • Barra final dupla no último compasso (se não especificada)
    • Layout inteligente com quebras de linha

  • Ver também: PARSERS_GUIDE.md para 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 #

  1. Store JSON as TEXT - SQLite handles JSON text efficiently
  2. Index metadata fields - For fast searching (composer, genre, difficulty)
  3. Validate before saving - Use MeasureValidator to ensure correctness
  4. Version your JSON schema - Add schema_version field for future migrations
  5. Compress large pieces - Use gzip for pieces with 50+ measures
  6. 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 #

  1. Always set TimeSignature - Either in the measure or as inherited
  2. Check remaining space - Use measure.remainingValue before adding notes
  3. 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
}
  1. Validate before rendering - The MeasureValidator provides 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:

  1. Modify the constant values
  2. Run flutter run with hot reload
  3. Visually inspect barline alignment
  4. 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 heads
  • StemRenderer - Note stems
  • FlagRenderer - Note flags
  • DotRenderer - Augmentation dots
  • LedgerLineRenderer - Ledger lines
  • AccidentalRenderer - Accidentals (sharps, flats, etc.)
  • ChordRenderer - Multi-note chords
  • DynamicRenderer - Dynamic markings
  • RepeatMarkRenderer - 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:

  1. ✅ Moves glyphs from Flutter's "top of box" coordinate to SMuFL's "baseline" coordinate
  2. ✅ Ensures noteheads align precisely with staff lines
  3. ✅ 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 #


💡 For Contributors #

When adding new renderers or modifying existing ones:

  1. Understand the coordinate system - The musical staff is centered on line 3 (Y=0)
  2. SMuFL baseline vs geometric center - Glyphs use baseline (0,0 at bottom-left), not center!
  3. Check metadata.json - Use SMuFL anchors for precise positioning
  4. Account for Y-axis inversion - Flutter (↓) vs OpenType (↑)
  5. Test with multiple staff positions - Verify alignment on lines AND spaces
  6. Document empirical values - Explain mathematically, not just "it works"
  7. Refer to technical guides:
    • SOLUCAO_FINAL_PONTOS.md - Dot positioning case study
    • BARLINE_CALIBRATION_GUIDE.md - Barline positioning
    • VISUAL_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:
    • systemEndMargin for normal barlines (thinner)
    • finalBarlineMargin for 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

2
likes
130
points
35
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Professional music notation rendering for Flutter with complete SMuFL support. Features 2932 glyphs, JSON import/export, automatic validation, and advanced beaming system.

License

unknown (license)

Dependencies

collection, flutter, pdf, printing, xml

More

Packages that depend on flutter_notemus