Minim:快速入门指南

欲开始使用Minim,首先你得建立一个Minim对象,这样你便能载入音频文件或者建立声音输入与输出。而后,在你退出程序前,你必须关闭你从Minim获得的所有I/O类然后关闭你的Minim范本。音频输入/输出类包括AudioPlayer, AudioSample, AudioSnippet, AudioInput, 和 AudioOutput。达到这个目的的一个很好的方式就是在你关闭所有音频类的位置定义一个停止(stop)方法(method)。以下是以上几点的一个范例(部分):

Minim minim;
AudioPlayer player;
AudioInput input;

void setup()
{
  size(100, 100);

  minim = new Minim(this);
  player = minim.loadFile("song.mp3");
  input = minim.getLineIn();
}

void draw()
{
  // 做你想做的
}

void stop()
{
  // 你从Minim.loadFile()获得的音频播放器
  player.close();
  // 你从Minim.getLineIn()获得的音频输入
  input.close();
  minim.stop();

  // 这些称作停止方法(stop method) 
  // you are overriding by defining your own
  // 它必须被呼叫用以完成你的程常规的清理工作 
  super.stop();
}

Ess及Sonia的用户应当对这些协定非常熟悉。做所有这些是因为全部音频输入/输出都在独立的线路工作,并且他们必须被允许以常规的方式结束。你同样可以在执行你程序的时候关闭一个音频类以释放它的资源。

继续阅读Minim:快速入门指南

Minim:关于

Minim是一个为在Proessing环境下工作的人们提供的易用的音频库(library),它基于JavaSound API、一点点Tritonus和Javazoom的MP3SPI开发。掩藏在其API之后的哲学是尽可能简单的将音频互动加入至你的sketch内,同时也为高级用户提供足够多的弹性。你无需做回调(callback)甚至无需直接操作采样数组(sample arrays),所有脏活累活都为你做好了。下载提供两种口味:1)仅仅是运行Minim必须的jar文件,2)包括javadocs、例子以及源码的完全版。Minim经LGPL许可发布。

安装:解压zip档到你sketchbook文件夹(在偏好设置里指定)的libraries文件夹内(如果不存在,则手动创建),承载它的文件夹应命名为minim(Processing现已自带Minim库)。

以下为Minim的一些特性:

  • 音频播放器(AudioPlayer):WAV, AIFF, AU, SND, 和 MP3文件的单声道或立体声回放。
  • 音频meta数据(AudioMetaData):一个装载文件meta数据的对象,例如ID3标签。
  • 音频录音机(AudioRecorder):经缓冲或直接将单声道或立体声音频录入硬盘。
  • 音频输入(AudioInput):单声道或立体声输入监听器。
  • 音频输出(AudioOutput):单声道或立体声合成。
  • 音频信号(AudioSignal):一个提供你写自己的声音合成类的简单界面。
  • 配备所有标准波形,粉噪音及白噪音生成器。此外,你还可以通过简单执行你自己的周期波形来扩展振荡器类。
  • 音频效果(AudioEffect):一个提供你写自己的音频效果的简单界面。
  • 配备低通(low pass)、高通(high pass)、带通(band pass)、陷波(notch)滤波器。此外,你还可以通过对你自己的IIR滤波器的简单执行来扩充IIRFilter类。
  • 可以方便的将信号和效果加于音频输入和输出上。
  • 提供一个可做频谱分析的FFT类。
  • 提供一个可做拍子侦测的BeatDetect类。

计划中的特性:

  • 更多的音频效果,如混响、延迟、移相、回响调制以及一点点破音。
  • 使用频主体过滤技术提供更好的滤波效果(frequency domain filtering techniques)。

要开始使用Minim,你可以进入下一课的学习,或去日它的javadocs。如果有任何问题或者发现任何bug,可以通过作者主页以及p5论坛联系他。

Processing:数组(下)

12. ok,现在来做100辆车吧:

pr9-3
pr9-3

Car[] cars = new Car[100]; // 由100个cars对象组成的数组!

void setup() {
size(200,200);
smooth();
for (int i = 0; i < cars.length; i ++ ) { // 用for循环初始化100辆车.
cars[i] = new Car(color(i*2),0,i*2,i/20.0);
}
}

void draw() {
background(255);
for (int i = 0; i < cars.length; i ++ ) { // 用for循环跑每一辆车.
cars[i].move();
cars[i].display();
}
}

当然,你的Car类还是放在一边,不需要改动任何参数,无论我们只跑1辆车,还是同时跑100辆,甚至1000辆车。

13. 当你设计你的类的时候,运用布尔变量来变换一个对象的属性是很方便的。例如,一个Car对象是跑还是不跑,Zoog可能高兴或不高兴。

pr9-4
pr9-4

14. 好的,来运用上边说的,继续来看超长晕眩代码。。。
// 一个stripes的数组
Stripe[] stripes = new Stripe[70];

void setup() {
size(200,200);

// Initialize all Stripe objects
for (int i = 0; i < stripes.length; i ++ ) {
stripes[i] = new Stripe();
}
}

void draw() {

background(30);
// 移动并显示所有stripes数组
for (int i = 0; i < stripes.length; i ++ ) { // 检查鼠标是否位于条纹上 stripes[i].rollover(mouseX,mouseY); // 将鼠标坐标传递进入一个对象
stripes[i].move(); stripes[i].display();
}
}

class Stripe {
float x; // 条纹的水平坐标值
float speed; // 条纹的移动速度
float w; // 条纹的宽度
// 一个布尔变量持续捕捉对象的状态.
boolean mouse; // 条纹的状态(鼠标是否位于条纹上?)

Stripe() {
// 所有条纹始于0
x = 0;
// 所有条纹都有一个随机的正向移动速度
speed = random(1);
w = random(10,30);
mouse = false;
}

// 绘制条纹
void display() {
// 布尔变量决定条纹颜色
if (mouse) {
fill(255);
} else {
fill(255-x,100+w,x);
}

noStroke();
rect(x,0,w,height);
}

// 移动条纹
void move() {
x += speed;
if (x > width + 20)
x = -20;
}

// 检查点(mx,my)是否位于条纹内.
void rollover(int mx, int my) {
// 条纹左沿是x, 右沿是x + w
if (mx > x && mx < x + w) {
mouse = true;
} else {
mouse = false;
}
}
}

pr9-5
pr9-5

15. 数组有一些函数可用于控制它的大小,它们是:horten( ), concat( ), subset( ), append( ), splice( ), expand( ) 。此外还有可以改变数组顺序的函数,比如sort( )reverse( ) 。Daniel这里偷懒了,告诉我们这些所有的函数都能在pr的参考里找到(废话。。),然后举了这么一个例子(使用append( )增大数组):
Ball[] balls = new Ball[1]; // 我们从仅有一个元素的数组开始.
float gravity = 0.1;

void setup() {
size(200,200);
smooth();

// 初始化0号球
balls[0] = new Ball(50,0,16);
}

void draw() {
background(255);

// 更新并显示所有的球
for (int i = 0; i < balls.length; i ++ ) {
// 无论数组有多长,更新并显示所有的元素.
balls[i].gravity();
balls[i].move();
balls[i].display();
}
}

void mousePressed() {
// 一个新的ball对象
Ball b = new Ball(mouseX,mouseY,random(10,30)); // 在鼠标位置制造一个新对象.
balls = (Ball[]) append(balls,b);
// 这里,函数append() 在数组的末尾增加了一个元素.
// append() 使用了两个引数. 第一个是你希望附加的数组, 第二是你希望附加的东西.
// 你需要将append() 得到的结果重新分配给最初的数组.
// 另外, append()函数要求你通过向圆括号内填入数组数据类型((Ball[]))的方式再次清楚的表述数组里的数据类型。
//这一过程叫做投掷(casting)
}

class Ball {
float x;
float y;
float speed;
float w;

Ball(float tempX, float tempY, float tempW) {
x = tempX;
y = tempY;
w = tempW;
speed = 0;
}

void gravity() {
// 重力加速度
speed = speed + gravity;
}

void move() {
// 将速度加到y轴
y = y + speed;
// 如果小球到底
// 将速度反向
if (y > height) {
speed = speed * -0.95;
y = height;
}
}

void display() {
// 显示球
fill(random(255));
stroke(0);
ellipse(x,y,w,w);
}
}

Processing:数组(上)

1. 一个数组(Array)就是包含了一串变量列(list of variables)的一个阵列。当程序需要诸多相似数据的例子的时候,就是使用数组的时候,比如画100辆上一课中的小车。

2. 列(list)因两个重要的原因显得非常有用:i、列记录列内元素的踪迹;ii、列保持那些元素的顺序。后一点至关重要,因为在很多程序中,信息的顺序与信息本身一样重要。

3. 一个列的第一位是0。

4. 申明一个列:列类型 [ ] 列名称

5. 每个列都有一个固定的数目,不可更改。

6. 创造一个新的列:列类型 [ ] 列名称 = new 类型 [列数目]

7. 几个例子:
// 一个由四个浮点数组成的列
float[] scores = new float[4];

// 一个由100个Humen对象组成的列
Human[] people = new Human[100];

// 用一个变量来指定数目
int num = 50;
Car[] cars = new Car[num];

// 用一个表达式来指定数目
Spaceship[] ships = new Spaceship[num*2 + 3];

8. 重复一千次,每个在0~10间随机:
float[] values = new float[1000];

for (int i = 0; i < 1000; i ++ ) {
values[i] = random(0,10);
}

9. 这么搞的话,我们就不必每次都在原括号内指定列的数目了:
float[] values = new float[100];

for (int i = 0; i < values.length; i ++ ) {
values[i] = 0;
}

是的,关键就在于values.length

pr9-1
pr9-1

10. 捕捉鼠标运动轨迹的一个例子(list的储存功能):
// 申明两个具有50个元素的数组.
int[] xpos = new int[50];
int[] ypos = new int[50];

void setup() {
size(200,200);

smooth();
// 初始化每个数组内的所有元素为0.
for (int i = 0; i < xpos.length; i ++ ) {
xpos[i] = 0;
ypos[i] = 0;
}
}

void draw() {
background(255);

// 变化数组的值
for (int i = 0; i < xpos.length-1; i ++ ) {
// 将所有原素向下移动一个位置.
// xpos[0] = xpos[1], xpos[1] = xpos = [2], 等等. 在倒数第二个元素的位置停止.
xpos[i] = xpos[i+1];
ypos[i] = ypos[i+1];
}

// 新位置
xpos[xpos.length-1] = mouseX; // 用鼠标位置更新数组的上一个位置.
ypos[ypos.length-1] = mouseY;

// 绘制一切
for (int i = 0; i < xpos.length; i ++ ) {
// 为数组内的每一个元素画一个圆.
// 颜色与大小同循环的计数器(i)挂钩.
noStroke();
fill(255-i*5);
ellipse(xpos[i],ypos[i],i,i);
}
}

11. 解读一下上边那个例子:
首先,申明两个数组
int[] xpos = new int[50];
int[] ypos = new int[50];

其次,在setup()内,我们必须初始化数组。因为一开始并没有任何鼠标运动,因此我们将数组填入0
for (int i = 0; i < xpos.length; i ++ ) {
xpos[i] = 0;
ypos[i] = 0;
}

在draw()内每个主要的循环,我们想要用当前鼠标的位置更新数组。让我们选择将鼠标当前位置放入数组被最后一个位置。数组的长度是50,意味着数目从0~49。最后一个数目是49,或者说,数组的长度减1。
xpos[xpos.length-1] = mouseX;
ypos[ypos.length-1] = mouseY;

现在进入难点。我们仅仅想要保存最后50个鼠标的位置。随着我们在数组末尾将鼠标位置储存进去,我们便改写了之前存储在那里的信息。如果鼠标前一帧位于(10,10)后一帧位于(15,15),我们就会想要将10,10)放在数组倒数第二位,而将(15,15)放在最后一位。解决方法是:在更新当前位置之前将所有元素向下移动一个位置。如图pr9-2。

pr9-2
pr9-2

第49位的元素移动到48位,48移动到47,如此往复。我们可以在将每个元素i设置为i+1的基础上循环这个数组,而达到效果。注意我们必须在倒数第二位的时候停止,因为对于长度为50的数组来说,没有元素50。换句话说,相较
i < xpos.length;
我们必须将它替换为:
i < xpos.length-1;

因此,此部分整段代码应写为:
for (int i = 0; i < xpos.length-1; i ++ ) {
xpos[i] = xpos[i+1];
ypos[i] = ypos[i+1];
}

最后,我们可以利用之前的鼠标位置记录画出一系列圆形。在整个xpos和ypos数组内,基于每个元素的值绘制一个圆形:
for (int i = 0; i < xpos.length; i ++ ) {
noStroke();
fill(255);
ellipse(xpos[i],ypos[i],32,32);
}

为了让它看起来更牛逼一些,我们可以选择将圆形的亮度和大小与数组的位置挂钩,即,较早的(因此更老)值将更亮而较晚(更新)的值则更大更暗。这可以由不断记数中的变量i赋值给color和size来实现。
for (int i = 0; i < xpos.length; i ++ ) {
noStroke();
fill(255-i*5);
ellipse(xpos[i],ypos[i],i,i);
}

学到这里:Daniel爷爷残酷的出了一道题:用面向对象的方式以Snake类重写上边这个例子。。。(一个更进一步的问题:创建一个包含x和y坐标的类Point,每个对象snake都含有一个Point对象的数组,而不是上边两个单独的x与y值的数组。。。。)。well,我承认我完全傻逼和晕乎了,如果你和我一样,那么就停下来稍后再想这个问题吧,如果你想来想去还是想不出来,那么可以点击这里下载答案。

Processing:对象

1. 类(Class)的名字常常以大写开头,用以与变量区分

2. 初始化对象(Object)的方式:myCar = new Car(); 。如果你忘记初始化一个对象,则他会被pr默认为null。null意味着什么也没有,既不是0,也不是负数。如果你遇到类似“NullPointerException”的错误,绝大多数情况下,便是由于忘记初始化一个对象造成的。

3. 使用一个对象的方式:variableName.objectMethod(Method Arguments); 例如:myCar.draw();

4. 看一个对象应用的例子:
Car myCar; // 申明对象car为一个全局变量

void setup() {
size(200,200);
// 初始化Car对象
myCar = new Car(); // 呼叫构造器(constructor)在setup() 内初始化car对象.
}

void draw() {
background(255);
// 操作对象Car.
myCar.move(); // 用.语法(dots syntax)呼叫对象方式(method)在draw( ) 内运行对象car.
myCar.display();
}

class Car { // 定以一个类(class).
color c; // 变量们.
float xpos;
float ypos;
float xspeed;

Car() { // 一个构造器(Constructor).
c = color(175);
xpos = width/2;
ypos = height/2;
xspeed = 1;
}

void display() { // 函数.
// 车就是一个矩形
rectMode(CENTER);
stroke(0);
fill(c);
rect(xpos,ypos,20,10);
}

void move() { // 函数.
xpos = xpos + xspeed;
if (xpos > width) {
xpos = 0;
}
}
}

5. 与自定义函数一样,一个类可以摆放在你程序任意的位置。

New Tab
New Tab

6. 点击pr窗口最右边的箭头按钮,会弹出菜单,选择“New Tab”则可以在当前窗口打开新标签,如上例,你可以将类Car写入新开的标签内,这样更便于阅读。注意,当你保存的时候,将会在你的sketch文件夹内同时保存成两个pde文件。本人经实践证明,必须把从class开始一直到结尾的代码全部拷入新标签中,程序才不会出错,新标签的名字可以随便取,但是最好与你的类一致。

7. 如果需要两个car对象呢?请看下边这段代码:
Car myCar1;
Car myCar2; // 两个对象!

void setup() {
size(200,200);
myCar1 = new Car(color(255,0,0),0,100,2); // 当对象被构造的时候,参数进入圆括号内
myCar2 = new Car(color(0,0,255),0,10,1);
}

void draw() {
background(255);
myCar1.move();
myCar1.display();
myCar2.move();
myCar2.display();
}

class Car { // 尽管有多个对象,我们仍仅需要一个类。就像无论我们制造多少个曲奇,我们都仅需要一台曲奇机。
color c;
float xpos;
float ypos;
float xspeed;

Car(color tempC, float tempXpos, float tempYpos, float tempXspeed) { // 构造器被引数定义.
c = tempC;
xpos = tempXpos;
ypos = tempYpos;
xspeed = tempXspeed;
}

void display() {
stroke(0);
fill(c);
rectMode(CENTER);
rect(xpos,ypos,20,10);
}

void move() {
xpos = xpos + xspeed;
if (xpos > width) {
xpos = 0;
}
}
}

8. 还记得之前那个弹跳小球的例子吗?看看它用对象和类重写的例子(源文件)。(是的,很绕,而且暂时还无法看出这样做的好处。。)

9. 对象是像整数和小数一样的数据类型。因为类是由数据构成的,因此一个对象可以包含另一个对象。例如:
class PlaceSetting {
Fork fork;
Spoon spoon;
PlaceSetting() {
fork = new Fork();
spoon = new Spoon();
}
}

10. 一个函数可以以一个对象作为其引数。

11. 在上一课中,我们知道,函数传递的是一个副本。而对象则不同,当其传递进入一个函数后,任何对被传递对象进行的改动都将影响到整个sketch内其出现的地方。这种现象叫做 参照传递
(pass by reference) 。

12. 什么时候应该使用“基于对象的程序设计”(object-oriented programming)?对于Daniel先生来说,答案是“总是”。对象允许你将一个软件程序内的概念组织成为模块,一个可重复利用的包裹。尽管如此,并不是在创建每一个项目的时候使用基于对象的方式都很方便和必要,尤其当你处于学习阶段的时候。对于你希望通过pr来创建的项目,Daniel老师的建议是:步进式。首先从往setup()和draw()里写代码开始,顺序按逻辑写出你想要的和你想看到的。随着你的项目开始成长,抽时间重新组织你的代码,也许首先从函数开始,然后进入对象。当然,是在不改变最终你想要的结果的前提下。

Processing:函数(Function)

1. 用户自定义函数的好处:模块化,易读,拆分大程序,反复利用性强。还有一个重要的:查错。我们可以//掉某些自定义函数,一一运行其余的,来逐一找到问题所在。

2. 比如line()这样的函数属于processing自带库的函数。

3. 一个函数定义的结构是这样子的:
returnType functionName (arguments ) {
// Code body of function
}

4. 你可以直接用 函数名称() 这样的方式在draw()内调用你的自定义函数。你可以在setup()draw()以外的任何地方定义你自己的函数,当然,惯例是放在你的draw()程序块下。

5. 引数(Argument)是执行函数的条件。属于局部变量的范畴。

6. 局部变量可以在一个函数中被申明和使用。

7. void drawCar(int x, int y, int thesize, color c) 圆括号里的值叫做引数(Argument)。
void draw() {
drawCar(100,100,64,color(20,250,0,79));
}

圆括号里的值叫做参数(Parameter)。
二者是一种传递的关系。
可以这样假设:主程序呼叫函数并传递引数。函数得到引数后,便可利用它做任何想做的事。

8. 传递参数时需要注意的:
1)你必须传递与定义函数相同数量的参数
2)当一个参数被传递,它的类型(type)必须与在自定函数内申明的引数一致。例如整数传整数,小数传小数
3)作为参数传递给函数的值可以是数字(20,5,4,3,等等),变量(x,y,等等),或者一个公式的结果(8+3, 4 * x/2, random(0,10), 等等)
4)引数属于函数内局部变量并仅能在那个函数内被辨知

9. 运行这段代码:
void setup() {
float num = 10;
println(“The number is: ” + num);
randomizer(num);
println(“The number is: ” + num);
}

void randomizer(float newnum) {
newnum=newnum+random(-2,2);
println(“The new number is: ” + newnum);
}

由它可见,无论newnum怎样改变,num的值永远都是10,num的值不受影响因为它所传递的只是一个副本(copy)。

10. 由上例,我们也能看出一个使用函数的程序的流程:
1)设置num等于10
2)列引出num的值
3)呼叫函数randomizer
a. 设置newnum等于newnum加上一个随机数字
b. 列印newnum的值
4)列印num的值

11. 将noLoop()用在draw()里边可以停止程序循环,也就是说只运行draw()里边的程序一次。

12. 原来写在很多函数前的“void”意思是没有返回类型(ReturnType)的意思。。

13. 你可以在函数名称前使用诸如int或float之类的返回类型,另外不要忘记在程序最后加上“return 返回值的名称(例如变量名)“

14. 计算两点间的距离,可以使用pr内置函数dist()。用法: dist(100, 100, mouseX, mouseY); 计算点(100,100)与鼠标位置之间的距离。

15. 开方的写法:sqrt()

16. 再来一个华氏转摄氏的例子:
void setup() {
float temp = tempConverter(60);
println(temp);
}

float tempConverter(float farenheit) {
float celsius = (farenheit – 32) * (5.0/9.0);
return celsius;
}

反正目的就是代来代去的把我们搞晕@@

依据以前几课的经验,本课出题的重点仍然在于将之前我们写过的程序用自定义函数的方式重写调用。