Piano Roll Conversion
Piano rolls are a popular representation of symbolic music data, especially in music generation and analysis tasks. Symusic provides efficient tools to convert between MIDI and piano roll representations.
What is a Piano Roll?
A piano roll is a matrix representation of music where:
- The rows represent different pitch values
- The columns represent time steps
- The values in the matrix represent note presence, velocity, or other properties
Piano rolls are often used in machine learning models for music generation and analysis because they're easy to process using neural networks and other algorithms.
Basic Piano Roll Conversion
Symusic can convert a Score or Track object to a piano roll representation:
from symusic import Score
import matplotlib.pyplot as plt
# Load a MIDI file
score = Score("path/to/file.mid")
# Resample to reduce size (recommended before piano roll conversion)
# This makes the time grid more manageable
# tpq=6 means 6 ticks per quarter note (1/24 note resolution)
# min_dur=1 ensures notes are at least 1 time unit long
score_resampled = score.resample(tpq=6, min_dur=1)
# Convert the entire score to a piano roll
# This creates a 4D array: [modes, tracks, pitch, time]
pianoroll = score_resampled.pianoroll(
modes=["onset", "frame"], # Type of information to include
pitch_range=[0, 128], # Range of MIDI pitches (default: all 128)
encode_velocity=True # If True, use velocity values; if False, use binary (0/1)
)
# Visualize the piano roll (combined onset and frame for the first track)
plt.figure(figsize=(12, 6))
plt.imshow(pianoroll[0, 0] + pianoroll[1, 0], origin='lower', aspect='auto',
extent=[0, pianoroll.shape[3], 0, 128])
plt.title('Piano Roll (Track 0)')
plt.xlabel('Time (in ticks)')
plt.ylabel('Pitch (MIDI note number)')
plt.colorbar(label='Velocity')
plt.show()
Piano Roll Modes
Symusic supports different piano roll modes that capture different aspects of musical notes:
- onset: Only the beginning (attack) of each note is marked
- frame: The entire duration of each note is marked
- offset: Only the end (release) of each note is marked
You can specify one or more modes when creating a piano roll:
# Create a piano roll with all three modes
pianoroll = score_resampled.pianoroll(
modes=["onset", "frame", "offset"],
pitch_range=[0, 128],
encode_velocity=True
)
# Access each mode separately
onset_roll = pianoroll[0] # Shape: [tracks, pitch, time]
frame_roll = pianoroll[1] # Shape: [tracks, pitch, time]
offset_roll = pianoroll[2] # Shape: [tracks, pitch, time]
Single Track Piano Roll
You can also create a piano roll for a single track:
from symusic import Score
import matplotlib.pyplot as plt
# Load a MIDI file
score = Score("path/to/file.mid")
# Resample for better processing
score_resampled = score.resample(tpq=6, min_dur=1)
# Select a track
track = score_resampled.tracks[0] # First track
# Convert track to piano roll (creates a 3D array: [modes, pitch, time])
track_pianoroll = track.pianoroll(
modes=["onset", "frame", "offset"],
pitch_range=[0, 128],
encode_velocity=True
)
# The shape is different from score.pianoroll() - no track dimension
print(f"Track piano roll shape: {track_pianoroll.shape}") # [3, 128, time_steps]
# Visualize the track's piano roll
plt.figure(figsize=(12, 6))
plt.imshow(track_pianoroll[1], origin='lower', aspect='auto', # Show only the frame mode
extent=[0, track_pianoroll.shape[2], 0, 128])
plt.title(f'Piano Roll - {track.name}')
plt.xlabel('Time (in ticks)')
plt.ylabel('Pitch (MIDI note number)')
plt.colorbar(label='Velocity')
plt.show()
Customizing Piano Roll Range
You can focus on a specific pitch range by specifying the pitch_range
parameter:
# Create a piano roll for just the piano range (21-108)
piano_range_roll = track.pianoroll(
modes=["onset", "frame"],
pitch_range=[21, 109], # MIDI notes 21-108 (standard piano)
encode_velocity=True
)
# Create a piano roll for just the bass range
bass_range_roll = track.pianoroll(
modes=["frame"],
pitch_range=[24, 60], # Low range
encode_velocity=False # Binary (simpler visualization)
)
Binary vs. Velocity Encoding
The encode_velocity
parameter controls whether the piano roll contains velocity information:
# Velocity-encoded piano roll (values are actual velocities: 0-127)
velocity_roll = track.pianoroll(
modes=["frame"],
encode_velocity=True
)
# Binary piano roll (values are just 0 or 1)
binary_roll = track.pianoroll(
modes=["frame"],
encode_velocity=False
)
Binary encoding is often simpler for visualization and certain types of processing, while velocity encoding preserves more musical information.
Working with Piano Roll Data
Once you've created a piano roll, you can process it using NumPy operations:
import numpy as np
from symusic import Score
# Load and convert to piano roll
score = Score("path/to/file.mid").resample(tpq=4, min_dur=1)
pianoroll = score.pianoroll(modes=["frame"], encode_velocity=True)
# If working with a score piano roll, access a specific track
track_roll = pianoroll[0, 0] # Mode 0 (frame), track 0
# Basic statistics
active_notes = np.count_nonzero(track_roll)
total_cells = track_roll.size
activity = active_notes / total_cells
print(f"Note activity: {activity:.2%}")
# Find the highest and lowest pitches used
active_pitches = np.any(track_roll > 0, axis=1)
lowest_pitch = np.argmax(active_pitches)
highest_pitch = len(active_pitches) - np.argmax(active_pitches[::-1]) - 1
print(f"Pitch range: {lowest_pitch} to {highest_pitch}")
# Find the average velocity of active notes
avg_velocity = np.mean(track_roll[track_roll > 0])
print(f"Average velocity: {avg_velocity:.1f}")
# Create a "piano roll profile" (how often each pitch is used)
pitch_profile = np.sum(track_roll > 0, axis=1)
Advanced Visualization
You can create more advanced visualizations of piano rolls:
import matplotlib.pyplot as plt
import matplotlib.colors as colors
import numpy as np
from symusic import Score
# Load and prepare data
score = Score("path/to/file.mid").resample(tpq=6, min_dur=1)
track = score.tracks[0]
pianoroll = track.pianoroll(modes=["onset", "frame"], pitch_range=[21, 109], encode_velocity=True)
# Create a custom colormap for better visualization
cmap = plt.cm.viridis
norm = colors.Normalize(vmin=0, vmax=127)
# Create a figure with multiple subplots
fig, axs = plt.subplots(3, 1, figsize=(12, 10), sharex=True)
# Plot onset roll
onset = pianoroll[0]
im0 = axs[0].imshow(onset, origin='lower', aspect='auto', cmap=cmap, norm=norm)
axs[0].set_title('Note Onsets')
axs[0].set_ylabel('Pitch')
fig.colorbar(im0, ax=axs[0], label='Velocity')
# Plot frame roll
frame = pianoroll[1]
im1 = axs[1].imshow(frame, origin='lower', aspect='auto', cmap=cmap, norm=norm)
axs[1].set_title('Note Frames')
axs[1].set_ylabel('Pitch')
fig.colorbar(im1, ax=axs[1], label='Velocity')
# Plot combined view (onset + frame)
combined = np.maximum(onset * 0.7, frame) # Emphasize onsets
im2 = axs[2].imshow(combined, origin='lower', aspect='auto', cmap=cmap, norm=norm)
axs[2].set_title('Combined View')
axs[2].set_ylabel('Pitch')
axs[2].set_xlabel('Time (ticks)')
fig.colorbar(im2, ax=axs[2], label='Velocity')
# Adjust layout and display
plt.tight_layout()
plt.show()
Tips for Piano Roll Processing
Resampling for Efficient Processing
Always resample your score before converting to piano roll to avoid generating excessively large matrices:
# Bad: Direct conversion without resampling could create a huge matrix
# pianoroll = score.pianoroll(modes=["frame"])
# Good: Resample first to reduce the time resolution
score_resampled = score.resample(tpq=4, min_dur=1) # 16th note resolution
pianoroll = score_resampled.pianoroll(modes=["frame"])
Choosing the Right Resolution
The tpq
(ticks per quarter note) parameter in resample()
controls the time resolution:
- Lower values (e.g., 2-4) create smaller matrices but lose some timing precision
- Higher values (e.g., 8-12) preserve more detail but create larger matrices
- For most applications, values between 4-8 work well (1/16 to 1/32 note resolution)
Memory Management
Piano rolls can be memory-intensive for long pieces or high resolutions. Consider:
- Processing shorter segments if memory is an issue
- Using lower
tpq
values for initial exploration - Limiting the pitch range to the most relevant notes
- Using binary encoding (
encode_velocity=False
) to save memory
# Memory-efficient piano roll for a long piece
efficient_roll = score.resample(tpq=4, min_dur=1).pianoroll(
modes=["frame"],
pitch_range=[36, 96], # Focus on middle pitch range
encode_velocity=False # Binary encoding
)
Converting Back to Notes
Currently, Symusic doesn't provide built-in functionality to convert piano rolls back to Score or Track objects. This is because the conversion is complex and can be ambiguous, especially when dealing with overlapping notes.
If you need to convert piano roll data back to MIDI, consider using external libraries like pretty_midi
or implementing custom algorithms based on your specific use case.