User:Sevish/Quasi-periodic golden ratio based scales

From Xenharmonic Wiki
Jump to navigation Jump to search

Sevish's note: I arrived at my approach with some intuition and experimentation, but there may be errors here. Corrections and improvements are welcome. I would like this information to be included on the wiki somewhere as others may find these scales as interesting as I do.

There exist musical scales that are the 1-dimensional analogue of a Penrose tiling. That is to say, the scales have these properties:

  • Quasi-periodic: there is no exact repetition within the structure, but instead the pitches are distributed based on ratios involving the golden ratio. This creates a structured but aperiodic pattern.
  • Subdivision rules: subdivision happens in a deterministic way, by taking the leftmost interval of the largest step size and subdividing it according to the golden proportion.
  • Fractal: the process generates self-similar patterns, with smaller segments retaining the same proportional relationships as larger ones, analogous to the hierarchical structure in Penrose tilings.

Example scales

The labels pentatonic, diatonic and chromatic may be misleading here, but are chosen because the scales more-or-less have the same density as an octave-repeating pentatonic, diatonic or chromatic scale respectively. The pentatonic version is a good place to start jamming casually.

The scales have these musical properties:

  • Irrational intervals: all intervals in the scale are derived from the golden ratio and could be considered a kind of anti-just intonation.
  • Acoustic phi: the interval of acoustic phi (~1.618 or ~833 cents) shows up a large number of times. This interval maximally avoids harmonic ratios when stacked, so when played using a timbre that contains mainly harmonic overtones it sounds rough. On the other hand, when played using a timbre that contains mainly golden ratio based overtones it sounds smooth.
  • Arbitrary scale density: because the scales are generated by an iterative process, you can run additional iterations to get a scale with more intervals that are more densely packed together.
  • Lack of tonic: for the pentatonic example in particular, as you run stepwise through the scale, it never feels like it lands on a tonic.
  • Two-step: if the number of intervals in the scale is a Fibonacci number, the scale will always contain exactly two step sizes.
  • Applicable to xenrhythm: when creating tunings based on these scales, the lowest frequency can be something very low i.e. in the tempo range of human perception. Any use of polytempi will be irrational and therefore xenrhythmic. Music has been written that incorporates both tempi and pitches that derive from one such tuning.

Caveats:

  • The quasi-periodic, non-repeating pattern will continue infinitely and can't be represented in a finite medium such as this document or a Scala file. Instead, we can work with a finite segment of the structure and do a finite number of subdivisions on it.
  • The diatonic and chromatic variants above sound vaguely 12-ish to me.
  • Despite all intervals being irrational, some may come close enough to rational intervals to be perceived as such. E.g. ~711 cents may be perceived as a perfect fifth. As the number of subdivisions increases, approximations to rational intervals become more accurate.

Scale generation process

  1. Given that we want to produce a finite segment of an infinite structure, we should start by determining the largest interval of the scale (which will be subdivided later). Ideally this interval should be larger than the total range of human perception of frequency, so that we can't distinguish our finite segment from the infinite one.
    1. Determine the total range of human perception of frequency, expressed as an interval.
      1. If you only care about pitches, the typical range is 20Hz-20000Hz, so the resulting interval would be 20000/20 = 1000 (11958.9 cents).
      2. If you want this scale wide enough to cover pitches and tempi, the typical range is 0.5Hz-20000Hz, so the resulting interval would be 20000/0.5 = 40000 (18345.3 cents).
    2. Find the smallest phi^phi^n value that is larger than the interval you just calculated.
      1. phi^phi^0 = 1.618033989
      2. phi^phi^1 = 2.178457568
      3. phi^phi^2 = 3.524818388
      4. phi^phi^3 = 7.678667293
      5. phi^phi^4 = 27.06590767
      6. phi^phi^5 = 207.8301
      7. phi^phi^6 = 5625.110297
      8. phi^phi^7 = 1169067.235
    3. The largest interval of the scale should therefore be phi^phi^6 = 5625.110297 (14949.2 cents). This was assuming that we only want a scale large enough to cover the pitch range.
  2. Begin the process of iterative subdivision.
    1. Find the leftmost interval of the largest step size.
    2. Divide that interval into logarithmically golden proportions.
  3. Repeat steps 2.1 and 2.2 until enough intervals are generated.

Compositions based on this scale

Durationplex - Sevish (2025)

Scale generation script

The following javascript is wrapped in HTML, so can be saved as a .html document and opened in a web browser to generate more scales. It will generate 144 values, which can be copied and pasted into Scale Workshop. Scale Workshop will then assign the first 128 values to the MIDI note numbers and ignore the rest.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="color-scheme" content="dark">
    <title>Fractal scale</title>
    <style>
        @media only screen and (min-width: 640px) {
            #results {
                columns: 4;
            }
        }
    </style>
</head>
<body>
    <pre id="results"></pre>
    <script>
        document.addEventListener("DOMContentLoaded", function() {




            // Starting ratio which will seed the scale
            let ratio = 1.6180339887498948;

            // How big the scale is
            let size = 144;

            // Calculate the frame interval
            let min_frame = 1000;
            let frame = ratio;
            while (frame < min_frame) {
                console.log("Current frame:", frame);
                frame = Math.pow(frame, ratio);
            }
            console.log("Frame:", frame);

            // Convert frame from decimal to cents
            frame = 1200 * Math.log2(frame);
            console.log("Frame in cents:", frame);

            // Assemble an array called "scale" to store the cents values we generate
            let scale = [0];
            scale.push(frame);

            // Number of iterations to calculate
            let iterations = size - 1;

            // Do iterations
            while (iterations !== 0) {

                console.log("Iteration:", iterations);

                // Find the largest distance between two values in the array
                let maxDistance = 0;
                let maxDistanceIndex = 0;

                for (let i = 0; i < scale.length - 1; i++) {
                    
                    const distance = Math.abs(scale[i + 1] - scale[i]);

                    if (distance > maxDistance) {
                        maxDistance = distance;
                        maxDistanceIndex = i;
                    }
                }

                // Add a new value that marks the ratio between the values with the largest distance
                const distance = scale[maxDistanceIndex + 1] - scale[maxDistanceIndex];
                const newValue = (distance / ratio) + scale[maxDistanceIndex];
                scale.push(newValue);

                // Sort the array numerically in ascending order
                scale.sort((a, b) => a - b);

                // End of this iteration
                iterations--;
            }

            // We now have size+1 values in the scale, so remove the first 0 cent interval to get a scale of the expected size
            scale.shift();

            // Output results
            console.log(scale);
            const resultsElement = document.getElementById("results");

            for (let i = 0; i < scale.length - 1; i++) {
                if ( i !== 0) {
                    resultsElement.innerText += '\n';
                }
                resultsElement.innerText += scale[i];
            }


        });
    </script>
</body>
</html>