1

I have a program that can take a waveform, such as a sine wave or sawtooth wave and detune it with another waveform that is slightly sharper or flatter. It takes the frequency of said waveform and adds or subtracts the amount of hertz to detune from it depending on which threads are activated. If I have a sine wave at 261.62Hz (middle C), and detune it to three threads - one sharp, one flat and one still at unison pitch (no change) - I end up with a detuned sine wave.

Now if I have a PCM audio sample (say a wave file of a trumpet with loop points) at middle C pitch with a play sample rate of 44100, how do I calculate the amount to add to or subtract from the play sample rate to detune it one hertz? How do I do this with other sample rates?

I need a formula that can detune the PCM sampled data at any sample rate to detune any possible threads up or down by one hertz.

Code for detuner

public class Detuner implements ISample {
// constants
public static final double DEF_DETUNING_VALUE = 0;
public static final boolean DEF_UNNISON_TUNING = true;
public static final boolean DEF_DETUNING = false;
public static final byte DEF_DETUNE_THREADS = 1;

// instance variables
private double celesteTuning;           // frequency to offset the tuning by
private boolean sharpTuning;            // turns on sharp tuning
private boolean flatTuning;             // turns on flat tuning
private boolean unisonTuning;           // turns on unison tuning
private double unisonToDetuningRatio;   // defines how loud the unison is to the detuning
private double detuningToProcess;       // use in certain scenarios
private ISample baseWaveform;           // the base waveform
private ISample[] sharpWaveforms;       // sharp
private ISample[] flatWaveforms;        // flat
private ISample unisonWaveGeneration;   // uninson
private byte detuneThreads;             // number of threads for each detune side
private double averageThreadDetuneAmount;   // the amount to add on each thread
private double unisonFrequency;
private AdsrEnvelope adsrEnvelope;

// constructor
public Detuner(double celesteTuning, boolean sharpTuning,
        boolean flatTuning, boolean unisonTuning,
        double unisonToDetuningRatio, ISample detuneWaveform,
        byte detuneThreads)
        throws CloneNotSupportedException {
    this.celesteTuning = celesteTuning;
    this.sharpTuning = sharpTuning;
    this.flatTuning = flatTuning;
    this.unisonTuning = unisonTuning;
    this.unisonToDetuningRatio = unisonToDetuningRatio;
    this.detuneThreads = detuneThreads;

    // if sharp, flat and unison tunings are used
    if (sharpTuning && flatTuning && !unisonTuning) {

        // detuningToProcess is celesteTuning / 2
        detuningToProcess = celesteTuning / 2;
    } // otherwise it is celesteTuning
    else {
        detuningToProcess = celesteTuning;
    }

    // set average thread detune and offset amount
    averageThreadDetuneAmount = detuningToProcess / detuneThreads;

    baseWaveform = (ISample) detuneWaveform;

    unisonFrequency = baseWaveform.getFrequency();

    // random offset to frequency
    double randomFrequencyOffset;

    // if unnison to detuning ratio is zero set it to division of all threads
    if (unisonToDetuningRatio == 0) {
        int totalThreads = detuneThreads;

        if (sharpTuning && flatTuning) {
            totalThreads *= 2;
        }

        // increment for unison thread
        totalThreads++;

        this.unisonToDetuningRatio = (double) 1 / (double) totalThreads;
    }

    if (sharpTuning) {

        // detuning holder
        double detuningHolder = averageThreadDetuneAmount;

        // initialise the array
        sharpWaveforms = new ISample[detuneThreads];

        // define averate amount to add to each thread
        for (int i = 0; i < sharpWaveforms.length; i++) {
            sharpWaveforms[i] = (ISample) baseWaveform.clone();
            if (i == 0) {
                randomFrequencyOffset = detuningToProcess;
            } else {
                randomFrequencyOffset = detuningHolder
                        + (Math.random() * averageThreadDetuneAmount
                        - averageThreadDetuneAmount / 2 / detuneThreads);
                detuningHolder += averageThreadDetuneAmount;
            }
            // if it is an audio sample up the play sample rate
            if (baseWaveform instanceof SamplePlayer) {
                // formula should go here and extract the sample rate of the sample player
            }
            sharpWaveforms[i].addToFrequency(randomFrequencyOffset);
            if (detuneThreads != 1) {
                sharpWaveforms[i].setOffset(Math.random());
            }
        }
    }

    if (flatTuning) {

        // detuning holder
        double detuningHolder = averageThreadDetuneAmount;

        // initialise the array
        flatWaveforms = new ISample[detuneThreads];

        // define averate amount to add to each thread
        for (int i = 0; i < flatWaveforms.length; i++) {
            flatWaveforms[i] = (ISample) baseWaveform.clone();
            if (i == 0) {
                randomFrequencyOffset = detuningToProcess;
            } else {
                randomFrequencyOffset = detuningHolder
                        + (Math.random() * averageThreadDetuneAmount
                        - averageThreadDetuneAmount / 2 / detuneThreads);
                detuningHolder += averageThreadDetuneAmount;
            }
            // if it is an audio sample up the play sample rate
            if (baseWaveform instanceof SamplePlayer) {
                // formula also goes here
            }
            flatWaveforms[i].subtractFromFrequency(randomFrequencyOffset);
            if (detuneThreads != 1) {
                flatWaveforms[i].setOffset(Math.random());
            }
        }
    }

    if (unisonTuning) {
        unisonWaveGeneration = (ISample) baseWaveform;
    }
}

```

2 Answers2

1

If you change the sampling rate, then all frequencies will scale proportionally by the same ratio of $\frac{f_{s2}}{f_{s1}}$ where $f_{s1}$ are the new and original sampling rates respectively.

To shift all frequencies by one Hz, you could take a Hilbert Transform of the waveform to get the Analytic Signal and then multiply the complex Analytic Signal by the complex exponential frequency $x[n]$ for a one Hz offset as given by:

$$x[n] = e^{j2 \pi f/f_s n} $$

Where $f$ is 1 Hz, $f_s$ is the sampling rate, and $n$ is the sample index. The real of this result would be the original waveform with all frequencies shifted by 1 Hz.

This approach is further outlined in this post.

The necessary Hilbert Transform can be easily implemented with the FFT as described in this post.

Dan Boschen
  • 50,942
  • 2
  • 57
  • 135
  • All I need is the sample rate for the signal tuned up or down one hertz, not the entire sample. The re sampler has already been coded and implemented. – Edward Eddy67716 Jul 14 '21 at 10:19
  • @EdwardEddy67716 To clarify your comment: It sounds like you are saying you want to change the sampling rate by 1 Hz, meaning 44100 to 44101 or 44099? Somehow I don't think that is really what you want (do you really have a system that will create a new sampling clock at a higher or lower frequency?) so I must be misunderstanding your statement. What do you mean by "entire sample"? Did you mean "entire signal"? – Dan Boschen Jul 14 '21 at 12:06
  • No, I want to detune the sample up or down by one hertz (or one beat per second) and I want to calculate the amount to add or subtract from the sampling rate to tune it up or down by one hertz. (changing the sample rate by one hertz does not do it properly) – Edward Eddy67716 Jul 14 '21 at 22:23
  • I think I am confused by “sample”- do you mean complete waveform and then with that do you mean you want every frequency in the waveform to shift by 1 Hz? – Dan Boschen Jul 14 '21 at 22:27
  • just update the sampling rate to one of the threads in order to make it have a one hertz beat. – Edward Eddy67716 Jul 14 '21 at 23:57
0

I found out that I need to calculate the wavelength of middle C in the sample. If the sample plays a Middle C of 261.63 hertz I can do this with the following formula. \begin{equation} w = p/f, \end{equation} Where w = wavelength of middle C in samples (261.63 in this example) p = play sample rate of middle C and f = middle c frequency (usually around 261.63 Hz)

Adding to or subtracting w from the play sample rate will detune it by 1Hz.

If I want to detune it by more or less than one hertz I can multiply w with the amount of hertz to detune the waveform by. \begin{equation} a = fw \end{equation} where f is detune frequency and a is detuning amount to add to/subtract from the play sample