I try to create a glissando (smooth pitch rise) from a start note to an end note (java code below). I linearly rise from the start note frequency to the stop note frequency like this
for (i = 0; i < b1.length; i++) {
instantFrequency = startFrequency + (i * deltaFreq / nrOfSamples);
b1[i] = (byte) (127 * Math.sin(2 * Math.PI * instantFrequency * i / sampleRate));
}
In the resulting audio fragment, the end of the glissando clearly has a higher pitch than the stop note. Is there something wrong with my math or is there an audiological reason why this rising sine seems to overshoot? Any ideas are greatly appreciated!
public static void main(String[] args) throws IOException {
int sampleRate = 44100;
int sampleSizeInBits = 8;
int nrOfChannels = 1;
byte[] sine220 = createTimedSine(220, sampleRate, 0.5);
byte[] gliss220to440 = createTimedGlissando(220, 440, sampleRate, 4);
byte[] sine440 = createTimedSine(440, sampleRate, 2);
byte[] fullWave = concatenate(sine220, gliss220to440, sine440);
AudioInputStream stream = new AudioInputStream(new ByteArrayInputStream(fullWave),
new AudioFormat(sampleRate, sampleSizeInBits, nrOfChannels, true, false), fullWave.length);
File fileOut = new File(path, filename);
Type wavType = AudioFileFormat.Type.WAVE;
try {
AudioSystem.write(stream, wavType, fileOut);
} catch (IOException e) {
System.out.println("Error writing output file '" + filename + "': " + e.getMessage());
}
}
public static byte[] createTimedSine(float frequency, int samplingRate, double duration) {
int nrOfSamples = (int) Math.round(duration * samplingRate);
return (createSampledSine(nrOfSamples, frequency, samplingRate));
}
public static byte[] createSampledSine(int nrOfSamples, float frequency, int sampleRate) {
byte[] b1 = new byte[nrOfSamples];
int i;
for (i = 0; i < b1.length; i++) {
b1[i] = (byte) (127 * Math.sin(2 * Math.PI * frequency * i / sampleRate));
}
System.out.println("Freq of sine: " + frequency);
return b1;
}
public static byte[] createTimedGlissando(float startFrequency, float stopFrequency, int samplingRate,
double duration) {
int nrOfSamples = (int) Math.round(duration * samplingRate);
return (createGlissando(nrOfSamples, startFrequency, stopFrequency, samplingRate));
}
public static byte[] createGlissando(int nrOfSamples, float startFrequency, float stopFrequency, int sampleRate) {
byte[] b1 = new byte[nrOfSamples];
float deltaFreq = (stopFrequency - startFrequency);
float instantFrequency = 0;
int i;
for (i = 0; i < b1.length; i++) {
instantFrequency = startFrequency + (i * deltaFreq / nrOfSamples);
b1[i] = (byte) (127 * Math.sin(2 * Math.PI * instantFrequency * i / sampleRate));
}
System.out.println("Start freq glissando :" + startFrequency);
System.out.println("Stop freq glissando :" + instantFrequency);
return b1;
}
static byte[] concatenate(byte[] a, byte[] b, byte[] c) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(a);
outputStream.write(b);
outputStream.write(c);
byte d[] = outputStream.toByteArray();
return d;
}
Console output:
Freq of sine: 220.0
Start freq glissando :220.0
Stop freq glissando :439.9975
Freq of sine: 440.0
The problem arises because the adjacent pitches for each frame are too wide. The calculation for
instantFrequency
is good, but arriving at a value by multiplying it byi
is dubious. When you go from i to i+1, the distance progressed is as follows:This is larger than the desired delta value, which should equal the new
instantFrequency
value, e.g.:The following code helped me figure out the problem, which had me puzzled for several hours. It was only after sleeping on it that I was able to get to the above succinct explanation (added in an edit).
Here is a simpler case that illustrates the issue. Since the problem occurs before the sin function calculations, I excluded them and all the operations that follow the trig calculation.
Let's consider
aa
as analogous to 220 Hz, andbb
as analogous to 440 Hz. In each section, we start at 0 and go to position 10. The amount we go forward is calculated similarly to your calculations. For the "fixed rate", we simply multiply the value of the step byi
(trips aa and bb). In trip ab I use a calculation similar to yours. The problem with it is that the last steps are too large. You can see this if you inspect the output lines:The distance traveled that "step" was close to 3, not the desired 2!
In the last example, trip cc, the calculation for
travelIncrement
is the same as forinstantFrequency
. But in this case the increment is simply added to the previous position.In fact, for purposes of audio synthesis (when creating wave forms computationally), it makes sense to use addition to minimize cpu cost. Along those lines, I usually do something more like the following, removing as many calculations from the inner loop as possible: