Skip to content

TYLERSFOSTER/Discrete-Signal-Tonality

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

87 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

tonnetzB

Tonal Structures
for Harmonic Motion
between Discrete Signals

Welcome to disig, a Python package for exploring the tonal geometry of discrete periodic audio signals.

Inspired by Euler’s original Tonnetz and its modern reinterpretations, disig extends these structures to the digital domainβ€”where time is sampled, frequencies are modular, and multiplication replaces dilation.

At the core of this project is a categorical and signal-theoretic perspective on harmony, treating musical intervals as modular rescalings and organizing them into richly structured networks. These discrete tonnetze visualize the harmonic motion between audio signals under arithmetic transformations, revealing patterns that echo deep number-theoretic symmetries.

The underlying structure is a manifestation of a category of representations, where signals transform functorially under modular arithmetic operations. Tonnetz are diagrams of morphisms in this category that encode how spectral content behaves under group actions. This representation-theoretic framing situates tonal motion within a broader categorical picture: musical intervals and harmonic movement, and larger tonal structures all emerge from group actions on spectral data.

The library includes:

  • Tools for generating and analyzing Tonnetz diagrams over arbitrary moduli
  • Visualizers for arithmetic and geometric clusters in signal space
  • Audio synthesis utilities for testing tonal structures directly via WAV playback

Whether you're a theorist, signal processing researcher, or just curious about how number theory meets timbre, disig provides an experimental playground for navigating the space of harmonic motion in modular time.

TODOs...

  • Write total_to_wav function in './src/dissig/io/print_wav.py'
  • Do experiments with "major seventh chords," interpreted as squares generated by the ultipliers $3$ and $5$
  • Add Β§ to README about dissigs tools for evolving discrete signals along discrete tonnetze
  • Fix 0's in divisor grid images in README.md
  • Analyze FT and STFT of step-realization of discrete audio signals

1. Harmonic movement between discrete signals

Tonnetze for continuous signals

Euler's tonnetz

A tonnetz (German for "tone network," with plural tonnetze) is a type of diagram that depicts the intervalic inte-relationship between a collection of pitches, pitch classes, or even chords. One of the first known examples of a tonnetz is a drawing that the mathematician Leonard Euler included in a 1739 treatise on music theory.

tonnetzB

Euler's tonnetz

Today, we tend to depict this same tonnetze as a grid network. This tonnetz depicts the relationship between the 12 pitch classes in a 12-tone equaltempered tuning when we move along the two important diatonic intervals P5 (a perfect fifth) and M3 (m major third):

tonnetzB

A modernized version of Euler's tonnetz.

Here, we depict the tonnetz as a grid network, instead of Euler's original system of cascading brackets, but the content is essentially identical. We've added dotted arrows along the bottom edges of the diagram to indicate how it loops back along itself along P5 and M3 intervals.

The diagram is interesting from a music theoretical perspecitive because to exhibits lots of important diatonic-based musical phenomena in striking and often quite suggestive geometric patterns. To give just one example, all major and minor triads appear in this tonnetz as span or cospan diagrams:

tonnetzB

Major and minor triads appear in our tonnetz as wedges (∧) and vees (∨), respectively

Modern tonnetze

Tonnetze became an important tool to developments in (musical) set theory and in neo-Riemann theory. For exemplary use of tonnetz in musical analysis, see:

  • Dmitri Tymoczko. A Geometry of Music: Harmony and Counterpoint in the Extended Common Practice. Oxford Studies in Music Theory. Oxford University Press, March 2011. 480 pages.
  • Richard Cohn. Audacious Euphony: Chromatic Harmony and the Triad’s Second Nature. Oxford Studies in Music Theory. Oxford University Press, January 2012. 256 pages.
  • Edward Gollin and Alexander Rehding, editors. The Oxford Handbook of Neo-Riemannian Music Theories. Oxford Handbooks. Oxford University Press, May 2014. 632 pages.

The general pattern in all of this work is a partial import, into music theory, of category theoretical diagrams coming from representation theory. Musical intervals, harmonic movement, and larger tonal structures all emerge from the action of the multiplicative monoid of integers $\mathbb{Z}$ on representations of the circle group $\mathbb{S}^{1}$.

Neo-Riemannian theory generalizies tonnetze so that they model transformations between not just pitches, but between chords and more general musical datastructures. This use of tonnetze abstracts away from common practice tonal function and voice-leading, and some reject this development as overly formal and historically detached. But much of this suspicion stems from a misunderstanding of the depth of insight these tools offer. Far from being mere abstractions, tonnetze reveal profound geometries underlying harmonic motion β€” geometries that remain relevant even beyond traditional tonal music.

Embracing this perspective, we explore the tonnetz not as a historical artifact or static diagram, but as a dynamic analytic and generative tool for navigating the musical content of signals themselves.

Tonality for discrete audio signals

Musical intervals for discrete audio signals

We can understand the edges in our modernized version of Euler's tonnetz as multiplication operations. Indeed, moving up a perfect fifth corresponds to rescaling playback speed of a continuous audio signal $f(t)$ by a factor of 3/2, i.e., $f(t)\mapsto f(3t/2)$. Likewise, moving up a major third corresponds to rescaling the playback speed of our continuous audio signal by a factor of 5/4, i.e., $f(t)\mapsto f(5t/4)$.

If we impose octave equivalence, then we ignore all factors of 2 when we rescale playback speed. Up to octave equivalence, movement up a perfect fifth amounts to rescaling the playback speed by any factor of 3, and movement up a major third amounts to rescaling the playback speed by a factor of 5, i.e., $f(t)\mapsto f(3t)$ and $f(t)\mapsto f(5t)$, respectively.

In this way, tonnetz-centric musical analysis can be derived from a theory of rescaling the playback speed of continous, periodic audio signals by integer factors:

tonnetzB

Moving a continuous periodic audio signal f(t) up two octaves via f(t) ↦ f(2t) ↦ f(4t)

Not all audio signals are continuous though. Discrete audio signals have played and continue to play an important role in music production and in signal processing.

For a discrete periodic audio signal, that is, for a periodic audio signal $s(i)$ that samples time at regular discrete intervals, so $i=0,1,2,\dots,\ell-1$ for some positive integer $\ell$, the notion of "rescaling times" doesn't quite make sense. We can still multiply the sample index $i$ by any integer $m$ to obtain a new signal: $s(i)\mapsto s(mi)$. However, the manner in which these sample indices $i$ transform under multiplication by an integer follows the rules of modular arithmetic, not the rules of continuous dilation:

tonnetzB

Moving a discrete periodic audio signal s(i) "up two octaves" via s(i) ↦ s(2i) ↦ s(4i)

These modular, multiplicative transformations $s(i)\mapsto s(mi)$ of sample-index $i$ for discrete audio signals $s(i)$ become natural candidates for the musical intervals possible between discrete audio signals.

If we take this proposal seriously, we arrive at the following:

  • Question: How is movement along all these "discrete musical intervals" interelated?

Tonnetze for discrete audio signals

If you've played with 8-bit tones, you may already have a sense that for discrete periodic audio signals, movement along musical intervals doesn't work in exactly the same way as it does for continous periodic audio signals. Discrete audio signals have complex timbres that seem to have mysterious relationships to one another.

A lot of this mystery can be calrified by modeling the signals as vertices in a directed graph that we call a discrete tonnetz, which depicts the multiplicative action of integers, modulo our sample count, as a categorical diagram. The set of nodes in this discrete tonnetz graph is the set of integers modulo our sample count, and each fixed integer $m$ induces a family of edges $$n\xrightarrow{m}mn\ (\text{mod}\ \ell)$$ in the discrete tonnetz graph.

The discrete tonnetz captures how discrete spectral energy shifts under modular scaling, and reveals surprising orbit structures tied to the arithmetic of the modulus.

>>> from dissig.tonnetze.networks import Tonnetz # dissig Tonnetz class

We instantiate the discrete tonnetz for discrete signals with sample count $=36$, with edges corresponding to integers $2$, $3$, $5$, and $7$:

>>> modulus = 36 # Sample count for our discrete audio signals
>>> integer_list = [2, 3, 5, 7] # Integers to induce edges in tonnetz
>>> tonnetz = Tonnetz(modulus, integer_list) # Instantiate Tonnetz instance

We can recover the graph underlying the tonnetz as a networkx.DiGraph instance via the Tonnetz.network attribute:

>>> print(tonnetz.network)
DiGraph with 36 nodes and 127 edges

That said, if you pass this graph naively into a graph visualization library like networkx.drawing, the result is nearly unreadable β€” cluttered, asymmetric, and blind to the underlying modular symmetries that actually organize the space. For a quick example, we can import dissig's discrete tonnetz class Tonnetz:

tonnetzB

Tonnetz for discrete audio signals with 36 samples

Large-scale structure of discrete tonnetze

To better display discrete tonnetze, we need to use results from early number theory about the structure of the ring $\mathbb{Z}/\ell\mathbb{Z}$.

Orbits under the unit group from natural clusters

The subset $(\mathbb{Z}/\ell\mathbb{Z})^{\times}$ of elements in $\mathbb{Z}/\ell\mathbb{Z}$ that are invertible under multiplication from a group called the group of units or the unit group. For any element $n\in\mathbb{Z}/\ell\mathbb{Z}$, we can consider its $(\mathbb{Z}/\ell\mathbb{Z})^{\times}$-orbit $(\mathbb{Z}/\ell\mathbb{Z})^{\times}\cdot n={mn\ (\text{mod}\ \ell):m\in (\mathbb{Z}/\ell\mathbb{Z})^{\times}}$.

These orbits form natural clusters in any discrete tonnetz. In fact, if we add all arrows coming from $(\mathbb{Z}/\ell\mathbb{Z})^{\times}$, each of these clusters forms a bi-directed clique. and we can use them to better organize our visualation.

In dissig, we provide a cluster-based visualization of any discrete tonnetz via the mode='dot' keyword argument of the nx_viz function. To provide an example, let us use our same discrete tonnetz for sample count $36$ and edges induced by $2$, $3$, and $5$:

>>> from dissig.tonnetze.networks import Tonnetz
...
>>> modulus = 36
>>> integer_list = [2, 3, 5]
>>> tonnetz = Tonnetz(modulus, integer_list)

But now, let us visulaize the tonnetz using nx_viz in 'dot' mode:

>>> from dissig.tonnetze.visualizers import nx_viz
...
>>> nx_viz(tonnetz, "test_viz", mode='dot')

We get the following, better organized visualization of the same discrete tonnetz:

tonnetzB

Orbit cluster are arranged along divisor grid

Complementary to the group of units inside $\mathbb{Z}/\ell\mathbb{Z}$, there's a grid formed by the divisors of $\ell$. Specifically, if we let $$\ell\ =\ p_{1}^{e_{1}}p_{2}^{e_{2}}\cdots p_{k}^{e_{k}}$$ denote the prime factorization of our sample count $\ell$, then we get a grid inside our tonnetz whenever we include the edges correpsonding to the prime factor integers $p_1$, %p_2$, ..., $p_{k}$. This grid starts at the node $1$, and is swept out by powers of these prime factors $p_1$ through $p_k$.

Here are several examples. To start, here's the 1-dimensional grid formed by the divisors of $16$, with arrows corresponding to multiplication by the only prime factor, $2$:

tonnetzB

Grid of divisors inside the discrete tonnetz for sample count $16$

Next, here's the 2-dimensional grid formed by the divisors of $36$, with arrows corresponding to multiplication by the two prime factors, $2$ and $3$:

tonnetzB

Grid of divisors inside the discrete tonnetz for sample count $36$

And finally, here's the 3-dimensional grid formed by the divisors of $60$, with arrows corresponding to multiplication by the three prime factors, $2$, $3$, and $5$:

tonnetzB

Grid of divisors inside the discrete tonnetz for sample count $60$

In dissig, we also provide a visualization of any discrete tonnetz via the mode='neato' keyword argument of the nx_viz function:

>>> from dissig.tonnetze.networks import Tonnetz
>>> from dissig.tonnetze.visualizers import nx_viz

To provide an example, let us return to our discrete tonnetz for sample count $36$ and edges induced by $2$, $3$, and $5$. This time, we also include edges induced by $7$:

>>> modulus = 36
>>> integer_list = [2, 3, 5, 7]
>>> tonnetz = Tonnetz(modulus, integer_list)

We get rather well organized version of this tonnetz if we pass to the nx_viz function with keyword argument 'neato':

>>> nx_viz(tonnetz, "test_viz", mode='neato')

The resulting visualization does a fantastic job as clarifying the complimentary natrue of the grid of divisors (blue) and the unit group orbits (red):

tonnetzB

A even better organized version of the tonnetz for sample count 36

Notice how subtracting $1$ do the sample count leads to a substantially different discrete tonnetz:

>>> modulus = 35 # Change modulus to 35
>>> tonnetz = Tonnetz(modulus, integer_list) # Use previous integer_list
>>> nx_viz(tonnetz, "test_viz", mode='neato', appearance_theme='dark')

tonnetzB

A even better organized version of the tonnetz for sample count 35

One of the major advantages of thse better organized visualizations is that they let us draw quick conlusions about tonnetze for relatively large sample counts:

>>> modulus = 216 # Change modulus to 216 == 2**3 * 3**3
>>> integer_list = [2, 3, 5, 11, 13] # New integer_list
>>> tonnetz = Tonnetz(modulus, integer_list)
>>> nx_viz(tonnetz, "test_viz", mode='neato', appearance_theme='dark')

tonnetzQ

A even better organized version of the tonnetz for sample count 216

[...]

Installation and Setup

πŸš€ Installation

Using PDM:

pdm install

Or using pip (if necessary):

pip install -e .

πŸ”§ Usage

Example usage:

>>> from dissig.tonnetze.networks import Tonnetz
>>> from dissig.tonnetze.visualizers import nx_viz
...
>>> modulus = 2**3 * 3**3
>>> integer_list = [2, 3, 5, 11, 13]
...
>>> tonnetz = Tonnetz(modulus, integer_list)
>>> nx_viz(tonnetz, "file_name")

[...]

>>> from dissig.signals.discrete import character_signal
>>> from dissig.tonnetze.networks import SignalTonnetz
>>> from dissig.io.print_wav import tonnetz_to_wav
...
>>> modulus = 2**2 * 3**2
>>> integer_list = [2, 3, 5]
...
>>> signal = character_signal(1, modulus)
>>> signal_tonnetz = SignalTonnetz(signal, integer_list)
>>> tonnetz_to_wav(signal_tonnetz, 440.0, 1.0)

πŸ§ͺ Running Tests

pdm run pytest

πŸ“„ Documentation

[...]

  • Images: docs/images/
  • LaTeX files: docs/tex/
  • External references: docs/external/

πŸ“¦ Project Structure

.
β”œβ”€β”€ docs  # Project documentation and resources
β”‚   β”œβ”€β”€ external
β”‚   β”œβ”€β”€ images
β”‚   └── tex
β”œβ”€β”€ results # Output files generated by the code
β”‚   β”œβ”€β”€ tonnetze_visuals
β”‚   └── wav_files
β”œβ”€β”€ src
β”‚   └── dissig  # Core project package
β”‚       β”œβ”€β”€ __init__.py
β”‚       β”œβ”€β”€ core.py  # High-level pipeline functions and orchestration
β”‚       β”œβ”€β”€ io  # Input/output utilities
β”‚       β”‚   β”œβ”€β”€ __init__.py
β”‚       β”‚   └── print_wav.py  # Functions for saving audio to WAV
β”‚       β”‚       β”œβ”€β”€ signal_to_wav()
β”‚       β”‚       └── tonnetz_to_wav()
β”‚       β”œβ”€β”€ signals # Signal representations and processing
β”‚       β”‚   β”œβ”€β”€ __init__.py
β”‚       β”‚   └── discrete.py # Discrete-time signal processing tools
β”‚       β”‚       β”œβ”€β”€ Signal  # Main Signal class
β”‚       β”‚       β”‚   β”œβ”€β”€ scale_time_by()
β”‚       β”‚       β”‚   β”œβ”€β”€ extract_real()
β”‚       β”‚       β”‚   β”œβ”€β”€ __len__()
β”‚       β”‚       β”‚   └── forward()
β”‚       β”‚       β”œβ”€β”€ character_signal() # Creates a character signal
β”‚       β”‚       └── signal_from_real() # Wraps real data as Signal
β”‚       β”œβ”€β”€ tonnetze  # Tonnetz network structures and visualizers
β”‚       β”‚   β”œβ”€β”€ __init__.py
β”‚       β”‚   β”œβ”€β”€ networks.py # Builds Tonnetz graph structures
β”‚       β”‚   β”‚   β”œβ”€β”€ Tonnetz # Main Tonnetz class
β”‚       β”‚   β”‚   β”‚   β”œβ”€β”€ generate_weighted_edges()
β”‚       β”‚   β”‚   β”‚   └── generate_network()
β”‚       β”‚   β”‚   └── SignalTonnetz # Tonnetz decorated with signals
β”‚       β”‚   β”‚       β”œβ”€β”€ generate_weighted_edges()
β”‚       β”‚   β”‚       β”œβ”€β”€ generate_network()
β”‚       β”‚   β”‚       └── propogate_signal()
β”‚       β”‚   └── visualizers.py  # Graph rendering functions
β”‚       β”‚       β”œβ”€β”€ nx_viz_cluster() # Clustered node visualization
β”‚       β”‚       └── nx_viz_neat() # Clean layout visualization
β”‚       └── utils # Math utility functions
β”‚           β”œβ”€β”€ __init__.py
β”‚           β”œβ”€β”€ arithmetic.py # Number theoretic functions
β”‚           β”‚   β”œβ”€β”€ all_divisors()
β”‚           β”‚   β”œβ”€β”€ multiplicative_units()
β”‚           β”‚   └── unit_clusters()
β”‚           └── primes.py # Prime-related functions
β”‚               β”œβ”€β”€ primes_below()
β”‚               β”œβ”€β”€ prime_divisors()
β”‚               └── prime_powers()
β”œβ”€β”€ tests # Unit tests for all modules
β”‚   β”œβ”€β”€ io
β”‚   β”œβ”€β”€ signals
β”‚   β”œβ”€β”€ tonnetze
β”‚   └── utils
β”œβ”€β”€ LICENSE # License file (MIT License)
β”œβ”€β”€ README.md # Top-level project overview and instructions
β”œβ”€β”€ pdm.lock  # PDM lock file for reproducible installs
β”œβ”€β”€ pyproject.toml  # Project configuration (dependencies, metadata)
β”œβ”€β”€ pytest.ini  # Pytest configuration file
└── requirements.txt  # Optional: basic dependency list for pip users

πŸ›  Development

Set up your development environment:

pdm install --dev

To enable import resolution in VSCode:

// .vscode/settings.json
{
  "python.analysis.extraPaths": ["./src"]
}

πŸ“ License

This project is licensed under the terms of the MIT License.

About

Package for exploring tonal gravity associated with just harmonic motion (modulo N) of discrete audio signals.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages