Skip to content

Instantly share code, notes, and snippets.

@usrbinkat
Created February 28, 2025 21:40
Show Gist options
  • Select an option

  • Save usrbinkat/fc6df24e5d3f58c3d5fde8aa1d62fd79 to your computer and use it in GitHub Desktop.

Select an option

Save usrbinkat/fc6df24e5d3f58c3d5fde8aa1d62fd79 to your computer and use it in GitHub Desktop.
Playing with Music Theory in UOR

Universal Object Reference (UOR) for Music Theory: A Rigorous Computational Framework

1. Introduction

Music, as a highly structured yet expressive system of organized sound, is fundamentally governed by mathematical principles. The Universal Object Reference (UOR) framework offers a robust unifying paradigm for encoding and generating music across distinct traditions by situating musical elements within a well-defined algebraic and geometric space. This document articulates an extensible UOR-based approach to music theory, emphasizing both theoretical rigor and computational applicability in music analysis, synthesis, and generative composition.

Framework Objectives

  • Formalize music fundamentals using precise mathematical structures.
  • Embed music theory in UOR through Clifford algebraic encodings, Lie group symmetries, and coherence norms.
  • Represent diverse musical systems (Western Classical, Hindustani Classical, and Gamelan) within a UOR framework, facilitating comparative and generative studies.
  • Integrate AI-driven methodologies for computational music generation and transformation within UOR.
  • Develop a hierarchical framework for music representation, supporting multi-level embeddings from sound waves to complex polyphonic structures.
  • Enable cross-cultural musical transformations, allowing seamless mapping between different tuning systems, rhythmic patterns, and harmonic traditions.
  • Facilitate real-time music generation and analysis, integrating symbolic and audio-based processing within a unified computational paradigm.
  • Demonstrate practical applications of UOR-based musical transformations using case studies from multiple musical traditions.
  • Refine the connection between UOR and existing mathematical frameworks in computational musicology.
  • Justify the necessity of Clifford algebras and Lie groups for music representation.

2. Mathematical Formalization of Music

2.1 Why Clifford Algebras for Music Representation?

Unlike traditional representations such as MIDI or pitch-class sets, Clifford algebras provide a mathematically rigorous framework for handling multi-dimensional transformations in music. They allow:

  • Encoding hierarchical structures such as harmonic progressions as multivectors.
  • Capturing non-commutative relationships that naturally arise in counterpoint and voice leading.
  • Preserving geometric structure when transforming between tuning systems or rhythmic patterns.
  • Facilitating efficient computational operations through sparse multivector representations.

2.2 Pitch and Frequency as Mathematical Objects

Define the pitch space $P$: [ P = { f | f \in \mathbb{R}^+, f \text{ is a fundamental frequency} } ] where each pitch corresponds to a frequency in Hertz (Hz). The standard equal-tempered tuning system is given by: [ f_n = f_0 \cdot 2^{n/12} ] where $n$ denotes the number of semitones from a reference frequency $f_0$ (e.g., 440 Hz for A4).

To generalize beyond Western tuning systems, we introduce: [ f_n = f_0 \cdot r_n ] where $r_n$ represents culturally specific tuning ratios (e.g., just intonation, non-Western microtonal divisions, Gamelan pelog/slendro scales).

This approach aligns with prior research in computational tuning systems, such as those explored by Sethares in adaptive tuning and historical pitch studies.

Python Implementation: Multi-System Pitch Representation

import numpy as np

def generate_frequencies(f0=440, semitones=12, tuning_ratios=None):
    if tuning_ratios is None:
        return np.array([f0 * (2 ** (n / semitones)) for n in range(-48, 49)])
    return np.array([f0 * ratio for ratio in tuning_ratios])

frequencies = generate_frequencies()
print("Equal temperament frequencies:", frequencies)

2.3 Case Study: Transforming Western C Major to Hindustani Bilawal

A crucial application of UOR is the ability to transform a Western scale into its Hindustani equivalent, adjusting for microtonal variations and hierarchical weighting.

Python Implementation: Western to Hindustani Transformation

import numpy as np

def western_to_hindustani_transformation(western_scale, raga_type="bilawal"):
    transformations = {
        "bilawal": np.array([1.0, 0.0, 0.0, 0.02, 0.0, 0.0, -0.02]),
        "kafi": np.array([1.0, 0.0, -0.03, 0.0, 0.0, -0.03, 0.0]),
        "bhairavi": np.array([1.0, -0.03, -0.03, 0.0, 0.0, -0.03, -0.03])
    }
    if raga_type in transformations:
        return western_scale * (2 ** (transformations[raga_type]/12))
    return western_scale

c_major = np.array([261.63, 293.66, 329.63, 349.23, 392.0, 440.0, 493.88])
bilawal = western_to_hindustani_transformation(c_major, "bilawal")
print("Hindustani Bilawal frequencies:", bilawal)

2.4 Expressivity and Emotion in UOR Music Representation

UOR extends beyond structural representation to capture musical expressivity:

  • Rubato and microtiming deviations are encoded as Lie group transformations on rhythm.
  • Ornamentations and glissandi are modeled as continuous Lie algebra operations on pitch vectors.
  • Expressive weightings assign emotional intensity to musical events, enhancing generative models.

Python Implementation: Expressive Performance Modeling

import numpy as np

def apply_rubato(note_durations, depth=0.1):
    rubato_curve = np.linspace(-depth, depth, len(note_durations))
    return note_durations * (1 + rubato_curve)

durations = np.array([1.0, 0.8, 1.2, 1.0, 0.9, 1.1])
expressive_durations = apply_rubato(durations)
print("Expressive durations:", expressive_durations)

5. Conclusion

This framework integrates UOR principles with mathematical music theory and computational musicology to construct a robust analytical model. By embedding fundamental musical elements within Clifford algebraic structures and leveraging Lie group transformations, we create a system that is both theoretically grounded and computationally practical. The next steps involve refining AI-driven expressivity modeling, implementing real-time cross-cultural music transformation experiments, and benchmarking computational performance to ensure feasibility for large-scale applications.

Universal Object Reference (UOR) for Music Theory: A Comprehensive Computational Framework

1. Introduction

Music represents one of humanity's most sophisticated encoding systems—a language where mathematical structure, cultural context, and artistic expression converge. The Universal Object Reference (UOR) framework offers a powerful paradigm for representing this complexity by embedding musical objects within a unified geometric and algebraic space that preserves their multidimensional relationships. This document presents a comprehensive UOR-based approach to music theory, bridging pure mathematics, computational musicology, and artistic expression.

Why UOR for Music Theory?

The UOR framework provides unique advantages for musical representation that address longstanding challenges in computational musicology:

  1. Unified Representation: Unlike domain-specific representations (MIDI, MusicXML, etc.), UOR enables a single mathematical framework to represent elements across all levels of musical structure—from acoustic waves to cultural systems.

  2. Dimensional Integrity: Clifford algebras preserve the dimensional characteristics of musical objects, allowing pitch, rhythm, and timbre to maintain their distinct mathematical properties while interacting in well-defined ways.

  3. Transformation Coherence: Lie group transformations provide mathematically rigorous ways to model how musical elements transform (e.g., from one tuning system to another) while preserving essential relationships.

  4. Cultural Neutrality: By starting from first mathematical principles rather than Western-centric notational systems, UOR avoids privileging any single musical tradition.

  5. Computational Implementability: Despite its mathematical sophistication, UOR representations can be computed efficiently through sparse encodings and parallelizable operations.

Framework Objectives

  • Formalize music fundamentals using precise mathematical structures that maintain expressive capacity.
  • Embed music theory in UOR through Clifford algebraic encodings, Lie group symmetries, and coherence norms.
  • Represent diverse musical systems (Western, Hindustani, Gamelan, etc.) within a unified framework.
  • Demonstrate practical applications through detailed musical case studies across traditions.
  • Enable cross-cultural musical transformations between different tuning systems, rhythmic patterns, and harmonic traditions.
  • Integrate expression and emotion within the mathematical framework.
  • Create computationally efficient implementations for real-time applications.

2. Mathematical Foundations: Why Clifford Algebras for Music?

2.1 The Multidimensional Nature of Musical Objects

Music inherently comprises multiple independent dimensions—pitch exists orthogonally to rhythm, timbre orthogonally to both, and so on. Clifford algebras provide a natural mathematical framework for representing and manipulating such multidimensional objects while preserving their dimensional integrity.

Unlike simpler vector spaces that treat dimensions as fully independent, Clifford algebras enable sophisticated multivector operations that capture how musical dimensions interact—for example, how timbre affects perceived pitch or how rhythmic placement influences harmonic function. This is crucial for modeling the actual complexity of musical perception.

Mathematical Definition: Musical Space in UOR

The musical space $M$ is defined as a graded Clifford algebra $Cl(V, Q)$ where:

  • $V$ is a vector space with basis elements corresponding to fundamental musical dimensions
  • $Q$ is a quadratic form encoding relationships between these dimensions

This formulation extends Tymoczko's geometric approach to voice-leading, Lewin's transformational theory, and Lerdahl's generative theory into a unified computational framework.

2.2 Pitch Representation

Pitch is embedded in a continuous frequency space $P \subset V$:

[ P = { f | f \in \mathbb{R}^+, f \text{ is a fundamental frequency} } ]

Using UOR's multi-base representation principle, we simultaneously encode pitch in multiple reference systems:

  1. Logarithmic frequency (scientific/acoustic representation): [f_n = f_0 \cdot 2^{n/12}]

  2. Ratio-based tuning (cultural representation): [f_n = f_0 \cdot r_n]

  3. Integer-relationship (mathematical representation): [f_n = f_0 \cdot \frac{p}{q}, p,q \in \mathbb{Z}^+]

This multi-representation approach allows the UOR framework to:

  • Model how humans perceive pitch differences logarithmically
  • Represent cultural tuning systems precisely
  • Identify mathematical relationships between notes

Python Implementation: Multi-System Pitch Representation

import numpy as np
from fractions import Fraction

class UORPitchSpace:
    def __init__(self, reference_frequency=440):
        self.reference = reference_frequency
        self.dimensions = 3  # Frequency, cents, ratio dimensions

    def equal_tempered_pitch(self, semitones):
        """Equal temperament representation (12-TET)"""
        return self.reference * (2 ** (semitones/12))
        
    def ratio_pitch(self, numerator, denominator):
        """Just intonation representation"""
        return self.reference * (numerator/denominator)
        
    def cultural_system(self, system_name, degree):
        """Cultural tuning systems"""
        systems = {
            "slendro": [1, 1.15, 1.31, 1.52, 1.74, 2],   # Approximated Gamelan slendro
            "pelog": [1, 1.07, 1.2, 1.4, 1.5, 1.67, 1.87, 2],  # Approximated pelog
            "just_major": [1, 9/8, 5/4, 4/3, 3/2, 5/3, 15/8, 2]  # Just intonation major
        }
        if system_name in systems and degree < len(systems[system_name]):
            return self.reference * systems[system_name][degree]
        return None
    
    def to_clifford_vector(self, frequency):
        """Convert frequency to Clifford algebra representation"""
        # Create multivector with components:
        # 1. Scalar component: octave number
        # 2. Vector component e1: normalized position within octave
        # 3. Vector component e2: deviation from equal temperament
        # This is a simplified representation showing the principle
        import math
        from clifford import Cl
        
        algebra, blades = Cl(2)  # 2D Clifford algebra
        
        # Calculate octave and position within octave
        octave = math.log2(frequency/self.reference) // 1
        position_in_octave = (math.log2(frequency/self.reference) % 1) * 12
        
        # Calculate closest equal-tempered pitch
        closest_et_semitone = round(position_in_octave)
        closest_et_freq = self.equal_tempered_pitch(closest_et_semitone)
        
        # Calculate deviation in cents
        deviation = 1200 * math.log2(frequency/closest_et_freq)
        
        # Create multivector
        mv = octave + position_in_octave * blades['e1'] + (deviation/100) * blades['e2']
        return mv

# Example usage
pitch_space = UORPitchSpace()
f = pitch_space.equal_tempered_pitch(3)  # C#/Db above A440
print(f"Frequency: {f:.2f} Hz")
print(f"Clifford representation: {pitch_space.to_clifford_vector(f)}")

This code demonstrates how UOR represents pitch in multiple systems simultaneously while preserving their relationships. The Clifford algebra representation encodes octave, position within octave, and deviation from equal temperament as different geometric dimensions, allowing us to perform meaningful transformations between tuning systems.

2.3 Scale Systems and Transformations

Scales are ordered subsets of pitch space, $S \subset P$, that serve as the foundation for melodic and harmonic organization. In UOR, scales are represented as paths through pitch space, encoded as:

[ S = { f_0, f_1, \dots, f_n } ]

The power of UOR comes from its ability to represent transformations between scale systems through Lie group actions. Given a scale $S$ and transformation $g \in G$ from a Lie group of musical transformations, we define:

[ S' = g \cdot S ]

This mathematical machinery enables precise modeling of:

  • Modal transpositions in Western, Maqam, and Raga systems
  • Microtonal shifts between just intonation and equal temperament
  • Complex cultural transformations like Hindustani "murchhana" (modal rotation)

Why Lie Groups? Lie groups provide the necessary mathematical structure to model continuous transformations while preserving essential musical relationships. Unlike discrete transformations, Lie groups can represent subtle tuning adjustments, expressive intonation, and microtonal variations that are essential to musical expressivity across cultures.

Example: Western to Hindustani Scale Transformation

import numpy as np
import matplotlib.pyplot as plt

def western_to_hindustani_transformation(western_scale, raga_type="bilawal"):
    """Transform Western scale to Hindustani raga structure using UOR principles"""
    # Define transformation matrices for different raga types
    # These encode the subtle microtonal adjustments and hierarchical emphasis
    transformations = {
        "bilawal": np.array([1.0, 0.0, 0.0, 0.02, 0.0, 0.0, -0.02]),  # Bilawal ≈ major scale with adjustments
        "kafi": np.array([1.0, 0.0, -0.03, 0.0, 0.0, -0.03, 0.0]),    # Kafi ≈ dorian with adjustments
        "bhairavi": np.array([1.0, -0.03, -0.03, 0.0, 0.0, -0.03, -0.03])  # Bhairavi ≈ phrygian with adjustments
    }
    
    # Apply microtonal adjustments (in semitones)
    if raga_type in transformations:
        adjusted_scale = western_scale * (2 ** (transformations[raga_type]/12))
        
        # Apply emphasis weights (hierarchy is critical in raga)
        emphasis = {
            "bilawal": [1.0, 0.4, 0.7, 0.5, 0.9, 0.6, 0.4],
            "kafi": [1.0, 0.5, 0.9, 0.5, 0.8, 0.9, 0.4],
            "bhairavi": [1.0, 0.9, 0.6, 0.8, 0.5, 0.9, 0.6]
        }
        
        return adjusted_scale, emphasis[raga_type]
    
    return western_scale, [1.0] * len(western_scale)

# Create Western C major scale
c_major = np.array([261.63, 293.66, 329.63, 349.23, 392.0, 440.0, 493.88])

# Transform to Hindustani equivalent (Bilawal thaat)
bilawal_frequencies, bilawal_emphasis = western_to_hindustani_transformation(c_major, "bilawal")

# Visualize the transformation
plt.figure(figsize=(12, 6))
plt.plot(range(7), [0] * 7, 'ro', markersize=10, label='C Major Scale')
plt.plot(range(7), (bilawal_frequencies - c_major), 'bo', markersize=[e*10 for e in bilawal_emphasis],
         label='Bilawal Adjustments')
plt.axhline(y=0, color='k', linestyle='-', alpha=0.3)
plt.grid(True, alpha=0.3)
plt.title('Western C Major to Hindustani Bilawal Transformation')
plt.ylabel('Frequency Adjustment (Hz)')
plt.xticks(range(7), ['Sa (C)', 'Re (D)', 'Ga (E)', 'Ma (F)', 'Pa (G)', 'Dha (A)', 'Ni (B)'])
plt.legend()
plt.tight_layout()
# plt.show()  # Uncomment to display chart

print("Western C Major frequencies:", c_major)
print("Hindustani Bilawal frequencies:", bilawal_frequencies)
print("Note emphasis in Bilawal:", bilawal_emphasis)

2.4 Rhythmic Structures and Temporal Embedding

Rhythm, like pitch, possesses a multidimensional structure that Clifford algebras can effectively represent. In UOR, we define:

  1. Temporal positioning (when events occur)
  2. Durational structure (how long events last)
  3. Hierarchical organization (metric accent patterns)

These dimensions interact in complex ways that simple vector representations cannot capture. For example, in a typical Western 4/4 measure, the first beat has both temporal position (start of measure) and hierarchical importance (primary accent). In Indian tala or Javanese gamelan structures, these relationships become even more complex.

The UOR framework represents rhythm as:

[ R = \sum_{i=1}^{n} w_i \cdot e_i \wedge t_i ]

Where:

  • $e_i$ represents position in cyclic time
  • $t_i$ represents linear time
  • $w_i$ represents accent weight
  • $\wedge$ is the exterior product from Clifford algebra

This representation enables modeling complex rhythmic structures like:

  • Western polymeter and polyrhythm
  • Hindustani tala with variable-length vibhags
  • Gamelan colotomic structures
  • West African cross-rhythms

Implementation: Complex Rhythmic Patterns

import numpy as np
from fractions import Fraction

class UORRhythmSpace:
    def __init__(self):
        self.dimensions = 3  # Position, duration, accent dimensions

    def western_meter(self, time_signature, subdivisions=16):
        """Generate Western metric structure"""
        beats_per_measure, beat_unit = map(int, time_signature.split('/'))
        total_subdivisions = beats_per_measure * (subdivisions // beat_unit)
        
        # Create position values (normalized within measure)
        positions = np.linspace(0, 1, total_subdivisions, endpoint=False)
        
        # Create duration values (as fraction of measure)
        durations = np.ones(total_subdivisions) * (1/total_subdivisions)
        
        # Create accent pattern (primary and secondary beats)
        accents = np.zeros(total_subdivisions)
        for i in range(0, total_subdivisions, subdivisions // beat_unit):
            if i == 0:  # Primary accent on first beat
                accents[i] =.1.0
            elif i % (2 * (subdivisions // beat_unit)) == 0:  # Secondary accents
                accents[i] = 0.7
            elif i % (subdivisions // beat_unit) == 0:  # Tertiary accents
                accents[i] = 0.4
        
        return {"positions": positions, "durations": durations, "accents": accents}
    
    def hindustani_tala(self, tala_name):
        """Generate Hindustani tala structure"""
        # Define common talas with their vibhag (section) structure and theka (pattern)
        tala_definitions = {
            "teental": {
                "vibhags": [4, 4, 4, 4],  # Four sections of 4 beats each
                "theka": ["dha", "dhin", "dhin", "dha", "dha", "dhin", "dhin", "dha", 
                          "dha", "tin", "tin", "ta", "ta", "dhin", "dhin", "dha"],
                "accents": [1.0, 0.5, 0.5, 0.7, 0.8, 0.5, 0.5, 0.7, 
                           0.9, 0.5, 0.5, 0.7, 0.8, 0.5, 0.5, 1.0]
            },
            "jhaptaal": {
                "vibhags": [2, 3, 2, 3],  # 2+3+2+3 = 10 beats
                "theka": ["dhi", "na", "dhi", "dhi", "na", "ti", "na", "dhi", "dhi", "na"],
                "accents": [1.0, 0.6, 0.8, 0.5, 0.7, 0.9, 0.6, 0.8, 0.5, 0.7]
            },
            "ektaal": {
                "vibhags": [2, 2, 2, 2, 2, 2],  # Six sections of 2 beats each
                "theka": ["dhin", "dhin", "dha", "dha", "dhin", "dhin", 
                          "dha", "dha", "dhin", "dhin", "dha", "dha"],
                "accents": [1.0, 0.6, 0.8, 0.5, 0.7, 0.6, 0.8, 0.5, 0.9, 0.6, 0.8, 0.7]
            }
        }
        
        if tala_name not in tala_definitions:
            return None
            
        tala = tala_definitions[tala_name]
        total_beats = sum(tala["vibhags"])
        
        # Create position values
        positions = np.linspace(0, 1, total_beats, endpoint=False)
        
        # Create duration values
        durations = np.ones(total_beats) * (1/total_beats)
        
        # Create hierarchical structure (sam is strongest, followed by tali & khali)
        # In actual practice, these would be derived from the vibhag structure
        accents = np.array(tala["accents"])
        
        return {
            "positions": positions, 
            "durations": durations, 
            "accents": accents,
            "vibhags": tala["vibhags"],
            "theka": tala["theka"]
        }
    
    def to_clifford_representation(self, rhythm_data):
        """Convert rhythm data to Clifford algebra representation"""
        from clifford import Cl
        
        # Create 3D Clifford algebra (position, duration, accent)
        algebra, blades = Cl(3)
        
        # Create multivector representation
        multivectors = []
        for i in range(len(rhythm_data["positions"])):
            # Position component (e1)
            # Duration component (e2)
            # Accent component (e3)
            # Also include the e1^e2 component to represent position-duration relationship
            mv = (rhythm_data["positions"][i] * blades['e1'] + 
                  rhythm_data["durations"][i] * blades['e2'] + 
                  rhythm_data["accents"][i] * blades['e3'] + 
                  rhythm_data["positions"][i] * rhythm_data["durations"][i] * blades['e12'])
            multivectors.append(mv)
            
        return multivectors

# Example usage
rhythm_space = UORRhythmSpace()
teental = rhythm_space.hindustani_tala("teental")
print("Teental (16-beat cycle):")
for i in range(len(teental["theka"])):
    print(f"Beat {i+1}: {teental['theka'][i]}, Accent: {teental['accents'][i]:.2f}")

# Western 4/4 for comparison
western_44 = rhythm_space.western_meter("4/4")

This implementation demonstrates how UOR can represent complex rhythmic structures from different traditions while preserving their distinctive organizational principles.

3. Expressivity and Emotion in UOR Music Representation

One of the core challenges in computational music representation is capturing expressive elements—the subtle variations in timing, dynamics, articulation, and timbre that convey emotion and artistic interpretation. The UOR framework addresses this challenge through several mechanisms:

3.1 Continuous Rather Than Discrete Representation

Unlike MIDI or traditional notation that quantize musical parameters, UOR's Clifford algebra representation supports continuous values across all dimensions. This enables modeling of:

  • Microtonal pitch inflections (crucial in non-Western traditions)
  • Subtle timing variations (rubato, swing, layakari)
  • Continuous dynamic changes
  • Spectral evolution of timbres

3.2 Embedding Expressive Parameters in Multivectors

Expressive parameters are encoded as additional dimensions in the Clifford algebra:

[ E = P \wedge R \wedge D \wedge A \wedge T ]

Where:

  • $P$: Pitch
  • $R$: Rhythm
  • $D$: Dynamics
  • $A$: Articulation
  • $T$: Timbre
  • $\wedge$: Exterior product that combines these dimensions

This multidimensional approach captures how these parameters interact—for example, how articulation affects timbre or how dynamics influence perceived rhythm.

Implementation: Expressive Performance Model

class UORExpressivePerformance:
    def __init__(self):
        self.dimensions = 5  # Pitch, rhythm, dynamics, articulation, timbre
        
    def apply_expressive_transformation(self, base_notes, performance_style):
        """Apply expressive transformation to a sequence of notes"""
        if performance_style == "romantic_rubato":
            # Apply timing variations (rubato)
            timing_curve = self._generate_timing_curve(len(base_notes), depth=0.15)
            
            # Apply dynamic variations
            dynamic_curve = self._generate_dynamic_curve(len(base_notes), depth=0.2)
            
            # Apply articulation variations
            articulation = self._generate_articulation_pattern(len(base_notes), style="legato")
            
            # Create transformed notes
            transformed_notes = []
            for i, note in enumerate(base_notes):
                expressive_note = note.copy()
                expressive_note["duration"] *= (1 + timing_curve[i])  # Stretch/compress duration
                expressive_note["velocity"] *= (1 + dynamic_curve[i])  # Modify dynamics
                expressive_note["articulation"] = articulation[i]     # Apply articulation
                transformed_notes.append(expressive_note)
                
            return transformed_notes
            
        elif performance_style == "north_indian_ornamentation":
            # Apply pitch inflections (meend/glides)
            pitch_ornaments = self._generate_indian_ornaments(base_notes)
            
            # Apply rhythmic layakari (rhythmic elasticity)
            layakari = self._generate_layakari(len(base_notes))
            
            # Create transformed notes with Indian ornaments
            transformed_notes = []
            for i, note in enumerate(base_notes):
                if pitch_ornaments[i]["type"] == "meend":
                    # Create a glide between notes
                    start_pitch = note["pitch"]
                    end_pitch = base_notes[min(i+1, len(base_notes)-1)]["pitch"]
                    pitch_curve = np.linspace(start_pitch, end_pitch, 10)
                    
                    # Add all intermediate pitches as separate notes
                    for p in pitch_curve:
                        ornament_note = note.copy()
                        ornament_note["pitch"] = p
                        ornament_note["duration"] *= 0.1  # Shorter durations for each step
                        transformed_notes.append(ornament_note)
                else:
                    # Apply timing variations from layakari
                    expressive_note = note.copy()
                    expressive_note["duration"] *= layakari[i]
                    transformed_notes.append(expressive_note)
                    
            return transformed_notes
            
        # Default: return unchanged
        return base_notes
    
    def _generate_timing_curve(self, length, depth=0.1):
        """Generate a timing variation curve (rubato)"""
        # Create a smooth curve using sine functions
        x = np.linspace(0, 2*np.pi, length)
        curve = depth * np.sin(x) + (depth/2) * np.sin(2*x + 0.5)
        return curve
    
    def _generate_dynamic_curve(self, length, depth=0.1):
        """Generate a dynamic variation curve"""
        x = np.linspace(0, 3*np.pi, length)
        curve = depth * np.sin(x) + (depth/3) * np.sin(3*x + 1.0)
        return curve
        
    def _generate_articulation_pattern(self, length, style="legato"):
        """Generate articulation patterns"""
        if style == "legato":
            return np.ones(length) * 0.9  # 90% of full duration
        elif style == "staccato":
            return np.ones(length) * 0.3  # 30% of full duration
        elif style == "mixed":
            pattern = [0.9, 0.7, 0.8, 0.4]  # Repeating pattern
            return [pattern[i % len(pattern)] for i in range(length)]
        return np.ones(length)  # Default to full value
    
    def _generate_indian_ornaments(self, notes):
        """Generate Hindustani ornaments for a sequence of notes"""
        ornaments = []
        for i in range(len(notes)):
            # Randomly assign ornament types with appropriate probabilities
            r = np.random.random()
            if r < 0.2:
                ornament = {"type": "meend", "intensity": 0.7}  # Slide/glide
            elif r < 0.4:
                ornament = {"type": "kan", "intensity": 0.5}    # Grace note
            elif r < 0.5:
                ornament = {"type": "andolan", "intensity": 0.6}  # Oscillation
            else:
                ornament = {"type": "none", "intensity": 0}
            ornaments.append(ornament)
        return ornaments
    
    def _generate_layakari(self, length):
        """Generate North Indian rhythmic elasticity (layakari)"""
        # Create rhythmic elasticity that maintains overall timing
        patterns = [
            [1.0, 1.0, 1.0, 1.0],  # Regular timing
            [0.8, 1.2, 0.8, 1.2],  # Slight swing
            [2/3, 4/3, 1.0, 1.0],  # Triplet feel
            [1.5, 0.5, 1.0, 1.0]   # Dotted rhythm
        ]
        selected_pattern = np.random.choice(len(patterns))
        return [patterns[selected_pattern][i % len(patterns[selected_pattern])] 
                for i in range(length)]

This implementation demonstrates how UOR can model expressive performance parameters that are essential for musical expression across different cultural traditions.

4. Case Studies: UOR Applied to Diverse Musical Traditions

To demonstrate the practical utility of the UOR framework, we present concrete examples of its application to music from different traditions.

4.1 Case Study: Bach's Prelude in C Major (BWV 846)

The first prelude from the Well-Tempered Clavier exemplifies Western tonal harmony through arpeggiated patterns. In UOR representation:

  1. Harmonic Analysis:
    • Each chord is represented as a multivector in the Clifford algebra
    • Chord progressions form paths through the harmonic space
    • Voice-leading movements become geometric transformations
# Simplified example of the first four measures of Bach's Prelude in C Major
bach_progression = [
    {"chord": "C major", "notes": [60, 64, 67, 72], "function": "tonic"},
    {"chord": "D minor/C", "notes": [60, 62, 69, 74], "function": "supertonic"},
    {"chord": "G7/B", "notes": [59, 62, 67, 71], "function": "dominant"},
    {"chord": "C major", "notes": [60, 64, 67, 72], "function": "tonic"}
]

def analyze_voice_leading(progression):
    """Analyze voice leading using UOR geometric principles"""
    voice_leading_vectors = []
    
    for i in range(len(progression)-1):
        # Calculate the geometric transformation between consecutive chords
        current_notes = np.array(progression[i]["notes"])
        next_notes = np.array(progression[i+1]["notes"])
        
        # Voice leading vector (how each voice moves)
        movement = next_notes - current_notes
        
        # Calculate voice leading efficiency using L2 norm
        efficiency = np.linalg.norm(movement)
        
        # Determine if the voice leading is smooth (small movements) or disjoint
        smoothness = "smooth" if efficiency < 5 else "disjoint"
        
        voice_leading_vectors.append({
            "from": progression[i]["chord"],
            "to": progression[i+1]["chord"],
            "movement": movement,
            "efficiency": efficiency,
            "smoothness": smoothness
        })
    
    return voice_leading_vectors

# Analyze voice leading
analysis = analyze_voice_leading(bach_progression)
for a in analysis:
    print(f"Voice leading from {a['from']} to {a['to']}:")
    print(f"  Movement vector: {a['movement']}")
    print(f"  Efficiency: {a['efficiency']:.2f} ({a['smoothness']})")

4.2 Case Study: Hindustani Raga Yaman

Raga Yaman exemplifies the complex melodic and improvisational framework of North Indian classical music.

# Define Raga Yaman (similar to Lydian mode but with cultural-specific intonation)
raga_yaman = {
    "name": "Yaman",
    "aroha": [60, 62, 64, 66, 67, 69, 71, 72],  # Ascending scale
    "avaroha": [72, 71, 69, 67, 66, 64, 62, 60],  # Descending scale
    "vadi": 67,  # Most important note (G)
    "samvadi": 62,  # Second most important note (D)
    "pakad": [67, 69, 71, 67, 64, 66, 64, 62, 60],  # Characteristic phrase
    "time": "evening",
    "rasa": "romantic, devotional"
}

def generate_alap(raga, duration=120):
    """Generate opening alap (unmetered improvisation) following raga rules"""
    # This is a simplified model of alap generation following traditional progression
    
    # Phase 1: Introduce the raga gradually from middle octave
    phrases = []
    
    # Start with the vadi (most important note)
    phrases.append({"notes": [raga["vadi"]], "durations": [4.0]})
    
    # Introduce neighboring notes
    vadi_index = raga["aroha"].index(raga["vadi"])
    if vadi_index > 0:
        phrases.append({
            "notes": [raga["vadi"], raga["aroha"][vadi_index-1], raga["vadi"]],
            "durations": [2.0, 1.5, 2.5]
        })
    
    if vadi_index < len(raga["aroha"])-1:
        phrases.append({
            "notes": [raga["vadi"], raga["aroha"][vadi_index+1], raga["vadi"]],
            "durations": [2.0, 1.5, 2.5]
        })
    
    # Add characteristic phrase (pakad)
    phrases.append({
        "notes": raga["pakad"],
        "durations": [1.0] * len(raga["pakad"])
    })
    
    # More extensive exploration of the raga
    # (This would be much more sophisticated in a full implementation)
    phrases.append({
        "notes": raga["aroha"],
        "durations": [1.0] * len(raga["aroha"])
    })
    
    phrases.append({
        "notes": raga["avaroha"],
        "durations": [1.0] * len(raga["avaroha"])
    })
    
    return phrases

# Generate alap in Raga Yaman
alap_phrases = generate_alap(raga_yaman)
for i, phrase in enumerate(alap_phrases):
    print(f"Alap Phrase {i+1}:")
    note_names = [["C", "D", "E", "F#", "G", "A", "B", "C'"][n % 60] for n in phrase["notes"]]
    print(f"  Notes: {note_names}")
    print(f"  Durations: {phrase['durations']}")

4.3 Case Study: Javanese Gamelan - Lancaran Form

Gamelan music demonstrates fundamentally different organizing principles from Western and Indian traditions, with cyclical formal structures and interlocking patterns.

# Define a simplified Lancaran form in Pelog scale
lancaran = {
    "name": "Lancaran Form",
    "pathet": "sanga",  # Similar to mode
    "scale": "pelog",   # 7-tone Javanese scale
    "gong_cycle": 16,   # Beats in one gong cycle
    "structure": {
        "gong": [16],       # Largest gong on beat 16
        "kenong": [4, 8, 12, 16],  # Medium gong on beats 4, 8, 12, 16
        "kethuk": [2, 6, 10, 14],  # Small gong on even beats
        "kempul": [6, 10, 14]      # Hanging gong on beats 6, 10, 14
    }
}

def generate_balungan(lancaran):
    """Generate the balungan (core melody) for a lancaran form"""
    # Simplified algorithmic generation of a balungan in pelog scale
    # In actual practice, this would be a known composition or proper improvisation
    
    # Define pelog scale degrees (approximate pitches)
    pelog_scale = [60, 61, 63, 67, 68, 70, 73, 74]  # Approximation in MIDI notes
    
    # Create a basic 16-beat melody pattern following lancaran structure
    balungan = []
    for i in range(1, lancaran["gong_cycle"] + 1):
        # Choose notes that align with structural points
        if i in lancaran["structure"]["gong"]:
            # Use tonic on gong stroke
            balungan.append(pelog_scale[0])
        elif i in lancaran["structure"]["kenong"]:
            # Use fifth on kenong stroke
            balungan.append(pelog_scale[4])
        elif i in lancaran["structure"]["kempul"]:
            # Use fourth or seventh on kempul strokes
            balungan.append(pelog_scale[3 if i % 8 == 6 else 6])
        else:
            # Use neighboring tones for other beats
            balungan.append(pelog_scale[i % 5 + 1])
    
    return balungan

# Generate ciblon drum patterns for the lancaran form
def generate_ciblon_pattern(lancaran):
    """Generate ciblon drum patterns for the lancaran"""
    # Basic ciblon syllables
    syllables = ["tak", "dlang", "tung", "dah", "ket"]
    
    pattern = []
    for i in range(1, lancaran["gong_cycle"] + 1):
        if i in lancaran["structure"]["gong"]:
            pattern.append("dah")
        elif i in lancaran["structure"]["kenong"]:
            pattern.append("tung")
        elif i in lancaran["structure"]["kethuk"]:
            pattern.append("ket")
        elif i % 2 == 1:  # Odd beats
            pattern.append("tak")
        else:
            pattern.append("dlang")
    
    return pattern

# Generate balungan and ciblon pattern
balungan = generate_balungan(lancaran)
ciblon = generate_ciblon_pattern(lancaran)

# Display the generated patterns
print("Lancaran Form in Pelog Scale")
print("Balungan (Core Melody):", balungan)
print("Ciblon Drum Pattern:", ciblon)
print("\nCyclical Structure:")
for i in range(lancaran["gong_cycle"]):
    beat_num = i + 1
    structural_markers = []
    
    if beat_num in lancaran["structure"]["gong"]:
        structural_markers.append("GONG")
    if beat_num in lancaran["structure"]["kenong"]:
        structural_markers.append("kenong")
    if beat_num in lancaran["structure"]["kethuk"]:
        structural_markers.append("kethuk")
    if beat_num in lancaran["structure"]["kempul"]:
        structural_markers.append("kempul")
        
    structure = " ".join(structural_markers) if structural_markers else "-"
    print(f"Beat {beat_num}: {balungan[i]} ({ciblon[i]}) {structure}")

5. Computational Efficiency and Implementation Considerations

The mathematical sophistication of UOR raises legitimate questions about computational efficiency. We address these concerns through several implementation strategies:

5.1 Sparse Representation of Clifford Multivectors

Most musical objects have sparse representations in the Clifford algebra, with many dimensions set to zero. By implementing sparse storage and operations, we achieve significant computational efficiency:

class SparseMultivector:
    """Efficient sparse implementation of Clifford algebra multivectors"""
    def __init__(self, components=None):
        # Store only non-zero components as {basis_blade: coefficient}
        self.components = components or {}
    
    def __add__(self, other):
        """Add two multivectors"""
        result = self.components.copy()
        for blade, coef in other.components.items():
            if blade in result:
                result[blade] += coef
                # Remove zero components to maintain sparsity
                if abs(result[blade]) < 1e-10:
                    del result[blade]
            else:
                result[blade] = coef
        return SparseMultivector(result)
    
    def __mul__(self, other):
        """Geometric product of multivectors"""
        result = {}
        # For each pair of basis elements, compute their geometric product
        for blade1, coef1 in self.components.items():
            for blade2, coef2 in other.components.items():
                product_blade, sign = self._geometric_product_basis(blade1, blade2)
                if product_blade in result:
                    result[product_blade] += sign * coef1 * coef2
                else:
                    result[product_blade] = sign * coef1 * coef2
        
        # Remove zero components
        result = {k: v for k, v in result.items() if abs(v) > 1e-10}
        return SparseMultivector(result)
    
    def _geometric_product_basis(self, blade1, blade2):
        """Compute geometric product of two basis blades"""
        # This would implement the full geometric product rules
        # Simplified version for demonstration
        if blade1 == 1 or blade2 == 1:
            return (blade2 if blade1 == 1 else blade1), 1
            
        # Very simplified - assumes orthogonal basis
        # A real implementation would handle all cases properly
        if blade1 == blade2:
            return 1, 1  # e_i * e_i = 1 (simplified)
        else:
            # Lexicographic combination for demonstration
            combined = f"{blade1}^{blade2}"
            return combined, 1
    
    def __repr__(self):
        return f"SparseMultivector({self.components})"

5.2 Parallelization of Operations

Many UOR operations can be parallelized, leveraging modern multi-core processors and GPUs:

import numpy as np
from concurrent.futures import ProcessPoolExecutor

def parallel_transform_notes(notes, transformation_function, num_workers=4):
    """Apply a transformation to multiple notes in parallel"""
    chunk_size = max(1, len(notes) // num_workers)
    chunks = [notes[i:i + chunk_size] for i in range(0, len(notes), chunk_size)]
    
    with ProcessPoolExecutor(max_workers=num_workers) as executor:
        # Process each chunk in parallel
        results = list(executor.map(transformation_function, chunks))
    
    # Flatten results
    return [note for chunk in results for note in chunk]

5.3 Computation-Time Tradeoffs

UOR implementations can use approximations where appropriate, trading mathematical precision for computational efficiency:

def efficient_clifford_operations(mv1, mv2, operation_type, precision_level=2):
    """Perform Clifford algebra operations with adjustable precision"""
    if precision_level == 1:
        # Fast approximation - use only dominant components
        dominant_blades = set()
        for blade, coef in mv1.components.items():
            if abs(coef) > 0.1:  # Threshold for "dominant"
                dominant_blades.add(blade)
        
        for blade, coef in mv2.components.items():
            if abs(coef) > 0.1:
                dominant_blades.add(blade)
        
        # Only compute products involving dominant blades
        # Implementation details omitted for brevity
        
    elif precision_level == 2:
        # Medium precision - use sparse representation
        # Use the SparseMultivector implementation
        
    else:
        # Full precision - use complete computation
        # Implementation details omitted for brevity
    
    # Return result based on operation type and precision level

6. UOR's Advantages Over Traditional Music Representation Systems

The UOR framework offers several key advantages over traditional music representation systems:

  1. Mathematical Consistency: UOR provides a unified mathematical language for all musical elements, avoiding the inconsistencies of ad-hoc representations.

  2. Cross-Cultural Applicability: By starting from mathematical first principles rather than Western notation, UOR can represent diverse musical traditions without cultural bias.

  3. Continuity vs. Discretization: Unlike MIDI or staff notation that quantize musical parameters, UOR represents continuous values and transformations.

  4. Multi-Dimensional Relationships: Clifford algebras and Lie groups capture the complex relationships between musical dimensions that simpler representations cannot.

  5. Computational Implementability: Despite its mathematical sophistication, UOR can be efficiently implemented for practical applications.

7. Conclusion and Future Directions

The Universal Object Reference framework for music theory represents a significant advancement in our ability to model, analyze, and generate music across cultural traditions. By embedding musical objects in a unified mathematical framework based on Clifford algebras and Lie groups, UOR provides both theoretical rigor and practical applicability.

Future work will focus on:

  1. Real-time UOR implementations for live performance and interactive applications.
  2. Cross-cultural transformation experiments to validate the effectiveness of UOR in mapping between different musical traditions.
  3. Integration with AI music generation systems to create more culturally aware and expressive computational music.
  4. Development of UOR-based music notation systems that represent the full expressive range of diverse musical traditions.

The UOR framework bridges the gap between the mathematical elegance of music theory and the rich expressivity of musical practice, offering a powerful tool for both musicological understanding and computational creativity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment