如何用任何编程语言的代码创建最少的音乐

2020-10-31 02:38:51

我画得不是很好,所以我用了格式化的C代码。这应该是一个用C语言编写的三角形声波。事实上,这段C代码播放的是两个八度的旋律,它们是用标准输入的文本符号写成的。

它只有160个字节,适合现代的tweet,之所以有它,是为了展示用任何编程语言,而不仅仅是cSound、Chuck或SonicPI这样的特殊语言,用代码创建最低限度的音乐是多么简单。

这就是声音--一种能及时振动和移动空气的东西,空气传到你的耳朵里,你就能听到。图中上下起伏的波浪说明空气在振动。为了用数字来描述这个波,人们提出了一个想法,即以固定的时间间隔测量波的振幅,并将这些采样的数据点用作“数字声音”。

出现了两个问题--多久采样一次声波,以及如何以数字方式表示振幅范围的单位?要回答第一个问题,我们应该回想一下,人耳听不到任何高于20000赫兹的声音,这就是为什么CD音乐的采样率是22000赫兹。然而,现代声卡往往使用两倍高的采样率-44100 Hz或48000 Hz,甚至96000 Hz。像Arduino或NES这样的Lo-fi音频设备根本不能以如此高的速度发出声音,所以它们使用了相当低的采样率,比如8000 Hz,这就是我们在本文中将使用的采样率。

幅度量化也是一个折衷的问题。理论上,人们可以识别振幅的非常细微的变化,但计算机不能对每个样本使用无限精确的数字。相反,它们执行量化-它们将振幅值映射到固定宽度的数字,如浮点数或整数。在上面的图片中,我使用+1和-1作为振幅的最小值和最大值,假设是FLOAT数据类型,但是其他流行的格式是带符号的int16,其中振幅从-32768变为+32767,或者uint8,其中振幅从0变为255.。最后一个是我们将在本文中使用的,因为它非常容易理解,并且带来了一些不错的技巧。

现在,有了8000的采样率和无符号uint8的数据格式,数字声音无非就是一个以每秒8000字节的速度在时间上移动的字节数组。

如果我们的数组全为零-将会静默。如果我们的阵列将全部是255-也将是静默的。然而,如果我们的数组包含不同的数字-那就是声波。

我们怎么能听到呢?一种途径是使用原生操作系统API,这将是通向跨平台编程地狱的途径,因为每个操作系统都有一组不同的API,同样复杂,使用起来也不愉快。这就是UNIX Way的用武之地。有一些小的实用程序,如果你幸运的话-它们甚至可能是你的操作系统附带的-允许你播放来自stdin的声音流。在Linux上,这将是aplay或pacat,而MacOS和Windows用户必须安装SOX并使用PLAY命令。

以下几个命令应该允许您以每秒8000个样本的速率播放标准输入中的原始无符号字节:

Alias play=';aplay';alias play=';pacat--rate 8000--channel 1--format u8';alias play-c1-b8-eunsign-traw-r8k-';alias play=';mplayer-cache 1024-Quiet-rawdio samplesize=1:channel=1:rate=8000-demuxer rawdio-';alias play=';ffplay-ar 8000-ac 1-f u8-nodisp-';#play一些白噪音/dev/urandom|play=1:channel=1:rate=8000-demuxer rawdio-';#play-ar 8000-ac 1-f u8-nodisecat/dev/urandom|play。

数字声音是一个非常简单的编程概念,生成它可以像编写一个for循环一样简单。例如,这个小应用程序应该会发出无限的白噪音:

//cc noise.c-o Noise&;&;./Noise|play#include<;stdio.h>;#include<;stdlib.h>;int main(){for(;;)putchar(rand());}。

为了演奏音符而不是噪音,我们应该使声音具有周期性,波形应该以特定的频率重复,该频率将定义音符的音高。以下是最常见的振荡器波形:

C语言中最简单的振荡器是锯齿波。C隐式地将int强制转换为putchar内的无符号字符,所以如果我们只写(int t=0;;t++)putchar(T),我们会得到一个锯齿波。它还不会是音符,只会发出低沉的嗡嗡声。要演奏一个音符,我们需要知道它的频率。例如,最常见的“参考”音符是来自倍频程4的A音符,它的频率正好是440 Hz,这就是大多数音叉(Kamerton)产生共鸣的地方。

440 Hz意味着振荡器应该从0到255每秒精确440次。我们还知道,在一秒内,我们的循环必须产生8000个值,因为这是我们的采样率。因此,每次循环迭代时,我们应该将振荡器计数器增加256*440/8000=14.08。大概有14个。

要创建方波,我们可以简单地获得振荡器计数器的第8位,对于值0..127,它将是0;对于值127..255,它将是0x80。这将产生一个与锯齿波频率相同的方波,它会稍微安静一些,因为振幅范围会窄一倍,但仍然足够大,可以听到它。

正弦波需要sin()函数,我们可以使用振荡器计数器(相位),将其除以255,再乘以2π。产生的振幅应该乘以255才能与其他振荡器一样响亮,因为sin()返回范围[-1..1]中的值。

/*锯齿*/for(int t=0,OSC=0;t<;8000;t++,OSC=OSC+14){putchar(OSC);}/*方形*/for(int t=0,OSC=0;t<;8000;t++,OSC=OSC+14){putchar(OSC&;0x80);}/*正弦波*/for(int t=0,OSC=0;t<;8000;T++,osc=osc+14){putchar(127×sin(osc/255.0*2*3.14)+128);}。

还有一种产生振荡声音的方法,这是一种很聪明的方法。它通常用于模拟低音或吉他弦。其想法是用随机数据填充数组。阵列的长度应该等于振荡器的周期,在我们的例子中,对于440 Hz,这将是~18个样本。然后,我们将“播放”该数组中的字节,在到达缓冲区末尾时返回到第一个项目。尽管充满了随机字节,但随机噪声的重复模式听起来就像振荡器,我们会听到独特的音调。但是,为了让它听起来像个把戏,我们必须在每次循环数组时平滑数据-我们将用当前元素和下一个元素的平均值替换元素。这就是如何在每次迭代中,随机数变得越来越平滑,直到它们全部相等,振荡器在静默中淡出:

Unsign char a[18];for(int i=0;i<;sizeof(A);i++)a[i]=rand();for(int t=0;t<;8000;t++){int i=t%sizeof(A);int j=(t+1)%sizeof(A);putchar(a[i]=(a[i]+a[j])/2);}。

试着使用更大的阵列,看看音调是如何变低的,声音的持续时间是如何变长的。它不是很像弦或卡林巴尖牙的声音吗?

既然我们可以演奏一个音符,我们怎么才能演奏旋律呢?我们需要及时改变音符的音高,这就是步长定序器所做的事情。我们可以有固定数量的步长,每个步长都有固定的持续时间,我们可以在循环中重复它们,并相应地改变音调。每一步可以包含振荡器相位计数器的增量,零表示暂停。例如,这里有一个熟悉的即兴小品“E B D E D B B A B”。它只使用了4个音符-一个八度的D和E,另一个较低的八度的A+B。如果我们看音符频率表,这些音符在八度5和4的音高将是659.2赫兹(E),587.3赫兹(D),440赫兹(A)和493.8赫兹(B)。

振荡器相位增量将约为21(E)、19(D)、14(A)和16(B)。每一步可以采集2000个样本(1/4秒)。则播放循环可能如下所示:

Int OSC=0;int melody[8]={21,16,19,21,19,16,14,16};for(int step=0;;step=(step+1)%8){int增量=melody[step];for(int t=0;t<;2000;t++){OSC=OSC+Increment;putchar(OSC);}}

我想,现在是时候从这篇文章的一开始就对旋律播放器进行模糊处理了:

//play.c#include<;stdio.h>;int main(){Float f;/*音符频率*/char c;/*";cdefgab";对于音符或";pr";对于暂停*/int d,o;/*d=持续时间,o=八度*/While(scanf(";%d%c%d";,&;d,&;c,&;O)>;0){/*将备注转换为小写*/c&;=31;/*c>;>;4对于CDEFGAB是0,对于";PR";*//*因此,对于停顿,f将是零,对于备注-55*/f=!(C&>;&>4)*55;/*我们在诺基亚Composer帖子中使用的将备注字母转换到备注索引的技巧*/c=(c*8/5+8)%12+o*12-22;/*注意`x`频率为2^(x/12),或(2^(1/12))^x*/While(c--){f*=1.0595;/*1.0595为2^(1/12)*/}/*以给定的音高播放给定时长的锯齿波*/for(d=16e3/d;d--;putchar(d*f*.032));}}。

它播放类似于MML、RTTTL或ABC表示法的音乐。它期望来自标准输入的一系列注释。注释可以用空格、逗号或scanf安全忽略的任何其他符号分隔。每个音符有3个部分-持续时间、音高和八度,例如,我们上面的循环可以写成“8e5 8b4 8d5 8e5 8d5 8b4 8a4 8b4”。如上所述,音符频率计算来自Nokia Composer,音符回放使用锯齿形振荡器。

由于大量溢出,CDEFGAB之外的ASCII符号会导致尖锐的音符:

有一种名为ByteBeat的小众音乐流派,其中的音乐以简洁的C表达式编写。我希望在以后的文章中更详细地介绍它,因为它结合了微小代码的智慧和音乐创作的创造力。它大量使用位移位和位掩码来变戏法处理音符。有些旋律是偶然创作的,有些是精心创作的,目的是为了达到某个最终目标。他们往往听起来有点刺耳,也许有点跑调,但他们的美丽在于他们的代码。字节跳动的典型示例是:

它产生不确定音高的重复旋律,听起来像多种乐器,有一定的节奏。如果你感兴趣,你可以在网上找到更多字节跳动的例子。

纯粹的摆动声音令人厌烦。但幸运的是,有一些音效我们可以毫不费力地应用到它上。

例如,上面的字节跳动曲调可以通过某种低通滤波器来平滑高频,留下低频。低通滤波器的最简单形式是将当前输出值与存储在累加器中的前一个输出值近似:

Int main(){int prev=0;for(int t=0;;t++){int output=t*(t+(t>;>;9|t>;>;>;13))%40&;120;prev=prev*0.8+output*0.2;putchar(Prev);}}。

声音应该变得更低沉,更不高音。如果将0.8/0.2更改为0.9/0.1,效果应该会更强。试着调整系数,看看它是如何影响声音的。

如果您想要降低低频而保留高频-只需从原始信号中减去滤波后的低通信号即可。

另一个简单的影响是延迟线,它只是另一个数组,存储一些最近的信号值。例如,我们希望以0.1秒的延迟重新播放我们的声音。在8000 Hz的采样率下,我们需要存储800个最新样本,并将它们与当前输出信号相加,偏移量为800字节:

#定义N 800 int main(){int delay[N]={0};for(int t=0;;t++){int output=((t*(42&;t>;>;10))&;0xff)/2;延迟[t%N]=输出;/*将当前样本放入延迟线*/putchar(输出+延迟[(t+1)%N]);/*将当前样本与延迟线上最早的样本混合*/}}。

这应该会给声音带来一点复调,会有一些回声。延迟线非常容易实现,并且与可能导致混响效果的滤波器混合。

人们可以用C语言编写更多可能的效果,但是这篇文章太长了,我想我应该停止了。类似地,玩振荡器可能会激发您创建样例播放器、颗粒合成器或调频合成器的灵感。当然,音序器也是一个无穷无尽的实验领域-从随机的音乐生成和自我演变的旋律,到像老式mod追踪器这样的紧凑型音序器,可以用在简短的演示和游戏中。

如果你喜欢音乐--请随意分享你的声音实验!同时,我正在准备一篇关于1位声音优雅的帖子,用一个简单的工具来制作1位音乐。如果你有任何其他的音乐+节目话题想知道--只要给我写信就行了。

我希望你喜欢这篇文章。你可以在Github、Twitter或通过RSS订阅,并向其投稿。