symusic (Symbolic Music)

repository·main·Indexed 41 days ago

https://github.com/yikai-liao/symusic

A high-performance, cross-platform toolkit for note-level MIDI and ABC symbolic music processing. Featuring a C++20 core exposed to Python via nanobind, it provides fast parsing, transformation workloads, and audio synthesis via SoundFonts. Key features include vectorized operations on Score and Track objects, time conversions between ticks, quarters, and seconds, and a comprehensive factory system for creating notes, tempo, and pitch bend data from NumPy arrays.

Tokens
26.5K
Snippets
69
Records
143
Agent score
51%

What's inside symusic

  1. Core features of symusic

    main

    Symusic is a cross-platform symbolic music toolkit designed for high-performance note-level MIDI and ABC workflows. Key capabilities include:

    • File I/O: Load MIDI and ABC files using Score("path.mid") or Score("path.abc", fmt="abc"). Export using dump_midi, dump_abc, or dumps_abc.
    • Time Conversions: Convert scores between tick, quarter, and second time units.
    • Analysis: Compute beats, downbeats, and piano rolls.
    • Transformations: Apply vectorized operations on Score and Track objects, including shift_pitch, shift_velocity, shift_time, sort, and filter.
    • Serialization: Efficiently serialize containers using pickle or structured NumPy exports.
    • Audio Synthesis: Render audio via SoundFonts using Synthesizer, BuiltInSF2, BuiltInSF3, and dump_wav.
  2. Use the symusic Python API

    main

    The symusic Python package provides high-level factories that mirror the core functionality of the library. Most user-facing surfaces are thin wrappers around nanobind classes defined in symusic.core.

    To work with music data, you should primarily use the high-level factories available in the symusic namespace rather than interacting with the low-level core directly, unless you require specific tick-based specializations.

  3. What is a Score in Symusic

    main

    A Score is the root container in Symusic that represents a complete musical piece. It acts as a global context (similar to a DAW session) that aggregates all tracks and global musical events.

    Key responsibilities of a Score include:

    • Aggregation: Holds all Track objects and global events like tempos, time signatures, and key signatures.
    • Global Metadata: Stores ticks_per_quarter (TPQ), which defines the mapping between tick-based time and musical durations.
    • Global Transformations: Provides methods to perform operations across the entire piece, such as clip, trim, sort, resample, and time/pitch/velocity shifts.
    • Exporting: Serves as the entry point for exporting data to MIDI, ABC, piano rolls, or synthesizing audio.
  4. How synthesis works in symusic

    main

    Symusic enables audio synthesis by converting a Score into sound using SoundFont 2/3 data and the Prestosynth engine. The process involves several key components:

    • SoundFont: An .sf2 or .sf3 file defining instrument samples. You can use BuiltInSF2/3 helpers to download curated fonts or provide a local path.
    • Synthesizer: A Python/nanobind wrapper around Prestosynth. It is initialized with an sf_path, sample_rate, and an optional quality flag.
    • Score: The input symbolic data. For correct instrument mapping, tracks must include program and is_drum metadata.
    • render(): The core method that converts the score to seconds, schedules events, and returns a NumPy array with shape [channels, samples].
    • dump_wav(): A utility to save the resulting NumPy array to a WAV file on disk.

    Internally, rendering is handled in second units, so time conversions are managed automatically. Percussion tracks (where is_drum=True) are mapped to the GM drum kit region within the SoundFont.

    from symusic import Score, Synthesizer, BuiltInSF3, dump_wav
    
    score = Score("piece.mid")
    sf_path = BuiltInSF3.MuseScoreGeneral().path(download=True)
    synth = Synthesizer(sf_path=sf_path, sample_rate=48000, quality=1)
    audio = synth.render(score, stereo=True)
    dump_wav("piece.wav", audio, sample_rate=48000)
  5. Understand the Track abstraction in symusic

    main

    A Track represents a single musical part within a Score. It acts as a container that groups instrument-specific events and metadata, allowing you to perform part-specific operations like transposing only string instruments or quantizing drum tracks without affecting the rest of the score.

    A Track contains:

    • Notes: A NoteList of note-on/off events (pitch, duration, velocity).
    • Control changes: MIDI CC data (volume, pan, modulation, etc.).
    • Pitch bends: High-resolution pitch deviations.
    • Pedals: Representations of sustain pedal regions.
    • Lyrics/Text: A TextMetaList for annotations or lyrics.

    All event lists within a track support list-like methods and helpers such as .start(), .end(), .empty(), .sort(), .filter(), .adjust_time(), and .numpy().

  6. Use stubs and type hints in symusic

    main

    Symusic uses nanobind to generate stubs (symusic/core.pyi) and the py.typed marker automatically during the build process (pip install . or pip install -e .).

    The typing surface mirrors the specializations (tick, quarter, second) exposed by the compiled extension. When writing explicit type annotations, use the generic helpers provided in symusic.types and symusic.core.

    import symusic.core as core
    import symusic.types as smt
    
    score: smt.Score[core.Tick] = core.ScoreTick(480)
    notes: core.PyVec[core.Note[core.Tick]] = core.NoteTickList()
  7. Understand Score anatomy and data structures

    main

    A Score is composed of several specialized lists, all backed by high-performance C++ pyvec objects to ensure fast slicing, copying, and filtering. The structure follows this hierarchy:

    • ticks_per_quarter: An integer defining the temporal resolution.
    • tracks: A TrackList containing Track<T> objects (per-instrument containers).
    • tempos: A TempoList for global tempo changes.
    • time_signatures: A TimeSignatureList for global meter changes.
    • key_signatures: A KeySignatureList for global key changes.
    • markers: A TextMetaList for free-form text metadata events.
  8. Understand Event Primitives and timing modes in symusic

    main

    In symusic.core, events are categorized by the type of musical information they carry and the temporal unit used to position them.

    Event Types

    • Notes: Musical notes.
    • Control Changes: MIDI-style control change messages.
    • Tempo: Changes to the musical tempo.
    • Pedals: Sustain or pedal state changes.
    • Pitch Bend: Pitch modulation events.
    • Key Signatures: Changes to the musical key.
    • Time Signatures: Changes to the meter/time signature.
    • Text Meta: Metadata or text-based annotations.

    Timing Modes

    Every event type is available in three distinct timing modes, allowing you to specify exactly when an event occurs based on the desired precision:

    1. Tick: Positioned using MIDI-style ticks (resolution depends on the file's PPQ).
    2. Quarter: Positioned using quarter-note units.
    3. Second: Positioned using absolute time in seconds.
  9. Inspect the score hierarchy

    main

    A Score object contains global events and a list of Track objects.

    Global Events: tempos, time_signatures, key_signatures, and markers.

    Track Structure: Each Track contains several list-like containers:

    • NoteList
    • ControlChangeList
    • PitchBendList
    • PedalList
    • TextMetaList

    These containers support list-like APIs and helpers such as .start(), .end(), .empty(), .copy(), .sort(), and .filter().

    from symusic import Score
    
    score = Score("example.mid")
    print(score.ticks_per_quarter)
    print(len(score.tracks))
    
    first_track = score.tracks[0]
    print(first_track.name, first_track.program, "drum" if first_track.is_drum else "melodic")
    print(len(first_track.notes), len(first_track.controls))
  10. Understand the concept of Events in Symusic

    main

    Events are the fundamental units of time-stamped information in Symusic. They represent occurrences such as note starts, tempo changes, or lyric appearances.

    All events share these core characteristics:

    • time: The timestamp of the event relative to the start of the score, expressed in the score's current time unit.
    • Time-aware types: Events are specialized by time unit (e.g., NoteTick, TempoQuarter, NoteSecond). In Python, factories automatically dispatch to the correct specialization based on the ttype.
    • Typed Containers: Events are stored in specialized lists (e.g., NoteList, TempoList, ControlChangeList) that support slicing, iteration, sorting, filtering, copying, and SoA (Structure of Arrays) exports.
  11. Understand Symusic's Generic Core Bindings

    main

    Symusic's Python bindings mirror a templated C++ API where musically meaningful objects (scores, tracks, and event vectors) are parameterized by a time unit. This allows the same algorithms to operate on different temporal scales such as ticks, musical quarters, or absolute seconds.

    Typing Helpers vs. Concrete Classes

    • Typing Placeholders: Names like Score, Track, Note, or PyVec in symusic.core act as typing.Generic definitions. They allow type checkers to recognize that while ScoreTick and ScoreSecond share the same API, they are not interchangeable.
    • Concrete Classes: At runtime, you must instantiate concrete classes like ScoreTick, ScoreQuarter, ScoreSecond, NoteTick, or NoteQuarter.
    • Factories: The symusic.factory module provides ergonomic constructors that route calls to the correct concrete core class based on the requested time unit.
  12. Manage time units: tick, quarter, and second

    main

    Symusic operates in three distinct time domains: tick, quarter, and second. Mixing these accidentally can cause incorrect durations or densities.

    Best Practices:

    • Choose one time unit for your workflow and stick to it.
    • Use Score(..., ttype="second") when calculating rates or densities based on wall-clock time.
    • Use explicit conversion methods before performing heavy analysis.

    Conversion Methods:

    • score.to("second")
    • score.to("quarter")