EDIT 2 + Answer
Turns out I needed to wrap between 0
and Frequency*2*Math.pi
.
Everyone who posted contributed to figuring out this issue. Since guest had the lowest reputation, I just marked his post as the answer. Thanks so much everyone, this was driving me crazy!
EDIT 1
Here's my WrapValue method, should have thought to post this before. It's not as sophisticated as Chris Taylor's, but it's having the same effect on my end.
public static double WrapValue(double value, double min, double max)
{
if (value > max)
return (value - max) + min;
if (value < min)
return max - (min - value);
return value;
}
This might be appropriate for Gamedev, but it's less game-y and more code-and-math-y so I put it here.
I'm trying to turn my Xbox into a digital instrument using the new XNA 4.0 DynamicSoundEffectInstance class, and I'm getting a click every second. I've determined this is caused by any attempt to wrap my domain value between 0 and 2*pi..
I wrote a little class called SineGenerator that just maintains a DynamicSoundEffectInstance and feeds it sample buffers generated with Math.Sin()
.
Since I want to be precise and use the 44.1 or 48k sampling rate, I'm keeping a double x
(the angle I'm feeding Math.Sin()
) and a double step
where step
is 2 * Math.PI / SAMPLING_FREQUENCY
. Every time I generate data for DynamicSoundEffectInstance.SubmitBuffer() I increment x
by step
and add sin(frequency * x)
to my sample buffer (truncated to a short
since XNA only supports 16 bit sample depth).
I figure I'd better wrap the angle between 0 and 2*pi so I don't loose precision for x
as it gets large. However, doing this introduces the click. I wrote my own double WrapValue(double val, double min, double max)
method in case MathHelper.WrapAngle() was being screwy. Neither wrapping between Math.PI and -Math.PI nor 0 and 2*Math.PI will get rid of the clicking. However, if I don't bother to wrap the value and just let it grow, the clicking disappears.
I'm thinking it has something to do with the accuracy of the .NET trig functions, how sin(0) != sin(2*pi), but I don't know enough to judge.
My question: Why is this happening, and should I even bother wrapping the angle?
The code:
using System;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework;
namespace TestDynAudio
{
class SineGenerator
{
// Sample rate and sample depth variables
private const int BUFFER_SAMPLE_CAPACITY = 1024;
private const int BIT_RATE = 16;
private const int SAMPLING_FREQUENCY = 48000;
private readonly int BYTES_PER_SAMPLE;
private readonly int SAMPLE_BUFFER_SIZE;
private DynamicSoundEffectInstance dynSound = new DynamicSoundEffectInstance(SAMPLING_FREQUENCY, AudioChannels.Mono);
private double x = 0; // The domain or angle value
private double step; // 48k / 2pi, increment x by this for each sample generated
private byte[] sampleData; // The sample buffer
private double volume = 1.0f; // Volume scale value
// Property for volume
public double Volume
{
get { return volume; }
set { if (value <= 1.0 && value >= 0.0) volume = value; }
}
// Property for frequency
public double Frequency { get; set; }
public SineGenerator()
{
Frequency = 440; // Default pitch set to A above middle C
step = Math.PI * 2 / SAMPLING_FREQUENCY;
BYTES_PER_SAMPLE = BIT_RATE / 8;
SAMPLE_BUFFER_SIZE = BUFFER_SAMPLE_CAPACITY * BYTES_PER_SAMPLE;
sampleData = new byte[SAMPLE_BUFFER_SIZE];
// Use the pull-method, DynamicSoundEffectInstance will
// raise an event when more samples are needed
dynSound.BufferNeeded += GenerateAudioData;
}
private void buildSampleData()
{
// Generate a sample with sin(frequency * domain),
// Convert the sample from a double to a short,
// Then write the bytes to the sample buffer
for (int i = 0; i < BUFFER_SAMPLE_CAPACITY; i++)
{
BitConverter.GetBytes((short)((Math.Sin(Frequency * x) * (double)short.MaxValue) * volume)).CopyTo(sampleData, i * 2);
// Simple value wrapper method that takes into account the
// different between the min/max and the passed value
x = MichaelMath.WrapValue(x + step, 0, 2f * (Single)Math.PI);
}
}
// Delegate for DynamicSoundInstance, generates samples then submits them
public void GenerateAudioData(Object sender, EventArgs args)
{
buildSampleData();
dynSound.SubmitBuffer(sampleData);
}
// Preloads a 3 sample buffers then plays the DynamicSoundInstance
public void play()
{
for (int i = 0; i < 3; i++)
{
buildSampleData();
dynSound.SubmitBuffer(sampleData);
}
dynSound.Play();
}
public void stop()
{
dynSound.Stop();
}
}
}
I am guessing that your Wrap function works fine, but you are not taking Sine(x) you are taking Sine(Frequency * x) - if Frequency*x doesn't not produce a round multiple of 2*PI then you get the pop. Try wrapping Frequency*x to get rid of the pop.