1
$\begingroup$

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;
        }
    }
```
$\endgroup$

2 Answers 2

1
$\begingroup$

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.

$\endgroup$
5
  • $\begingroup$ 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. $\endgroup$ Jul 14, 2021 at 10:19
  • $\begingroup$ @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"? $\endgroup$ Jul 14, 2021 at 12:06
  • $\begingroup$ 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) $\endgroup$ Jul 14, 2021 at 22:23
  • $\begingroup$ 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? $\endgroup$ Jul 14, 2021 at 22:27
  • $\begingroup$ just update the sampling rate to one of the threads in order to make it have a one hertz beat. $\endgroup$ Jul 14, 2021 at 23:57
0
$\begingroup$

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

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.