|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
ContentsIntroductionThis is Part II in a series of articles describing my C# Synth Toolkit. Part I gave us an overview of the toolkit. In this part, we'll take a more hands-on approach and create a very simple synthesizer. This synthesizer will have a single oscillator per voice. The oscillator will be capable of synthesizing one of three waveforms: sawtooth, square or triangle. It will have the ability to be panned left or right. In addition, the synthesizer will use the toolkit's chorus effect. Even though this example synthesizer will have a simple architecture, it will help us understand how to use the toolkit to create our own synthesizers. SimpleOscillator ClassThe first step in creating our synthesizer is to write the oscillator component. Oscillators are responsible for creating a synthesizer's waveforms. We'll call it EnumerationsIt's helpful, but not required, to create a public enumeration representing the component's parameters. This lets clients of the component know what parameters it has. Our public class SimpleOscillator : StereoSynthComponent, IProgramable, IBendable
{
#region Enumerations
public enum ParameterId
{
Panning,
WaveformType
}
public enum WaveformType
{
Sawtooth,
Square,
Triangle
}
#endregion
// Rest of class...
}
FieldsNext comes public class SimpleOscillator : StereoSynthComponent, IProgramable, IBendable
{
// Enumerations...
#region Fields
#region Constants
// The number of waveforms.
public const int WaveformTypeCount = (int)WaveformType.Triangle + 1;
#endregion
// Determines the oscillator's position in the stereo field.
private double panning = 0.5;
// The type of waveform the SimpleOscillator is currently producing.
private WaveformType waveType = WaveformType.Sawtooth;
// The note that is currently playing.
private int currentNote;
// Phase accumulator.
private double accumulator = 0;
// The amount to modulate the pitch based on pitch wheel movement.
private double pitchBendModulation = 0;
// Indicates whether the SimpleOscillator is currently playing.
private bool playing = false;
// Indicates whether the SimpleOscillator will overwrite the
// values in its buffer each time it synthesizes output.
private bool synthesizeReplaceEnabled = true;
#endregion
// Rest of class...
}
ConstructorsLet's look at public class SimpleOscillator : StereoSynthComponent, IProgramable, IBendable
{
// Enumerations and fields...
#region Construction
public SimpleOscillator(SampleRate sampleRate, StereoBuffer buffer)
: base(sampleRate, buffer)
{
Initialize();
}
public SimpleOscillator(SampleRate sampleRate, StereoBuffer buffer,
string name)
: base(sampleRate, buffer, name)
{
Initialize();
}
private void Initialize()
{
currentNote = A440NoteNumber;
}
#endregion
// Rest of class...
}
The only difference between the two constructors is that the second one has a Notice the When the sample rate changes, the synthesizer updates its The Both constructors call the Overridden Methods and PropertiesThe
The The public override void Trigger(int previousNote, int note, int velocity)
{
currentNote = note;
playing = true;
}
public override void Release(int velocity)
{
playing = false;
}
The The Here are public override bool SynthesizeReplaceEnabled
{
get
{
return synthesizeReplaceEnabled;
}
set
{
synthesizeReplaceEnabled = value;
}
}
public override int Ordinal
{
get
{
return 1;
}
}
The The IProgramable InterfaceThe public interface IProgramable
{
string GetParameterName(int index);
string GetParameterLabel(int index);
string GetParameterDisplay(int index);
double GetParameterValue(int index);
void SetParameterValue(int index, double value);
int ParameterCount
{
get;
}
}
The For example, the label for the The Let's take a look at public string GetParameterName(int index)
{
#region Require
if(index < 0 || index >= ParameterCount)
{
throw new ArgumentOutOfRangeException("index");
}
#endregion
string result = string.Empty;
string name = Name;
if(!string.IsNullOrEmpty(name))
{
name = name + " ";
}
switch((ParameterId)index)
{
case ParameterId.Panning:
result = name + "Panning";
break;
case ParameterId.WaveformType:
result = name + "Waveform";
break;
default:
Debug.Fail("Unhandled parameter.");
break;
}
return result;
}
}
First, the method makes sure that The synth component's name is prepended to the name of each parameter. This helps distinguish parameters belonging to different instances of the same synth component. For example, say that you have two ADSR envelope objects, one for modulating the synth's amplitude and another for modulating its filter. The envelopes are given the names "Amplitude Envelope" and "Filter Envelope" respectively. Both envelopes will have an attack time parameter. However, the amplitude envelope's attack time parameter will have the name "Amplitude Envelope Attack Time" and the filter envelope's attack time parameter will have the name "Filter Envelope Attack Time." Next is the public string GetParameterLabel(int index)
{
#region Require
if(index < 0 || index >= ParameterCount)
{
throw new ArgumentOutOfRangeException("index");
}
#endregion
string result = string.Empty;
switch((ParameterId)index)
{
case ParameterId.Panning:
result = "Left/Right";
break;
case ParameterId.WaveformType:
result = "Type";
break;
default:
Debug.Fail("Unhandled parameter.");
break;
}
return result;
}
This method simply returns a text representation of how a parameter should be labeled. Again, it's helpful to think of knob and switch labels typically found on a hardware synthesizers. Next is the public string GetParameterDisplay(int index)
{
#region Require
if(index < 0 || index >= ParameterCount)
{
throw new ArgumentOutOfRangeException("index");
}
#endregion
string result = string.Empty;
switch((ParameterId)index)
{
case ParameterId.Panning:
{
double position = panning * 2 - 1;
result = position.ToString("F");
}
break;
case ParameterId.WaveformType:
result = waveType.ToString();
break;
default:
Debug.Fail("Unhandled parameter.");
break;
}
return result;
}
Note the Finally, we have the public double GetParameterValue(int index)
{
#region Require
if(index < 0 || index >= ParameterCount)
{
throw new ArgumentOutOfRangeException("index");
}
#endregion
double result = 0;
switch((ParameterId)index)
{
case ParameterId.Panning:
result = panning;
break;
case ParameterId.WaveformType:
result = (double)(int)waveType / (WaveformTypeCount - 1);
break;
default:
Debug.Fail("Unhandled parameter.");
break;
}
return result;
}
public void SetParameterValue(int index, double value)
{
#region Require
if(index < 0 || index >= ParameterCount)
{
throw new ArgumentOutOfRangeException("index");
}
else if(value < 0 || value > 1)
{
throw new ArgumentOutOfRangeException("value");
}
#endregion
switch((ParameterId)index)
{
case ParameterId.Panning:
panning = value;
break;
case ParameterId.WaveformType:
waveType = (WaveformType)(int)Math.Round(value *
(WaveformTypeCount - 1));
break;
default:
Debug.Fail("Unhandled parameter.");
break;
}
}
Notice the conversions taking place for the waveform type parameter. In the IBendable InterfaceThe Here is the public interface IBendable
{
double PitchBendModulation
{
get;
set;
}
}
The #region IBendable Members
public double PitchBendModulation
{
get
{
return pitchBendModulation;
}
set
{
pitchBendModulation = value;
}
}
#endregion
Wrapping Up the SimpleOscillator ClassBefore leaving the public bool IsPlaying
{
get
{
return playing;
}
}
As you can see, quite a bit of work went into writing this class. Fortunately, the hardest part is over. The majority of work you'll do when using the toolkit will be in writing the synthesizer's components. SimpleVoice ClassThe next step is to create a class that is derived from the An important note about the Here is the implementation of the public class SimpleVoice : Voice
{
#region SimpleVoice Members
#region Fields
// Used for generating the voice's waveform.
private SimpleOscillator oscillator;
#endregion
#region Construction
public SimpleVoice(SampleRate sampleRate, StereoBuffer buffer)
: base(sampleRate, buffer)
{
Initialize(buffer);
}
public SimpleVoice
(SampleRate sampleRate, StereoBuffer buffer, string name)
: base(sampleRate, buffer, name)
{
Initialize(buffer);
}
#endregion
#region Methods
private void Initialize(StereoBuffer buffer)
{
// Create oscillator passing it the sample rate used by the
// voice and the buffer given to the voice.
oscillator = new SimpleOscillator(SampleRate, buffer);
// Add components that will synthesize output.
AddComponent(oscillator);
// Add components that have parameters that will be adjusted.
AddParameters(oscillator);
// Add components that can respond to pitch bend modulation.
AddBendable(oscillator);
}
public override void ProcessControllerMessage
(ControllerType controllerType, double value)
{
// Nothing to do here as the voice doesn't respond to
// controller messages.
}
#endregion
#region Properties
protected override bool IsPlaying
{
get
{
return oscillator.IsPlaying;
}
}
public override bool SynthesizeReplaceEnabled
{
get
{
return oscillator.SynthesizeReplaceEnabled;
}
set
{
oscillator.SynthesizeReplaceEnabled = value;
}
}
#endregion
#endregion
}
Take a look at the For example, say that an LFO component is modulating the frequency of an oscillator. The oscillator component will have a higher The The SynthHostForm ClassWe are almost finished with our synthesizer. The only thing that remains is to derive a class from the public partial class Form1 : SynthHostForm
{
public Form1()
{
InitializeComponent();
}
protected override Synthesizer CreateSynthesizer
(int deviceId, int bufferSize, int sampleRate)
{
// Create a delegate for creating SimpleVoice objects.
VoiceFactory voiceFactory =
delegate(SampleRate sr, StereoBuffer buffer, string name)
{
return new SimpleVoice(sr, buffer, name);
};
// Create a delegate for creating effect objects.
EffectFactory effectFactory =
delegate(SampleRate sr, StereoBuffer buffer)
{
return new EffectComponent[] { new Chorus(sr, buffer) };
};
return new Synthesizer(
"Simple Synth",
deviceId,
bufferSize,
sampleRate,
voiceFactory,
8,
effectFactory);
}
protected override Form CreateEditor(Synthesizer synth)
{
throw new NotSupportedException();
}
protected override bool HasEditor
{
get
{
return false;
}
}
}
The We won't be creating an editor for this synthesizer, so the ConclusionIn this part, we've created a simple synthesizer and have become more familiar with the toolkit in the process. In Part III, we'll create a much more sophisticated synthesizer, one that has many of the bells and whistles we associate with traditional subtractive synthesizers. But for now I hope you've enjoyed the ride so far. I look forward to hearing your comments and suggestions. Thanks for your time. History
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||