Source code for djalgo.loop

import numpy as np
from . import utils

import plotly.graph_objects as go
import colorsys

[docs] class Polyloop: """ Represents a collection of polyloops, which are sequences of musical notes. """ def __init__(self, polyloops, measure_length=4, insert_rests=True): """ Initializes a Polyloop object. Parameters: - polyloops (dict or list): A dictionary of polyloops where keys are names and values are sequences, or a list of polyloops. Each polyloop is expected to be in the form [(pitch, duration, offset), ...]. If a list is provided, it will be converted to a dict with keys 'polyloop 0', 'polyloop 1', etc. - measure_length (int): The length of a measure in beats. Defaults to 4. - insert_rests (bool): Whether to insert rests in the polyloops. Defaults to True. """ self.measure_length = measure_length # Convert list to dict if needed if not isinstance(polyloops, dict): # If it's a single polyloop (list of tuples), wrap it in a list if polyloops and isinstance(polyloops[0], tuple): polyloops = [polyloops] # Convert list of polyloops to dict polyloops = {f'polyloop {i}': polyloop for i, polyloop in enumerate(polyloops)} # Process each polyloop self.polyloops = { name: utils.fill_gaps_with_rests(polyloop) if insert_rests else polyloop for name, polyloop in polyloops.items() }
[docs] def plot_polyloops(self, pulse=1/4, colors=None, renderer=None): """ Plots the given polyloops as a radar chart, including arcs to represent the duration of each note. Parameters: - pulse (float): The duration of each pulse in beats. Defaults to 1/4. - colors (list): A list of colors to use for the plot. If not provided, a default color scheme will be used. - renderer (str): The Plotly renderer to use. If None, will try to auto-detect the renderer. Returns: - fig (plotly.graph_objects.Figure): The generated radar chart figure. """ # Configure Plotly renderer for different environments try: import plotly.io as pio # Only configure if renderer is specified or not already set if renderer: pio.renderers.default = renderer # If no renderer is specified but we're in a known environment, configure automatically elif 'marimo' in str(type(globals().get('mo', ''))): pio.renderers.default = 'notebook' except (ImportError, AttributeError): pass # Get polyloop names and sequences polyloop_names = list(self.polyloops.keys()) polyloops = list(self.polyloops.values()) # Filter out rests polyloops_without_rests = [[note for note in polyloop if note[0] is not None] for polyloop in polyloops] n_polyloops = len(polyloops) traces = [] if colors is None: colors = [colorsys.hsv_to_rgb(i/n_polyloops, 1, 1) for i in range(n_polyloops)] colors = ['rgba(%d, %d, %d, 0.5)' % (int(r*255), int(g*255), int(b*255)) for r, g, b in colors] fig = go.Figure() for i, (polyloop_name, polyloop) in enumerate(zip(polyloop_names, polyloops_without_rests)): for pitch, duration, offset in polyloop: start_theta, duration_theta = offset * 360 / self.measure_length, duration * 360 / self.measure_length arc = np.linspace(start_theta, start_theta + duration_theta, 100) # Generate points for a smooth arc r = [n_polyloops-i-1] * 100 # Constant radius for the arc fig.add_trace(go.Scatterpolar( r=r, theta=arc % 360, # Ensure theta is within 0-360 range mode='lines', line=dict(color='rgba(60, 60, 60, 0.65)', width=8), name=f'{polyloop_name} Duration', showlegend=False )) for pitch, duration, offset in polyloop: start_theta, end_theta = offset * 360 / self.measure_length, (offset + duration) * 360 / self.measure_length for theta in [start_theta, end_theta]: fig.add_trace(go.Scatterpolar( r=[n_polyloops-i-0.9, n_polyloops-i-1.1], theta=[theta % 360, theta % 360], mode='lines', line=dict(color='Black', width=3), name=f'{polyloop_name} Start/End', showlegend=False )) if polyloop: start_thetas = [offset * 360 / self.measure_length for _, _, offset in polyloop] start_thetas.append(start_thetas[0]) traces.append(go.Scatterpolar( r=[n_polyloops-i-1]*(len(polyloop)+1), # Account for the loop closure theta=start_thetas, mode='lines', line=dict(color='rgba(0, 0, 0, 0.65)', width=1), fill='toself', fillcolor=colors[i % len(colors)], name=polyloop_name, showlegend=True )) for trace in reversed(traces): fig.add_trace(trace) tickvals = np.linspace(0, 360, int(self.measure_length/pulse), endpoint=False) ticktext = [str(i % self.measure_length) for i in np.arange(0, self.measure_length, pulse)] radial_tickvals = np.arange(0, n_polyloops, 1) fig.update_layout( polar=dict( radialaxis=dict( visible=True, range=[n_polyloops, -0.1], tickvals=radial_tickvals, ticktext=polyloop_names ), angularaxis=dict( tickvals=tickvals, ticktext=ticktext, direction="clockwise", rotation=90 ) ), template='none', showlegend=True ) fig.add_annotation( x=0.5, y=0.5, text="↻", showarrow=False, font=dict(size=30, color='White'), xref="paper", yref="paper" ) return fig