声音合成:使用 AS3 在频率之间进行插值
Sound synthesis: interpolate betweeen frequencies using AS3
我有点迷茫,希望有人能对此有所启发。
出于好奇,我正在研究一个简单的 softsynth/sequencer。一些想法
取自家用电脑黄金时代流行的 .mod 格式。
目前它只是一个模型。笔记是从一个数组中读出的
最多 64 个值,其中数组中的每个位置对应于第 16 个值
笔记。到目前为止一切顺利,一切正常,旋律播放
正好。如果从一个音符过渡到另一个音符,就会出现问题。
例如f4 -> g#4。由于这是一个突然的变化,所以有一个明显的 pop/click
声音。为了补偿我试图在不同频率之间进行插值
并开始编写一个简单的示例来说明我的想法并验证它是
正在工作。
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 赫兹,绿点代表插值。
在我看来,这看起来应该可行!
但是,如果我将这种技术应用到我的项目中,结果就不一样了!
事实上,如果我将输出渲染为一个 wave 文件,这些文件之间的转换
两个频率看起来像这样:
显然,这会使爆音变得更糟。可能有什么问题?
这是我的(缩短的)代码:
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;
}
}
谢谢! =)
你错过了当你切换频率时,generate
中的 pos
值失去了不变性,也就是说,当 运行 具有不同的频率时,Math.sin((pos) * Math.PI * 2 / sampleRate * frequency * 2)
给出了非常不同的值。相反,您应该使用 "phase" 变量,它会 运行 从 0 到 1,然后回到 0,然后像锯齿图一样再次向前,并且将通过 (current frequency)*(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);
}
}
事实上,您的通道应该进入一个单独的 class,它将所有参数(相位、频率、波形、音量)保存在一起,然后,无论何时您需要它们进行采样,您只需可以调用 channels[i].generateNextSample()
并获得一个浮点数,而无需参数的所有麻烦。另外,一个频道,一个频率,所以跳过那些 "oldFrequency" 东西。
作为后续,Channel
class 的草图:
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);
}
}
我有点迷茫,希望有人能对此有所启发。 出于好奇,我正在研究一个简单的 softsynth/sequencer。一些想法 取自家用电脑黄金时代流行的 .mod 格式。 目前它只是一个模型。笔记是从一个数组中读出的 最多 64 个值,其中数组中的每个位置对应于第 16 个值 笔记。到目前为止一切顺利,一切正常,旋律播放 正好。如果从一个音符过渡到另一个音符,就会出现问题。 例如f4 -> g#4。由于这是一个突然的变化,所以有一个明显的 pop/click 声音。为了补偿我试图在不同频率之间进行插值 并开始编写一个简单的示例来说明我的想法并验证它是 正在工作。
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 赫兹,绿点代表插值。 在我看来,这看起来应该可行! 但是,如果我将这种技术应用到我的项目中,结果就不一样了! 事实上,如果我将输出渲染为一个 wave 文件,这些文件之间的转换 两个频率看起来像这样:
显然,这会使爆音变得更糟。可能有什么问题? 这是我的(缩短的)代码:
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;
}
}
谢谢! =)
你错过了当你切换频率时,generate
中的 pos
值失去了不变性,也就是说,当 运行 具有不同的频率时,Math.sin((pos) * Math.PI * 2 / sampleRate * frequency * 2)
给出了非常不同的值。相反,您应该使用 "phase" 变量,它会 运行 从 0 到 1,然后回到 0,然后像锯齿图一样再次向前,并且将通过 (current frequency)*(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);
}
}
事实上,您的通道应该进入一个单独的 class,它将所有参数(相位、频率、波形、音量)保存在一起,然后,无论何时您需要它们进行采样,您只需可以调用 channels[i].generateNextSample()
并获得一个浮点数,而无需参数的所有麻烦。另外,一个频道,一个频率,所以跳过那些 "oldFrequency" 东西。
作为后续,Channel
class 的草图:
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);
}
}