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 (list): A list of polyloops. Each polyloop is expected to be in the form [(offset, pitch, duration), ...]. - 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 self.polyloops = [utils.fill_gaps_with_rests(polyloop) for polyloop in polyloops] if insert_rests else polyloops
[docs] def plot_polyloops(self, pulse=1/4, colors=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. Returns: - fig (plotly.graph_objects.Figure): The generated radar chart figure. """ self.polyloops = [self.polyloops] if not any(isinstance(i, list) for i in self.polyloops) else self.polyloops polyloops_without_rests = [[note for note in polyloop if note[0] is not None] for polyloop in self.polyloops] n_polyloops = len(self.polyloops) traces = [] #colors = go.Figure().layout.template.layout.colorway if colors is None else colors if colors is None: colors = [colorsys.hsv_to_rgb(i/n_polyloops, 1, 1) for i in range(n_polyloops)] #colors = [colorsys.hls_to_rgb(0, i/n_polyloops, 0) 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 in enumerate(polyloops_without_rests): for _, duration, offset in polyloop: # Ignore the pitch component 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), # colors[i % len(colors)] name=f'Polyloop {i+1} Duration', showlegend=False )) for _, 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 {i+1} 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], name=f'Polyloop {i}', showlegend=False )) 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=[str(i) for i in radial_tickvals] ), 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