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 by i
is dubious. When you go from i to i+1, the distance progressed is as follows:
distance = ((n+1) * instantFrequency[n+1]) - (n * instantFrequency[n])
This is larger than the desired delta value, which should equal the new instantFrequency
value, e.g.:
distance = ((n+1) * instantFrequency[n]) - (n * instantFrequency[n])
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.
public class CuriousSeries {
public static void main(String[] args) {
double aa = 1; // analogous to your 220
double bb = 2; // analogous to your 440
double delta = bb - aa;
int steps = 10;
double[] travelVals = new double[steps + 1];
// trip aa
for (int i = 0; i <= 10; i++) {
travelVals[i] = aa * i;
System.out.println("aa trip. travelVals[" + i + "] = " + travelVals[i]);
}
// trip ab
for (int i = 0; i <= 10; i++) {
double instantFreq = aa + (i / 10.0) * delta;
travelVals[i] = instantFreq * i;
System.out.println("ab trip. travelVals[" + i + "] = " + travelVals[i]);
}
// trip bb
for (int i = 0; i <= 10; i++) {
travelVals[i] = bb * i;
System.out.println("bb trip. travelVals[" + i + "] = " + travelVals[i]);
}
// trip cc
travelVals[0] = 0;
for (int i = 1; i <= 10; i++) {
double travelIncrement = aa + (i / 10.0) * delta;
travelVals[i] = travelVals[i-1] + travelIncrement;
System.out.println("cc trip. travelVals[" + i + "] = " + travelVals[i]);
}
}
}
Let's consider aa
as analogous to 220 Hz, and bb
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 by i
(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:
ab trip. travelSum[9] = 17.099999999999998
ab trip. travelSum[10] = 20.0
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 for instantFrequency
. 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:
double cursor = 0;
double prevCursor = 0;
double pitchIncrement = 2 * Math.PI * frequency / sampleRate;
for (int i = 0; i < n; i++) {
cursor = prevCursor + pitchIncrement;
audioVal[i] = Math.sin(cursor);
prevCursor = cursor;
}