声音合成:使用 AS3 在频率之间插值

Posted

技术标签:

【中文标题】声音合成:使用 AS3 在频率之间插值【英文标题】:Sound synthesis: interpolate betweeen frequencies using AS3 【发布时间】:2016-10-26 00:03:49 【问题描述】:

我有点迷茫,希望有人能对此有所了解。 出于好奇,我正在开发一个简单的软合成器/音序器。一些想法 取自家用电脑黄金时代流行的 .mod 格式。 目前它只是一个模型。从数组中读出注释 最多 64 个值,其中数组中的每个位置对应于十六分之一 笔记。到目前为止一切顺利,一切正常,旋律响起 正好。如果从一个音符过渡到另一个音符,就会出现问题。 例如f4 -> g#4。由于这是一个突然的变化,因此会出现明显的弹出/点击 声音。为了补偿我试图在不同频率之间进行插值 并开始编写一个简单的示例来说明我的想法并验证它 工作。

import flash.display.Sprite;
import flash.events.Event;
import flash.display.Bitmap;
import flash.display.BitmapData;

public class Main extends Sprite

    private var sampleRate:int = 44100;
    private var oldFreq:Number = 349.1941058508811;
    private var newFreq:Number = 349.1941058508811;
    private var volume:Number = 15;
    private var position:int = 0;
    private var bmp:Bitmap = new Bitmap();
    private var bmpData:BitmapData = new BitmapData(400, 100, false, 0x000000);
    private var col:uint = 0xff0000;

    public function Main():void
    
        if (stage)
            init();
        else
            addEventListener(Event.ADDED_TO_STAGE, init);
    

    private function init(e:Event = null):void
    
        removeEventListener(Event.ADDED_TO_STAGE, init);
        bmp.bitmapData = bmpData;
        addChild(bmp);

        for (var a:int = 0; a < 280; a++)
        
            if (a == 140)
            
                col = 0x00ff00;
                newFreq = 415.26411519488113;
            
            if (a == 180)
            
                col = 0x0000ff;
            
            oldFreq = oldFreq * 0.9 + newFreq * 0.1;
            bmpData.setPixel(position, Math.sin((position) * Math.PI * 2 / sampleRate * oldFreq * 2) * volume + bmpData.height/2, col);
            position++;
        
    

这将生成以下输出:

蓝点代表 349.1941058508811 赫兹的正弦波,红色代表 415.26411519488113 赫兹,绿点代表插值。 对我来说,看起来这应该可行! 但是,如果我将这种技术应用于我的项目,结果就不一样了! 事实上,如果我将输出渲染到一个波形文件,这些之间的转换 两个频率看起来像这样:

显然它使弹出更糟。什么可能是错的? 这是我的(缩短的)代码:

import flash.display.*;
import flash.events.Event;
import flash.events.*;
import flash.utils.ByteArray;
import flash.media.*;
import flash.utils.getTimer;

public class Main extends Sprite

    private var sampleRate:int = 44100;
    private var bufferSize:int = 8192;
    private var bpm:int = 125;
    private var numberOfRows:int = 64;
    private var currentRow:int = 0;
    private var quarterNoteLength:Number;
    private var sixteenthNoteLength:Number;
    private var numOctaves:int = 8;
    private var patterns:Array = new Array();
    private var currentPattern:int;
    private var songOrder:Array = new Array();
    private var notes:Array = new Array("c-", "c#", "d-", "d#", "e-", "f-", "f#", "g-", "g#", "a-", "a#", "b-");
    private var frequencies:Array = new Array();
    private var samplePosition:Number = 0;
    private var position:int = 0;
    private var channel1:Object = new Object();

    public function Main():void
    
        if (stage)
            init();
        else
            addEventListener(Event.ADDED_TO_STAGE, init);
    

    private function init(e:Event = null):void
    
        removeEventListener(Event.ADDED_TO_STAGE, init);
        quarterNoteLength = sampleRate * 60 / bpm;
        sixteenthNoteLength = quarterNoteLength / 2 / 2;
        for (var a:int = 0; a < numOctaves; a++)
        
            for (var b:int = 0; b < notes.length; b++)
            
                frequencies.push(new Array(notes[b % notes.length] + a, 16.35 * Math.pow(2, frequencies.length / 12)));
            
        
        patterns.push(new Array("f-4", "", "", "", "g#4", "", "", "f-4", "", "f-4", "a#4", "", "f-4", "", "d#4", "", "f-4", "", "", "", "c-5", "", "", "f-4", "", "f-4", "c#5", "", "c-5", "", "g#4", "", "f-4", "", "c-5", "", "f-5", "", "f-4", "d#4", "", "d#4", "c-4", "", "g-4", "", "f-4", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""));
        songOrder = new Array(0, 0);
        currentRow = 0;
        currentPattern = 0;
        channel1.volume = .05;
        channel1.waveform = "sine";
        channel1.frequency = [0];
        channel1.oldFrequency = [0,0,0,0];
        channel1.noteTriggered = false;

        updateRow();
        var sound:Sound = new Sound();
        sound.addEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData);
        sound.play();
    

    private function updateRow():void
    
        var tempNote:String = patterns[songOrder[currentPattern]][currentRow];
        if (tempNote != "")
        
            channel1.frequency = new Array();
            if (tempNote.indexOf("|") == -1)
            
                channel1.frequency.push(findFrequency(tempNote));
            

            channel1.noteTriggered = true;
        

    

    private function onSampleData(event:SampleDataEvent):void
    
        var sampleData:Number;
        for (var i:int = 0; i < bufferSize; i++)
        
            if (++samplePosition == sixteenthNoteLength)
            
                if (++currentRow == numberOfRows)
                
                    currentRow = 0;
                    if (++currentPattern == songOrder.length)
                    
                        currentPattern = 0;
                    
                
                updateRow();
                samplePosition = 0;
            


            for (var a:int = 0; a < (channel1.frequency).length; a++ )
            
                channel1.oldFrequency[a] = channel1.oldFrequency[a]*0.9+channel1.frequency[a]*0.1;          
            

            if ((channel1.frequency).length == 1)
            
                sampleData = generate(channel1.waveform, position, channel1.oldFrequency[0], channel1.volume);
            
            else
            
                sampleData = generate(channel1.waveform, position, channel1.oldFrequency[0], channel1.volume);
                sampleData += generate(channel1.waveform, position, channel1.oldFrequency[1], channel1.volume);
            

            event.data.writeFloat(sampleData);
            event.data.writeFloat(sampleData);

            position++;
        
    

    private function generate(waveForm:String, pos:Number, frequency:Number, volume:Number):Number
    
        var retVal:Number
        switch (waveForm)
        
            case "square": 
                retVal = Math.sin((pos) * 2 * Math.PI / sampleRate * frequency) > 0 ? volume : -volume;
                break;
            case "sine": 
                retVal = Math.sin((pos) * Math.PI * 2 / sampleRate * frequency * 2) * volume;
                break;
            case "sawtooth": 
                retVal = (2 * (pos % (sampleRate / frequency)) / (sampleRate / frequency) - 1) * volume;
                break;
        
        return retVal;
    

    private function findFrequency(inpNote:String):Number
    
        var retVal:Number;
        for (var a:int = 0; a < frequencies.length; a++)
        
            if (frequencies[a][0] == inpNote)
            
                retVal = frequencies[a][1];
                break;
            
        
        return retVal;
    

谢谢! =)

【问题讨论】:

顺便说一句,如果您将 140 替换为 160,将 180 替换为 210,您的原始插值也存在不同相位的问题。 【参考方案1】:

您错过了当您切换频率时,generate 中的pos 值失去了不变性,也就是说,Math.sin((pos) * Math.PI * 2 / sampleRate * frequency * 2) 在以不同频率运行时会给出非常不同的值。相反,您应该使用“相位”变量,该变量将从 0 运行到 1,然后返回 0 并像锯齿图一样再次转发,并且将按(当前频率)*(1/采样率)的值转发。所以错误是您将两个generate() 结果添加到一个sampleData (因为干扰,您显然不能这样做)并且您使用一个position 作为时间值来计算相位而不是累积相位。检查这种方法,它应该会更好一点:

private function generate(waveForm:String, var phase:Number, frequency:Number, volume:Number):Number 
    // "pos" changed to "phase". This also means that "generate" should be called once per sample
    var retVal:Number;
    switch (waveForm)
    
        case "square": 
            retVal = Math.sin(phase * 2 * Math.PI) > 0 ? volume : -volume;
            break;
        case "sine": 
            retVal = Math.sin(phase * 2 * Math.PI ) * volume;
            break;
        case "sawtooth": 
            retVal = (2*Math.abs(2*phase-1)-1)* volume;
            break;
    
    phase+=frequency/sampleRate;// calculate new phase
    if (phase>1.0)  phase-=1.0;  // normalize phase to 0..1
    return retVal;

private function onSampleData(event:SampleDataEvent):void 
    var sampleData:Number;
    for(var i:int=0;i<bufferSize;i++) 
        if (++samplePosition == sixteenthNoteLength)
         // leaving this part as is, seems working
            if (++currentRow == numberOfRows)
            
                currentRow = 0;
                if (++currentPattern == songOrder.length)
                
                    currentPattern = 0;
                
            
            updateRow();
            samplePosition = 0;
        
        sampleData=0;
        for (i=0;i</*channels.length*/1;i++)   
            // TODO convert "channel1" to an array 
            // sampleData+=generate(channels[i].waveform, channels[i].phase, channels[i].frequency, channels[i].volume);
            sampleData+=generate(channel1.waveform, channel1.phase, channel1.frequency[0], channel1.volume);
        
        event.data.writeFloat(sampleData);
        event.data.writeFloat(sampleData);
    

事实上,您的通道应该转到一个单独的类,它将所有参数(相位、频率、波形、音量)放在一起,然后,当您需要它们进行采样时,您只需调用 channels[i].generateNextSample() 和无需所有麻烦的参数即可获得浮点数。另外,一个频道,一个频率,所以跳过那些“oldFrequency”的东西。

作为后续,Channel 类的草图:

public class Channel 
    public const WAVE_SINE:int=0;
    public const WAVE_SQUARE:int=1;
    public const WAVE_SAWTOOTH:int=2;
    private var phase:Number=0;
    private var currentVolume:Number=0;
    public var volume:Number; // 0 to 1, should build a setter to normalize
    public var frequency:Number=0;
    public var waveform:int; // should also not allow changing this mid-play probably
    public function Channel(v:Number=0,wf:int=WAVE_SINE,f:Number=0) 
        this.volume=v;
        this.frequency=f;
        this.waveform=wf;
        phase=0;
        currentVolume=0;
    
    public function generateNextSample():Number ... // use the generate() code above to fill
    public function reset():void  currentVolume=0; phase=0;  // POW
    // rest to taste, enabled, active, whatever

使用示例:

var ch:Vector.<Channel>=new Vector.<Channel>();
ch.push(new Channel());
function onSampleData(e:SampleDataEvent):void 
    for (var j:int=0;j<8192;j++) 
        // here to input code that can alter channels' freqs, volumes etc
        var sd:Number=0;
        for (var i:int=ch.length-1; i>=0;i--)  sd+=ch[i].generateNextSample(); 
        e.data.writeFloat(sd);
        e.data.writeFloat(sd);
    

【讨论】:

我认为这可能与阶段有关,但无法弄清楚如何解决它。无论如何,如果我将相位保持在不同频率之间,过渡看起来有点像这样:Goldwave 1 如果我在触发新音符时将其重置为 0,就像这样:Goldwave 2 我可以通过在新音符开始时在不同幅度之间进行插值,但也许有更好的方法。你有什么建议吗?感谢您提供有用的回答 Vesper! 你可以将当前的音量插值到改变的音量,这样可以消除“Goldwave 1”中的凹凸,只要插值超过几十个样本即可。这需要volume 上的设置器和将currentVolume 缓慢调整到generateNextSample() 中指定的音量的代码。

以上是关于声音合成:使用 AS3 在频率之间插值的主要内容,如果未能解决你的问题,请参考以下文章

合成器从一个频率滑到另一个频率

如何在 iPhone/Mac 上使用 CoreAudio 合成声音

合成器是一种啥乐器?求购合成器

水果编曲软件FL Studio使用教程——Vocodex插件

如何合成声音?

如何在 C 中进行声音合成?