声场 | 音频波形显示探究

图1 封面

引言

本文将持续研究隐藏在电音合成软件背后的数学问题:线性代数、信号与系统、和声学基础。以上的数学知识与游戏开发、计算机音频、计算机图像联系紧密。正因为拥有强大的数学基础,计算机模拟的音像世界才可以像今天这样逼真。数学在音频编辑器的开发中占有重要的位置。对此数学单独进行研究,由此显得非常重要。本文将对声音和合成和显示进行探究,进而使用这一些专业知识编写音频编辑器或图形程序。

一.线性代数

1.点乘

我们知道点乘在数学中,又称数量积。只需要记住,向量点乘就是对应分量乘积的和,其结果是一个标量。在平面中,两个向量的点乘可表示为:

a = \begin{bmatrix} x_1\\ y_1\\ \end{bmatrix} ,b = \begin{bmatrix} x_2\\ y_2\\ \end{bmatrix}
a \cdot b = [x_1*y_1 + x_2*y_2]

一般来说,点乘的结果描述了两个向量的相似程度,点乘结果越大,两向量越接近。点乘等于向量的大小与向量夹角cos值的积。其公式为:

a \cdot b =||a||*||b||cos\theta \tag{1}

实际上,我们在计算中,更重要的是求出两个向量的之间的夹角。对上述的公式(1)稍作变换,可得:

\theta = arccos(\frac{a \cdot b}{||a||*||b||})

值得注意的是,此公式十分重要。在本文中被用于Unity仿真中的向量夹角计算。

2.线性空间变换

方阵能描述任意的线性变换。其中,线性变换,从感性上讲,指的是对图片或函数图像等其所在空间的,拉伸、旋转、仿射等变换。
想象任意一个2维空间的向量,比如说:a = \begin{bmatrix}3\\5\\\end{bmatrix}。那么,向量a其实可以表示为:a = 3*\begin{bmatrix}1\\0\\ \end{bmatrix} + 5*\begin{bmatrix}0\\1\\ \end{bmatrix}。由于向量可视为空间中的一个点,向量a表示从原点向x走3个单位后,再向y5个单位。更进一步地,对于任意一个向量:b = \begin{bmatrix}x_1\\y_1\\\end{bmatrix},可以表示为:从原点出发,向x轴走x_1个单位后,再向y轴方向走y_1个单位,即到达向量b的所在地。那么,任意一个二维空间中的向量,可以表示为基向量的线性组合

一个任意的向量a,被描述为了两个基向量x= \begin{bmatrix}1\\0\\ \end{bmatrix} , y = \begin{bmatrix}0\\1\\ \end{bmatrix}的线性组合。但实际上,基向量不一定要相互垂直(讲人话:x轴和y轴不一定要相互垂直)。只要两个向量不在一条直线上,就可以构成一组基向量来描述二维空间中的任意一个点。

以下是几个不同基向量所张成的空间。为了方便观察,我在其中放置一样的y=3.5sin(5x)函数。可以观察到,随着选取的基向量的不同,空间可以发生放大、缩小、切变等线性变换。存在于里面的函数图像,虽然位置没有发生改变,但是由于函数所处空间的改变,函数“被迫”发生了改变。

图2 空间变换

设基向量X = \begin{bmatrix}x_1\\y_1\\\end{bmatrix},Y = \begin{bmatrix}x_2\\y_2\\\end{bmatrix}张成了一个二维平面空间。使用基向量X,Y构成一个2\times2的矩阵M:
M = \begin{bmatrix} x_1 & y_1 \\ x_2 & y_2 \\ \end{bmatrix} \tag{2}
用一个向量a = \begin{bmatrix}x&y\end{bmatrix}乘以该矩阵M,则称为对向量a的一次“变换”。
a*M = \begin{bmatrix}x&y\end{bmatrix} \begin{bmatrix} x_1 & y_1 \\ x_2 & y_2 \\ \end{bmatrix} \tag{2} = \begin{bmatrix}x*x_1 + y*x_2 & x*y_1 + y*y_2\end{bmatrix}

3. 旋转

问题的提出
将一个函数图像逆时针旋转30°

具体求解步骤
Step 1:随机生成一个函数序列
Step 2:构造方阵M
Step 3:函数序列每一点的坐标,乘以方阵M
Step 4:显示该函数图像

函数序列生成
使用matlab自带的rand()函数生成随机离散序列X(n),使用smooth()函数对X(n)进行数据平滑。
其代码如下:

clc;
clear;
%%
%%随机生成一个起点和终点都在0附近,均值为0的函数波形
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),'.')

构造方阵M
在笛卡尔坐标系中,基向量X = \begin{bmatrix}1&0\\\end{bmatrix},Y = \begin{bmatrix}0&1\\\end{bmatrix}.使得向量X、Y向逆时针旋转30°,可得:
X = \begin{bmatrix}cos30°&sin30°\\\end{bmatrix}
Y = \begin{bmatrix}cos(30° + 90°)&sin(30° + 90°)\\\end{bmatrix}
则,M = \begin{bmatrix} cos30° & sin30° \\ cos(30° + 90°) & sin(30° + 90°) \\ \end{bmatrix} \tag{2}

结果演示
根据上文求解步骤编写代码,实现函数旋转算法。代码运行结果如下所示。

图3 函数旋转

稳定性测试
使用Matlab,随机生成一个函数序列,使用开发的旋转算法持续旋转函数图像,并将结果打印出来,观察算法的稳定性。由结果可见,代码对函数图像的旋转稳定可靠。

核心代码

for angle = 1:10:360
%目标向量
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

其结果如下:

图4 算法稳定性测试

模型的改进
令人兴奋的是,将图片放置在空间中,使用方阵M对空间进行线性变换,可以很方便实现图片的放大缩小旋转操作。只需写一点点matlab代码。便可得到以下结果:
图5 图像放大、缩小、旋转代码实现结果

Unity仿真

在matlab完成算法探究以后,在游戏开发引擎Unity上实现该算法,完成算法的落地。

具体求解步骤
Step 1:使用Unity搭建参加场景,创建地面(Plane)、圆柱(Cylinder)等物体
Step 2:编写脚本,绑定组件。脚本逻辑是:当用户按下鼠标时,摄像头坐标系中发出的射线与地面相互碰撞。根据此来确定基向量以及方阵M
Step 3:根据方阵M计算组件线段渲染器(LineRender)应该所处的位置
Step 4:显示该函数图像

稳定性测试
多次地、随机地点击游戏画面,测试算法的稳定性和性能。实验结果表明,算法稳定可靠地运行,完成了对算法的落地。.

图6 Unity仿真测试1

图7 Unity仿真测试2

代码

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;  //用于存储每一点的坐标
    private LineRenderer lineRenderer;

    // Start is called before the first frame update
    void Start()
    {
        Vec = new Vector3();
        //设置长度
        GetComponent<LineRenderer>().positionCount = (int)length;
    }
    public void Run(float x,float y){
        float l = (float)Math.Sqrt(x*x + y*y);//模长,假设0为原点
        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()
    {

    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容