65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 合成器工具包 - 第 II 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (21投票s)

2007年7月16日

MIT

12分钟阅读

viewsIcon

120269

downloadIcon

1366

演示如何使用 C# 合成器工具包创建简单的合成器。

Screenshot - SimpleSynthesizer.png

目录

引言

这是我的 C# 合成器工具包系列文章的第二部分。 第一部分 概述了该工具包。 在这一部分,我们将采取更实际的方法,创建一个非常简单的合成器。 这个合成器每个音色只有一个振荡器。 振荡器能够合成三种波形之一:锯齿波、方波或三角波。 它能够向左或向右进行声像(panning)。 此外,合成器将使用工具包的合唱效果。 即使这个示例合成器架构简单,它也将帮助我们理解如何使用该工具包来创建自己的合成器。

SimpleOscillator 类

创建合成器的第一步是编写振荡器组件。 振荡器负责创建合成器的波形。 我们称之为 SimpleOscillator。 它将派生自 StereoSynthComponent 类,因为它的输出是立体的,并实现 IProgramableIBendable 接口。

枚举

创建一个公共枚举来表示组件的参数是有帮助的,但不是必需的。 这可以让组件的客户端知道它有哪些参数。 我们的 SimpleOscillator 类只有两个参数:声像位置和波形类型。 除了有一个表示参数的枚举外,我们还将添加一个表示波形类型的枚举。

public class SimpleOscillator : StereoSynthComponent, IProgramable, IBendable
{
    #region Enumerations

    public enum ParameterId
    {
        Panning,
        WaveformType
    }

    public enum WaveformType
    {
        Sawtooth,
        Square,
        Triangle
    }

    #endregion

    // Rest of class...
}

字段

接下来是 SimpleOscillator 的字段。 每个字段都附有解释其含义的注释

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...
}

构造函数

让我们看看 SimpleOscillator 的构造函数。 有两个

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...
}

两个构造函数之间的唯一区别是第二个构造函数有一个 name 参数。 有时为合成器组件命名(例如,“幅度包络”)很有帮助,但它是可选的;不是必需的。

请注意 SampleRate 对象。 我发现为表示采样率而创建专用的 SampleRate 类对设计工具包很有帮助。 合成器内的所有组件共享相同的采样率。 与其为每个组件提供一个 SampleRate 属性(在合成器采样率更改时需要更新),不如让所有组件共享同一个 SampleRate 对象。

当采样率更改时,合成器会更新其 SampleRate 对象的 SamplesPerSecond 属性为新的采样率值。 该对象进而引发 SampleRateChanged 事件。 需要在采样率更改时得到通知的单个组件会注册此事件,并在事件触发时进行必要的内部值更新。 我们的 SimpleOscillator 类不需要此通知,因此它只需将 SampleRate 对象传递给基类而不注册事件。

SimpleOscillator 具有立体声输出,因此它使用传递给其构造函数的 StereoBuffer 对象。 在合成其输出时,它会将结果写入其 StereoBufferStereoBuffer 是一个轻量级包装器,围绕着一个多维数组(左声道和右声道两个维度)。 MonoBuffer 由单声道输出的合成器组件使用。 它是围绕一个一维数组的轻量级包装器。 StereoBufferMonoBuffer 类都使缓冲区管理更加容易。 与采样率值一样,缓冲区大小值由合成器内的所有组件共享。 缓冲区类使更新缓冲区大小更加容易,因为每个 Voice 都会跟踪其组件使用的所有缓冲区。 当缓冲区大小更改时,Voice 可以更改所有正在使用的缓冲区的大小,而无需组件担心。

两个构造函数都调用 Initialize 方法。 此方法包含从构造函数中提取出来的初始化代码;它只是一个辅助方法。 它将 currentNote 字段初始化为表示 A 440Hz (69) 的 MIDI 音符编号。 此时,SimpleOscillator 处于有效状态,可以使用了。

重写的方法和属性

SimpleOscillator 类派生自抽象的 StereoSynthComponent 类。 该类又派生自 SynthComponent 类。 SynthComponent 类有许多方法和属性我们必须重写。

  • 方法
    • Synthesize(合成)
    • 触发器
    • Release
  • 属性
    • SynthesizeReplaceEnabled(合成替换启用)
    • Ordinal

Synthesize 方法是任何合成器组件的核心。 在此,组件合成其输出。 Synthesize 方法接受两个整数参数:offsetcountoffset 参数是缓冲区中的零基索引偏移值。 它指示组件应在何处开始在其缓冲区中合成输出。 count 值指示应合成多少个样本。 SimpleOscillatorSynthesize 方法有些复杂,坦白说,并不太好看。 因此,为了简洁起见,我将在此省略显示。 请参阅下载中包含的 SimpleSynthDemo 项目以获取更多详细信息。

SimpleOscillatorTriggerRelease 方法如下所示

public override void Trigger(int previousNote, int note, int velocity)
{
    currentNote = note;

    playing = true;
}

public override void Release(int velocity)
{
    playing = false;
}

Synthesizer 响应 MIDI note-on 事件触发 Voice 时,会调用 Trigger 方法。 Voice 进而触发其组件。 在这里,SimpleOscillator 会跟踪正在播放的音符。 此外,它会设置一个标志,指示它当前正在“播放”。 它会忽略 previousNotevelocity 参数。

previousNote 参数指示之前正在播放的音符。 这对某些组件可能很有用。 例如,负责滑音的组件需要知道之前正在播放的音符,以便它可以从前一个音符扫到当前音符。 velocity 参数指示当前音符播放的速度。 能够响应速度的组件将使用此值来调整其输出。

这是 SimpleOscillator 重写的属性

public override bool SynthesizeReplaceEnabled
{
    get
    {
        return synthesizeReplaceEnabled;
    }
    set
    {
        synthesizeReplaceEnabled = value;
    }
}

public override int Ordinal
{
    get
    {
        return 1;
    }
}

SynthesizeReplaceEnabled 属性指示一个组件在每次合成其输出时是否会覆盖其缓冲区中的值。 对于某些组件,缓冲区不被覆盖很有用。 例如,您可以让多个组件共享同一个缓冲区。 如果它们将输出 ADD 到缓冲区而不是覆盖缓冲区,则每个组件的输出会与其他组件混合在一起。 这有助于提高效率。

Ordinal 值表示组件应排序的顺序。 如第一部分所述,每个 Voice 都会跟踪其组件,并根据它们的 Ordinal 值进行排序。 本质上,组件被组织成一个定向无环图。 组件的 Ordinal 值是其所有输入的 Ordinal 值之和加 1。 由于 SimpleOscillator 组件没有任何输入,因此其 Ordinal 值为 1。

IProgramable 接口

IProgramable 接口表示获取和设置参数值的功能。 通常,合成器组件实现此 interface 以允许操作其参数。 该 interface 如下所示

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;
    }
}

每个方法的 index 参数是对象参数的零基索引。 GetParameterName 方法顾名思义,获取参数的名称。 GetParameterLabel 获取一个字符串,表示参数应如何标记。 想象一下硬件合成器上的旋钮或开关标签。

例如,SimpleOscillator 声像参数的标签是“Left/Right”。 GetParameterDisplay 方法获取参数值的文本表示。 所有参数的值范围都在 [0, 1] 之间。 然而,这个值可能不是最直观的用户显示。 为参数提供更易理解的文本表示可能很有用。 例如,SimpleOscillator 的声像参数显示为 [-1, 1] 的范围,其中 0 为中心,-1 为最左,1 为最右。

GetParameterValueSetParameterValue 方法分别获取和设置参数的值。 需要记住的重要一点是,如上所述,所有参数的范围都是 [0, 1]。 ParameterCount 属性获取实现类提供的参数数量。 对于使用 VST 编程的人来说,这一切可能看起来很熟悉。 IProgramable 接口是基于 VST 处理参数的方法。

让我们看一下 SimpleOscillatorGetParameterName 方法的实现

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;
    }
}

首先,该方法确保 index 在范围内。 其次,它检索 SimpleOscillator 的名称。 如果未为组件命名,则名称可能为空。 如果名称不为空(或 null),则在名称后附加一个空格。 最后,该方法将 index 强制转换为我们创建的用于表示参数的 ParameterId 枚举类型,并根据该值进行切换。

合成器组件的名称会预置到每个参数的名称之前。 这有助于区分属于同一合成器组件不同实例的参数。 例如,假设您有两个 ADSR 包络对象,一个用于调制合成器的幅度,另一个用于调制其滤波器。 这些包络分别命名为“Amplitude Envelope”和“Filter Envelope”。 这两个包络都有一个攻击时间参数。 但是,幅度包络的攻击时间参数将命名为“Amplitude Envelope Attack Time”,而滤波器包络的攻击时间参数将命名为“Filter Envelope Attack Time”。

接下来是 GetParameterLabel 方法

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;
}

此方法仅返回一个文本表示,说明参数应如何标记。 同样,想象一下硬件合成器上常见的旋钮和开关标签。

接下来是 GetParameterDisplay 方法

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;
}

注意 panning 参数以及如何将其转换为 string。 所有参数的范围都是 [0, 1],但使用实际值显示 panning 参数不太有用。 因此,在此将其转换为 [-1, 1] 范围内的中间值。 然后将此值转换为 string

最后,我们有了用于获取和设置参数值的 GetParameterValueSetParameterValue 方法

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;
    }
}

请注意波形类型参数正在进行的转换。 在 GetParameterValue 方法中,它被转换为 [0, 1] 范围内的值(如工具包所要求)。 在 SetParameterValue 方法中,它被转换为表示波形类型的枚举。

IBendable 接口

IBendable 接口表示响应音高弯音轮生成的调制的函数。 Synthesizer 接收音高弯音消息,并根据其音高弯音范围设置将其转换为调制值。 这些值将被传递给所有实现 IBendable 接口的对象。

这是 IBendable 接口;它非常简单

public interface IBendable
{
    double PitchBendModulation
    {
        get;
        set;
    }
}

SimpleOscillator 通过简单地跟踪音高弯音调制来实现此接口。 它稍后将在其 Synthesize 方法中使用此值来调制其音高。

#region IBendable Members

public double PitchBendModulation
{
    get
    {
        return pitchBendModulation;
    }
    set
    {
        pitchBendModulation = value;
    }
}

#endregion

总结 SimpleOscillator 类

在离开 SimpleOscillator 类之前,我们将为其添加一个额外的属性,指示它当前是否 playing

public bool IsPlaying
{
    get
    {
       return playing;
    }
}

如您所见,编写此类花费了大量精力。 幸运的是,最难的部分已经结束。 使用该工具包时,您的大部分工作将是编写合成器的组件。

SimpleVoice 类

下一步是创建一个派生自 Voice 类的类。 它将表示我们合成器中的单个音色。 派生自 Voice 的类会协调一组协同工作的合成器组件来合成音色的输出。 幸运的是,对于这个例子,我们的音色类比振荡器类更容易实现。

关于 Voice 基类有一个重要的说明。 它本身就是一个合成器组件,并且派生自 StereoSynthComponent 类,该类又派生自 SynthComponent 类。 Voice 类重写了 SynthComponent 类中的一些方法和属性,而将其他方法留给派生类重写。

这是 SimpleVoice 类的实现

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
}

查看 Initialize 辅助方法。 它会创建一个 SimpleOscillator 类的实例,并将音色的 StereoBuffer 传递给它。 SimpleOscillator 将把其输出写入此缓冲区。 然后它会调用 AddComponent 方法。 此方法属于 Voice 基类。 派生类调用此方法将组件添加到 Voice 的组件集合中。 此集合根据每个组件的 Ordinal 值进行排序。 当调用 VoiceSynthesize 方法时,它会遍历其组件集合,对每个组件调用 Synthesize。 这确保组件按正确的顺序合成其输出。

例如,假设一个 LFO 组件正在调制振荡器的频率。 振荡器组件的 Ordinal 值将高于 LFO,因此它将出现在 Voice 的组件集合中的 LFO 之后。 换句话说,LFO 的 Synthesize 方法将在振荡器之前被调用,以确保 LFO 的输出已准备好供振荡器使用。

AddParameters 方法会接收 SimpleOscillator 对象。 这使得 Voice 基类能够自动化参数处理。 您只需添加实现 IProgramable 接口的所有对象,而无需自己协调。 Voice 基类以及 Synthesizer 类会处理其余的。 此外,SimpleOscillator 对象会被传递给 AddBendable 方法。 每当音高弯音轮移动时,其位置值都会被转换并传递给所有 IBendable 对象。

SynthHostForm 类

我们的合成器快完成了。 剩下的唯一一件事就是从 SynthHostForm 类派生一个类。 当应用程序实例化此类的实例时,它将提供一个运行我们 Synthesizer 的环境。 在此类中,我们只会重写几个方法和一个属性

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;
        }
    }
}

将创建 Synthesizer 对象以使用我们的 SimpleVoice 类作为其音色。 这是通过将 Synthesizer 传递一个返回 Voice 对象的委托来完成的。 换句话说,创建我们自己的 Synthesizer 所要做的就是传递一个创建我们编写的自定义音色的委托。 此外,我们还传递第二个委托来创建效果集合。 这会在合成器中创建效果链。

我们不会为这个合成器创建编辑器,因此 HasEditor 属性返回 false,而 CreateEditor 方法在被调用时会抛出 NotSupportedException

结论

在本部分中,我们创建了一个简单的合成器,并在过程中对该工具包有了更深入的了解。 在第三部分中,我们将创建一个更复杂的合成器,它具有我们与传统减法合成器相关联的许多功能。 但现在,我希望您喜欢到目前为止的旅程。 我期待听到您的评论和建议。 感谢您的时间。

历史

  • 2007 年 7 月 16 日 - 第一个版本
  • 2007 年 8 月 16 日 - 第二个版本
© . All rights reserved.