jeudi 23 juin 2016

Sound synthesis: interpolate betweeen frequencies using AS3

I'm a little lost and hopefully someone can shed some light on this. Out of curiosity I'm working on a simple softsynth/sequencer. Some ideas were taken from the .mod format popular in the golden era of home computers. At the moment it's just a mock-up. Notes are read out from an array holding up to 64 values, where each position in the array corresponds to a sixteenth note. So far so good, everything's working as it should and the melody plays just fine. The problem arises if there's a transition from one note to another. e.g. f4 -> g#4. Since this is an abrupt change there's a noticeable pop/click sound. To compensate I'm trying to interpolate between different frequencies and started to code a simple example to illustrate my idea and verify it's working.

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

This will generate the following output: interpolated

The blue dots represent a sine wave at 349.1941058508811 hz, the red 415.26411519488113 hz and the green dots the interpolation. For my eyes, it looks like this should work! If I apply this technique to my project however, the result isn't the same! In fact, if I render the output to a wave file, the transition between those two frequencies looks like this: Goldwave screenshot

Obviously it makes the popping even worse. What could possibly be wrong? Here's my (shortened )code:

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

Thanks! =)

Aucun commentaire:

Enregistrer un commentaire