From Primes to Pitch: Turning Mertens Oscillations Into Sound

 



From Primes to Pitch: Turning Mertens Oscillations Into Sound

Previously, in this post, I explored the oscillatory behavior of the Mertens function and shared C++ code to compute its values. That was the analytical side of the story.

Now let’s take a creative turn: instead of just plotting the Mertens function, what if we could listen to it?

To make that possible, I wrote a small C++ class called MertensSonifier. Its job is simple but powerful:

  • 📥 Read values of M(n) either directly from memory or from a file (like the mertens.dat produced by the earlier code).
  • 🎚 Normalize those values into audible frequencies between 200 Hz and 2000 Hz, so each oscillation becomes a tone.
  • 🎶 Generate a WAV file by mapping each M(n) to a short sine wave segment, creating a sequence of sounds that reflect the function’s chaotic ups and downs.

In other words, the class transforms the abstract oscillations of number theory into something you can actually hear. Each prime, each fluctuation, becomes part of a strange melody — the hidden “music” of arithmetic.

This approach blends mathematics, signal processing, and creative coding. It’s not just about computation anymore; it’s about experiencing number theory through another sense.

#pragma once
#include <vector>
#include <algorithm>
#include <cmath>
#include <sndfile.h>
#include <stdexcept>

#include <string>
#include <fstream>
#include <sstream>
#include <iostream>

class MertensSonifier {
public:
    // Construct directly from a sequence
    MertensSonifier(const std::vector<int>& mertensSeq)
        : mertens(mertensSeq) {
        initMinMax();
    }

    // Construct from a file produced by save_mertens
    MertensSonifier(const std::string& filename) {
        loadFromFile(filename);
        initMinMax();
    }

    // Normalize M(n) to frequency range [200, 2000] Hz
    double normalizeToFreq(int val) const {
        double norm = (double)(val - minVal) / (maxVal - minVal);
        return 200.0 + norm * (2000.0 - 200.0);
    }

    // Generate WAV file with sine tones
    void writeWav(const char* filename) const {
        SF_INFO sfinfo;
        sfinfo.channels = 1;
        sfinfo.samplerate = 44100;
        sfinfo.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;

        SNDFILE* file = sf_open(filename, SFM_WRITE, &sfinfo);
        if (!file) {
            throw std::runtime_error("Failed to open WAV file for writing");
        }

        std::vector<short> samples;
        for (int val : mertens) {
            double freq = normalizeToFreq(val);
            int duration = sfinfo.samplerate / 250; // 1/250 sec per tone
            for (int i = 0; i < duration; i++) {
                double t = (double)i / sfinfo.samplerate;
                double sample = sin(2 * M_PI * freq * t);
                samples.push_back((short)(sample * 32767));
            }
        }

        sf_write_short(file, samples.data(), samples.size());
        sf_close(file);
    }

private:
    std::vector<int> mertens;
    int minVal, maxVal;

    void initMinMax() {
        minVal = *std::min_element(mertens.begin(), mertens.end());
        maxVal = *std::max_element(mertens.begin(), mertens.end());
    }

    void loadFromFile(const std::string& filename) {
        std::ifstream fin(filename);
        if (!fin) {
            throw std::runtime_error("Cannot open file " + filename);
        }
        mertens.clear();
        std::string line;
        while (std::getline(fin, line)) {
            std::istringstream iss(line);
            long long n;
            int M;
            if (iss >> n >> M) {
                mertens.push_back(M);
            }
        }
    }
};

Here’s a clear explanation of what my MertensSonifier class does, broken down piece by piece:

🧩 Purpose

The class takes values of the Mertens function  and turns them into sound. It maps each integer value to a frequency between 200 Hz and 2000 Hz, then generates a WAV file where each  becomes a short sine wave tone. In effect, it “sonifies” number theory.

📂 Structure

Includes

  • <vector>, <algorithm>, <cmath> → for storing sequences, finding min/max, and math functions.
  • <sndfile.h> → libsndfile library for writing WAV files.
  • <stdexcept> → for throwing exceptions.
  • <string>, <fstream>, <sstream>, <iostream> → for file I/O and parsing.

Public Interface

  1. Constructors
    • MertensSonifier(const std::vector<int>& mertensSeq) → Build directly from a sequence already in memory.
    • MertensSonifier(const std::string& filename) → Load values from a file (like mertens.dat created by your save_mertens function).
  2. normalizeToFreq(int val)
    • Maps a Mertens value into the frequency range [200, 2000] Hz.
    • Uses linear scaling: smallest value → 200 Hz, largest → 2000 Hz.
  3. writeWav(const char filename)*
    • Creates a WAV file with sine tones.
    • Each  becomes a short tone (duration = 1/250 sec).
    • Uses libsndfile to write samples in PCM 16‑bit format.
    • Throws an exception if the file can’t be opened.

Private Helpers

  • std::vector<int> mertens → stores the sequence of Mertens values.
  • int minVal, maxVal → track min/max for normalization.
  • initMinMax() → computes min/max from the sequence.
  • loadFromFile(const std::string& filename) → reads n M(n) pairs from a file, storing only the M(n) values.

🔊 Workflow

  1. Input: Either load from a file (mertens.dat) or pass a vector of values.
  2. Normalization: Scale values into audible frequencies.
  3. Sound Generation: For each value, generate a sine wave segment.
  4. Output: Write all samples into a WAV file.

Main application

#include <iostream>
#include "include/arithmetic.h"  
#include "include/MertensSonifier.h"
#include <fstream>  // for file output

using namespace std;
using namespace mobius;

// Function to save M(n) for n=1..x into a file
void save_mertens(mertens &calc, int64 x, const string &filename = "mertens.dat")
{
    ofstream fout(filename);
    if (!fout)
    {
        cerr << "Error: cannot open file " << filename << " for writing\n";
        return;
    }

    for (int64 n = 1; n <= x; ++n)
    {
        fout << n << " " << calc.mertens_M(n) << "\n";
    }

    fout.close();
    cout << "Mertens function values saved to " << filename << "\n";
}

int main()
{
    int64 x = 15000;

    cout << "M(" << x << ") computation starting...\n";

    mertens calc(x);

    // Save values to mertens.dat
    save_mertens(calc, x);

    try
    {
        // Load directly from the file you saved earlier
        MertensSonifier sonifier("mertens.dat");
        sonifier.writeWav("mertens.wav");
        std::cout << "WAV file generated from mertens.dat\n";
    }
    catch (const std::exception &e)
    {
        std::cerr << "Error: " << e.what() << "\n";
    }

    return 0;
}


arithmetic.h here


The important twist: 

The output isn’t just “music.” It’s a signal — which means you can analyze it with the same tools used in signal processing. Run it through an FFT, and you’ll see the spectral fingerprints of the Mertens function’s oscillations. Suddenly, analytic number theory becomes a dataset for spectral analysis.

And if you’re curious about the broader landscape of transforms, the mathematical engines that make such experiments possible, I explore them in my book The Essential Transform Toolkit. It’s a practical and intuitive guide to Fourier, Laplace, and beyond, designed for engineers, educators, and learners who want more than formulas on a page.

Comments