
引言
本文將持續(xù)研究隱藏在電音合成軟件背后的數(shù)學(xué)問題:線性代數(shù)、信號與系統(tǒng)、和聲學(xué)基礎(chǔ)。以上的數(shù)學(xué)知識與游戲開發(fā)、計(jì)算機(jī)音頻、計(jì)算機(jī)圖像聯(lián)系緊密。正因?yàn)閾碛袕?qiáng)大的數(shù)學(xué)基礎(chǔ),計(jì)算機(jī)模擬的音像世界才可以像今天這樣逼真。數(shù)學(xué)在音頻編輯器的開發(fā)中占有重要的位置。對此數(shù)學(xué)單獨(dú)進(jìn)行研究,由此顯得非常重要。本文將對聲音和合成和顯示進(jìn)行探究,進(jìn)而使用這一些專業(yè)知識編寫音頻編輯器或圖形程序。
一.線性代數(shù)
1.點(diǎn)乘
我們知道點(diǎn)乘在數(shù)學(xué)中,又稱數(shù)量積。只需要記住,向量點(diǎn)乘就是對應(yīng)分量乘積的和,其結(jié)果是一個標(biāo)量。在平面中,兩個向量的點(diǎn)乘可表示為:
設(shè)
則
一般來說,點(diǎn)乘的結(jié)果描述了兩個向量的相似程度,點(diǎn)乘結(jié)果越大,兩向量越接近。點(diǎn)乘等于向量的大小與向量夾角cos值的積。其公式為:
實(shí)際上,我們在計(jì)算中,更重要的是求出兩個向量的之間的夾角。對上述的公式(1)稍作變換,可得:
值得注意的是,此公式十分重要。在本文中被用于Unity仿真中的向量夾角計(jì)算。
2.線性空間變換
方陣能描述任意的線性變換。其中,線性變換,從感性上講,指的是對圖片或函數(shù)圖像等其所在空間的,拉伸、旋轉(zhuǎn)、仿射等變換。
想象任意一個2維空間的向量,比如說:。那么,向量a其實(shí)可以表示為:
。由于向量可視為空間中的一個點(diǎn),向量
表示從原點(diǎn)向
走3個單位后,再向
走
個單位。更進(jìn)一步地,對于任意一個向量:
,可以表示為:從原點(diǎn)出發(fā),向
軸走
個單位后,再向
軸方向走
個單位,即到達(dá)向量
的所在地。那么,任意一個二維空間中的向量,可以表示為基向量的線性組合。
一個任意的向量,被描述為了兩個基向量:
的線性組合。但實(shí)際上,基向量不一定要相互垂直(講人話:x軸和y軸不一定要相互垂直)。只要兩個向量不在一條直線上,就可以構(gòu)成一組基向量來描述二維空間中的任意一個點(diǎn)。
以下是幾個不同基向量所張成的空間。為了方便觀察,我在其中放置一樣的函數(shù)??梢杂^察到,隨著選取的基向量的不同,空間可以發(fā)生放大、縮小、切變等線性變換。存在于里面的函數(shù)圖像,雖然位置沒有發(fā)生改變,但是由于函數(shù)所處空間的改變,函數(shù)“被迫”發(fā)生了改變。

設(shè)基向量,
張成了一個二維平面空間。使用基向量X,Y構(gòu)成一個
的矩陣
:
用一個向量乘以該矩陣
,則稱為對向量
的一次“變換”。
3. 旋轉(zhuǎn)
問題的提出:
將一個函數(shù)圖像逆時針旋轉(zhuǎn)
具體求解步驟:
隨機(jī)生成一個函數(shù)序列
構(gòu)造方陣
函數(shù)序列每一點(diǎn)的坐標(biāo),乘以方陣
顯示該函數(shù)圖像
函數(shù)序列生成:
使用matlab自帶的函數(shù)生成隨機(jī)離散序列
,使用
函數(shù)對
進(jìn)行數(shù)據(jù)平滑。
其代碼如下:
clc;
clear;
%%
%%隨機(jī)生成一個起點(diǎn)和終點(diǎn)都在0附近,均值為0的函數(shù)波形
n = 300;
x = 0.5*rand(1,n);
x = x - mean(x);
x(1:10) = 0;
x(n-10:n) = 0;
for i = 1:10
x = smooth(x,10);
end
x(1) = 0;
x(length(x)) = 0;
X = zeros(n,2);
X(:,1) = linspace(0,1,n);
X(:,2) = x;
plot(X(:,1),X(:,2),'.')
構(gòu)造方陣:
在笛卡爾坐標(biāo)系中,基向量,
.使得向量
向逆時針旋轉(zhuǎn)
,可得:
則,
結(jié)果演示:
根據(jù)上文求解步驟編寫代碼,實(shí)現(xiàn)函數(shù)旋轉(zhuǎn)算法。代碼運(yùn)行結(jié)果如下所示。

穩(wěn)定性測試:
使用Matlab,隨機(jī)生成一個函數(shù)序列,使用開發(fā)的旋轉(zhuǎn)算法持續(xù)旋轉(zhuǎn)函數(shù)圖像,并將結(jié)果打印出來,觀察算法的穩(wěn)定性。由結(jié)果可見,代碼對函數(shù)圖像的旋轉(zhuǎn)穩(wěn)定可靠。
核心代碼:
for angle = 1:10:360
%目標(biāo)向量
pos1 = [cosd(angle);sind(angle)];
pos2 = [0;0];
pos3 = pos1 - pos2;
pos0 = [1;0];
%%
if(pos3(2)>=0)
alpha = acos(sum(pos3.*pos0)/(norm(pos3)*norm(pos0)));
else
alpha = -1*acos(sum(pos3.*pos0)/(norm(pos3)*norm(pos0)));
end
%(alpha/pi)*180
temp = [cos(alpha) sin(alpha);...
cos(alpha + pi/2) sin(alpha+ pi/2);];
A = norm(pos3)/1;
result = X*(A*temp) + pos2';
plot(result(:,1),result(:,2),'.');
hold on
end
其結(jié)果如下:

模型的改進(jìn):
令人興奮的是,將圖片放置在空間中,使用方陣M對空間進(jìn)行線性變換,可以很方便實(shí)現(xiàn)圖片的放大、縮小、旋轉(zhuǎn)操作。只需寫一點(diǎn)點(diǎn)matlab代碼。便可得到以下結(jié)果:

Unity仿真
在matlab完成算法探究以后,在游戲開發(fā)引擎Unity上實(shí)現(xiàn)該算法,完成算法的落地。
具體求解步驟:
使用Unity搭建參加場景,創(chuàng)建地面(Plane)、圓柱(Cylinder)等物體
編寫腳本,綁定組件。腳本邏輯是:當(dāng)用戶按下鼠標(biāo)時,攝像頭坐標(biāo)系中發(fā)出的射線與地面相互碰撞。根據(jù)此來確定基向量以及方陣
。
根據(jù)方陣
計(jì)算組件線段渲染器(LineRender)應(yīng)該所處的位置
顯示該函數(shù)圖像
穩(wěn)定性測試:
多次地、隨機(jī)地點(diǎn)擊游戲畫面,測試算法的穩(wěn)定性和性能。實(shí)驗(yàn)結(jié)果表明,算法穩(wěn)定可靠地運(yùn)行,完成了對算法的落地。.


代碼:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LineTest : MonoBehaviour
{
public float length = 0;
public float height = 0;//線段的高度
public float w = 0; //角頻率
public float A = 0; //震動幅度
private Vector3 Vec; //用于存儲每一點(diǎn)的坐標(biāo)
private LineRenderer lineRenderer;
// Start is called before the first frame update
void Start()
{
Vec = new Vector3();
//設(shè)置長度
GetComponent<LineRenderer>().positionCount = (int)length;
}
public void Run(float x,float y){
float l = (float)Math.Sqrt(x*x + y*y);//模長,假設(shè)0為原點(diǎn)
float alpha = 0; //用于求向量夾角
for (int i=0;i<length;i++){
float posX = (float)(i/length);
float posY = A*(float)Math.Sin(w*posX);
alpha = 0;
if(y>=0){
alpha = (float)Math.Acos(x/l);
}
else{
alpha = -1*(float)Math.Acos(x/l);
}
Vec.x = 0.1f*(float)(posX*l*Math.Cos(alpha) + posY*l*Math.Cos(alpha + Math.PI/2));
Vec.y = height;
Vec.z = 0.1f*(float)(posX*l*Math.Sin(alpha) + posY*l*Math.Sin(alpha + Math.PI/2));
GetComponent<LineRenderer>().SetPosition(i,Vec);
}
}
// Update is called once per frame
void Update()
{
}
}