Processing:算法(下)

6.2.4. 雨滴
可以分为如下步骤:
1)一个运动的雨滴
2)一个雨滴对象的数组
3)随机数量的雨滴(每次显示一滴)
4)优化雨滴外形

1)比较简单(先用个简单的圆形代替)
float x,y; // 雨滴位置变量
void setup() {
size(400,400);
background(0);
x = width/2;
y = 0;
}
void draw() {
background(255);
// 显示雨滴
fill(50,100,150);
noStroke();
ellipse(x,y,16,16);
// 移动雨滴
y++ ;
}

再次,我们需要通过将雨滴做成类,而得到一个关于雨滴类的数组,以实现很多雨滴。我们可以为其增加诸如速度或大小等变量,同时还可以加入一个测试雨滴是否滴到地面的函数。
class Drop {
float x,y; // 雨滴位置变量
float speed; // 雨滴速度
color c; // 雨滴颜色
float r; // 雨滴半径

Drop() {
r = 8; // 所有雨滴都是同样大小
x = random(width); // 由一个随机的x位置开始滴落
y = -r*4; // 从屏幕稍微靠上滴落
speed = random(1,5); // 选取一个随机速度
c = color(50,100,150); // 颜色
}

// 让雨滴滴落
void move() {
y += speed; // 以speed的速度运动
}

// 检查其是否落地
boolean reachedBottom() {
// 如果我们超过了底部一点点
if (y > height + r*4){
return true;
} else {
return false;
}
}

// 显示雨滴
void display() {
fill(50,100,150);
noStroke();
ellipse(x,y,r*2,r*2);
}
}

2)从一滴到一个数组滴
// 雨滴数组
Drop[] drops = new Drop[50];

void setup() {
size(400,400);
smooth();
// 初始化所有雨滴
for (int i = 0; i < drops.length; i++ ) {
drops[i] = new Drop();
}
}

void draw() {
background(255);
// 移动并显示所有雨滴
for (int i = 0; i < drops.length; i++ ) {
drops[i].move();
drops[i].display();
}
}

现在的问题是,全部雨滴一次性就下来了。而我们正在制作的游戏要求我们每N秒落一滴——因此现在我们就将进入第3)步。我们先抛开时间间隔问题,先做到每一帧更新一滴,然后将数组扩大,以容纳更多雨滴。

要做到上边说的,我们要启用一个新的变量来记录雨滴的总数——“totalDrops”。

Setup:
◎ 创建一个大小为1000的雨滴数组
◎ 设置totalDrops = 0。

Draw:
◎ 在数组totalDrops的位置创建一个新的雨滴。
◎ 递增 totalDrops(因此下一次我们到这个位置的时候,我们可以在数组的另一个点创建一滴雨)。
◎ 若totalDrops超出数组的大小,将它重设为0并从头开始。
◎ 移动并显示所有可用的雨滴(例如totalDrops)

代码如下:
// 雨滴数组
Drop[] drops = new Drop[1000];

// 一个新的储存雨滴总数的变量
int totalDrops = 0;

void setup() {
size(400,400);
smooth();
background(0);
}

void draw() {
background(255);

// 初始化一滴雨
drops[totalDrops] = new Drop();
// 递增totalDrops
totalDrops++ ;
// 如果到了数组最末
if (totalDrops >= drops.length) {
totalDrops = 0; // 重启
}

// 移动并显示雨滴
for (int i = 0; i < totalDrops; i++ ) { // 新特性!我们并非移动并显示所有雨滴,仅显示当前显示的“totalDrops”
drops[i].move();
drops[i].display();
}
}

雨滴
雨滴

4)好了,你知道一个圆形的确不像一滴雨,所以我们现在想办法模拟一滴雨的样子。Daniel用了一个聪明的方法,在垂直位置画一串圆,从小到大。
background(255);
for (int i = 2; i < 8; i++ ){
noStroke();
fill(50,100,150);
ellipse(width/2,height/2+i*4,i*2,i*2);
}

雨滴数组
雨滴数组

我们可以将这个算法整合到上例的雨滴类中,设x,y为圆形起始位置,并在for内将圆形半径循环到i。
// 显示雨滴
void display() {
// Display the drop
noStroke();
fill(c);
for (int i = 2; i < r; i++ ) {
ellipse(x,y+i*4,i*2,i*2);
}
}

6.3. OK,现在我们把它们全部放到一起
开一个新的sketch,开四个Tab,如下图,并将上边几个单独测试成功的部分填入。

4 Tabs
4 Tabs

我们无需改动各个分部的代码,我们需要考虑的是主程序内setup()draw()的内容。继续来写伪码:

Setup:
◎ 创建一个Catcher对象
◎ 创建一个雨滴数组
◎ 设置totalDrops为0
◎ 创建计时器对象
◎ 开启计时器

Draw:
◎ 设置Catcher位置为鼠标位置
◎ 显示Catcher
◎ 移动所有可用的雨滴
◎ 显示所有可用的雨滴
◎ 如果Catcher和雨滴相交
— 将相交雨滴从萤幕移除
◎ 如果计时器结束(finish)
— 增加雨滴的数量
— 重启计时器

全局变量:
Catcher catcher; // 一个容器对象
Timer timer; // 一个计时器对象
Drop[] drops; // 一个雨滴对象数组
int totalDrops = 0; // 雨滴总数

setup()中,变量被初始化。注意,我们可以跳过初始化数组中雨滴这一步,因为它们一次只被创建一滴。我们同样需要呼叫计时器的start()函数。
void setup() {
size(400,400);
catcher = new Catcher(32); // 以半径32创建容器
drops = new Drop[1000]; // 在数组中创建1000点
timer = new Timer(2000); // 创建一个每两秒运转一次的计时器

timer.start(); // 开启计时器
}

draw()内,对象们呼叫它们各自的方法。再次,我们只是将先前各部分的代码按顺序粘入。
Catcher catcher; // 一个容器对象
Timer timer; // 一个计时器对象
Drop[] drops; // 一个雨滴对象数组
int totalDrops = 0; // 雨滴总数

void setup() {
size(400,400);
catcher = new Catcher(32); // 以半径32创建容器
drops = new Drop[1000]; // 在数组中创建1000点
timer = new Timer(2000); // 创建一个每两秒运转一次的计时器

timer.start(); // 开启计时器
}

void draw() {
background(255);

// 来自6.2.1. 容器
catcher.setLocation(mouseX,mouseY); // 设置容器位置
catcher.display(); // 显示容器

// 来自6.2.3. 计时器
// 检查计时器
if (timer.isFinished()) {
println( ” 2 seconds have passed! ” );
timer.start();
}

// 来自6.2.4. 雨滴
// 初始化一滴雨
drops[totalDrops] = new Drop();
// 递增totalDrops
totalDrops ++ ;

// 如果到达数组最末
if (totalDrops >= drops.length) {
totalDrops = 0; // 重启
}

// 移动并显示所有雨滴
for (int i = 0; i < totalDrops; i ++ ) {
drops[i].move();
drops[i].display();
}
}

接下来就是有机的整合这几个部分。 比如说,我们在每2秒之后应仅产生一滴新的雨滴(遵照计时器的isFinished( )函数)
// 检查计时器
if (timer.isFinished()) {
// 处理雨滴
// 初始化一滴雨
drops[totalDrops] = new Drop();
// 递增totalDrops
totalDrops++ ;
// 如果到达数组最末
if (totalDrops > = drops.length) {
totalDrops = 0; // 重启
}
timer.start();
// 对象协同工作!这里,如果计时器结束,一个雨滴对象将被添加(通过递增”totalDrops”)
}

我们同时需要找到什么时候容器与雨滴相交。在之前的例子中,我们这样测试相交:
boolean intersecting = ball1.intersect(ball2);
if (intersecting) {
println( “The circles are intersecting!”);
}

我们在这能做同样的尝试,呼叫Catcher类内的intersec()函数并将之传递给系统内的每一滴雨,让它们消失。这段代码假设caught()函数可以完成这项工作:
// 移动并显示所有雨滴
for (int i = 0; i < totalDrops; i++ ){
drops[i].move();
drops[i].display();
// 对象协同工作!这里,Catcher对象检测自己是否与雨滴数组内的任何雨滴对象相交
if (catcher.intersect(drops[i])) {
drops[i].caught();
}
}

我们的容器类最初并不包含intersect()函数,雨滴也不包含caught()。因此这些便是我们需要在相交程序内新增的代码部分。

intersect()的加入并不困难,因为我们之前就已经做出了,所以我们只需将之前的代码拷入Catcher类里(将引数由Ball对象换为Drop对象)
// 一个基于是否相交返回真或假的函数
boolean intersect(Drop d) {
// 计算距离
float distance = dist(x,y,d.x,d.y);
// 比较距离与两半径之和
// 除了呼叫函数,我们还可以用点语法将变量插入一个对象
if (distance < r + d.r) {
return true;
} else {
return false;
}
}

当一滴雨被捕捉到,我们要设置其位置远离萤幕并通过将其速度设为0以停止其运动。尽管之前我们并没有事先在相交环节实现这个功能,但现在来做也很简单。
// 如果一滴雨被捕捉到
void caught() {
speed = 0; // 通过将其速度设为0以停止其运动
y = –1000; // 设置其位置远离萤幕
}

好了,,,全部的代码见这里(真鸡巴长- -b)

7. 本章的要点并非教你如何制作一个捉雨游戏,而是一种解决问题的方法——有了一个主意,将它打散为部分,为那些部分描绘伪码,并每次实现它们的一小部分。

记住熟悉这样的流程需要时间和练习,这很重要。每个人在初学编程的时候都会在其间挣扎。

在我们继续探索这本书剩下部分之前,让我们花点时间想一下我们学到的。在之前的部分,我们涵盖了编程的所有基础部分:
◎ 数据——以变量和数组的形式
◎ 控制流——以条件式和循环的形式
◎ 组织——以对象和函数的形式

这些概念不仅适用于processing,同时也适用于所有编程语言和环境,比如C++ , Actionscript和服务器端编程语言例如PHP。语法可能改变,但概念不会。

Processing:算法(上)

1. 回顾之前的学习。以Zoog为例,他教会了我们用p5自带库绘制基础的形状。从那开始,Zoog升级为可以与鼠标互动,通过变量自动移动,运用条件式变化方向,用循环扩展他的身体,通过函数组织他的代码,将他的数据和功能封装进入一个对象,最后通过数组复制他自己。学到现在,我们应该停下来一会,想想我们学到的然后如何使用它们得到我们想要的结果。我们的想法是什么?变量、条件式、循环、函数、对象和数组,它们能如何来帮助我们?

2. 在之前的学习中,我们把所有精力集中在“一种特性”的编程例子上。Zoog可以并且只会抖动。Zoog不会忽然开始跳起来,并且Zoog通常都是一个人,从不在路上与其他外星生物互动。当然,我们可以将之前那些例子带向更远,但现在更重要的是,抓住基本的机能(functionality),只有这样我们才能真正学到基本原理(fundamentals)。

3. 本课我们的目标主要是示范一个大型的项目如何由许多“单一特性”的小程序构成。你,程序员,应当从全局出发,但必须学会如何将它打散为不同独立的部分以成功执行你的预期。

3.1. 我们的步骤
1)想法——从一个想法开始。
2)部分——将这个想法打散为小块。
a. 算法伪码——以伪码的形式日出各个部分的算法
b. 算法代码——用代码执行算法。
c. 对象——将与这个算法有关的数据和功能建入一个类。
3)整合——将第二步得到的所有类整合为一个更大的算法。

4. 一个算法就是一个解决问题的程序或公式。在计算机编程中,一个算法就是要求执行一项任务的步骤。我们之前所有的例子都包括一个算法。

5. 通常意义上,编程被认为按以下的程序运作:1)发展一个想法,2)设计出一套实现这个想法的算法,3)写出实现那个算法的代码。再细分的话,编程的程序就是:1)发展一个想法,2)将那个想法分解为多个更小的可管理的部分,3)为每个部分设计出算法,4)为每个部分写代码,5)为所有部分一同设计出算法,6)整合各部分的代码。

继续阅读Processing:算法(上)

Minim:控制器

[ javadoc | 范例 ]

像之前的章节里提到的,JavaSound使用Line在你的系统和程序间传递音频。每条线路都可以通过它进行声相、平衡、增益和音量调节。优点是你无须在你的合成类里执行以上的控制,但缺点是在回放音频时,只有当你的软件接到采样后这些控制才会起作用。这意味着你在播放一个立体声音频时,即使将平衡调至极右,你也不会在你的采样中看到任何差别。换句话说,当你以为左声道的值为0的时候,它仍将保存原始音频文件左声道内的一起实际存在的东西。另外,在监听音频输入的时候,设置一个控制将被反映入采样缓冲器中,因为它将在你的软件收到音频之前起效。

打印控制

并非所有控制在所有线路上都可用。你可以使用方法printControls打印出一个控制器上可用的控制,同时包括那些控制的范围。

示范代码在线范例

import ddf.minim.*;

Minim minim;
AudioOutput out;

void setup()
{
  size(512, 200);
  minim = new Minim(this);
  out = minim.getLineOut();
  out.printControls();
}

void draw()
{
  background(0);
}

void stop()
{
  // 日完记得关闭Minim音频类
  out.close();
  minim.stop();

  super.stop();
}

询问控制

Controller提供方法hasControl,这样你可以在尝试使用一个控制前测定它是否存在。hasControl的引数为Control.Type。控制器包括三个静态成员(member),他们是BALANCE, GAIN, PAN, MUTE, SAMPLE_RATE和VOLUME。

示范代码在线范例

import ddf.minim.*;

Minim minim;
AudioOutput out;

void setup()
{
  size(512, 200);
  minim = new Minim(this);
  out = minim.getLineOut();

  textFont(createFont("Arial", 12));
}

void draw()
{
  background(0);

  if ( out.hasControl(Controller.PAN) )
  {
    text("The output has a pan control.", 5, 15);
  }
  else
  {
    text("The output doesn't have a pan control.", 5, 15);
  }

  if ( out.hasControl(Controller.VOLUME) )
  {
    text("The output has a volume control.", 5, 30);
  }
  else
  {
    text("The output doesn't have a volume control.", 5, 30);
  }

  if ( out.hasControl(Controller.SAMPLE_RATE) )
  {
    text("The output has a sample rate control.", 5, 45);
  }
  else
  {
    text("The output doesn't have a sample rate control.", 5, 45);
  }

  if ( out.hasControl(Controller.BALANCE) )
  {
    text("The output has a balance control.", 5, 60);
  }
  else
  {
    text("The output doesn't have a balance control.", 5, 60);
  }

  if ( out.hasControl(Controller.MUTE) )
  {
    text("The output has a mute control.", 5, 75);
  }
  else
  {
    text("The output doesn't have a mute control.", 5, 75);
  }

  if ( out.hasControl(Controller.GAIN) )
  {
    text("The output has a gain control.", 5, 90);
  }
  else
  {
    text("The output doesn't have a gain control.", 5, 105);
  }
}

void stop()
{
  // 日完记得XXXXXX
  out.close();
  minim.stop();

  super.stop();
}

获取和设置控制器

当你知道了你可以使用什么控制后,你可以使用适当的方法get(获取)和set(设置)操作它们。

setBalance(float value)
getBalance()
setGain(float value)
getGain()
setPan(float value)
getPan()
setVolume(float value)
getVolume()

平衡的范围是-1~1,增益常从-80~6,声相也是从-1~1,音量的范围作者说他自己也不知道。。。从所有的使用上,增益基本上等同于音量。以下是操作一个输出的增益的例子。其他控制的例子可以在这里找到。

示范代码在线范例

import ddf.minim.*;
import ddf.minim.signals.*;

Minim minim;
AudioOutput out;
Oscillator  osc;
WaveformRenderer waveform;

void setup()
{
  size(512, 200);
  minim = new Minim(this);
  out = minim.getLineOut();

  // see the example AudioOutput >> SawWaveSignal for more about this class
  osc = new SawWave(100, 0.2, out.sampleRate());
  // see the example Polyphonic >> addSignal for more about this
  out.addSignal(osc);

  waveform = new WaveformRenderer();
  // see the example Recordable >> addListener for more about this
  out.addListener(waveform);

  textFont(createFont("Arial", 12));
}

void draw()
{
  background(0);
  // see waveform.pde for more about this
  waveform.draw();

  if ( out.hasControl(Controller.GAIN) )
  {
    // 将鼠标位置映射到可听到的增益范围内
    float val = map(mouseX, 0, width, 6, -48);
    // 如果一个增益控制不可用,这里将什么都不做
    out.setGain(val);
    // 如果一个增益控制不可用,这里将输出0
    text("The current gain is " + out.getGain() + ".", 5, 15);
  }
  else
  {
    text("The output doesn't have a gain control.", 5, 15);
  }
}

void stop()
{
  // 日完记得XXXXX
  out.close();
  minim.stop();

  super.stop();
}

变化控制

因为反映的是一系列连续值的范围,所以所有这些控制都被称为浮点控制。Controller为持续改变这些控制之一的值提供方法。这被称为变化(shifting)。shifting的方法如下:

shiftBalance(float from, float to, int ms)
shiftGain(float from, float to, int ms)
shiftPan(float from, float to, int ms)
shiftVolume(float from, float to, int ms)

很明显的,from是一个值开始的点,to是目标值,ms是以毫秒计的shift作用时间。

示范代码在线范例

import ddf.minim.*;
import ddf.minim.signals.*;

Minim minim;
AudioOutput out;
WaveformRenderer waveform;
SawWave saw;

void setup()
{
  size(512, 200);
  minim = new Minim(this);
  out = minim.getLineOut();

  waveform = new WaveformRenderer();
  // see the example Recordable >> addListener for more about this
  out.addListener(waveform);

  // see the example AudioOutput >> SawWaveSignal for more about this
  saw = new SawWave(100, 0.2, out.sampleRate());
  // see the example Polyphonic >> addSignal for more about this
  out.addSignal(saw);

  textFont(createFont("Arial", 12));
}

void draw()
{
  background(0);
  // see waveform.pde for more about this
  waveform.draw();

  if ( out.hasControl(Controller.PAN) )
  {
    text("The current pan value is " + out.getPan() + ".", 5, 15);
  }
  else
  {
    text("The output doesn't have a pan control.", 5, 15);
  }

  if ( out.hasControl(Controller.VOLUME) )
  {
    text("The current volume value is " + out.getVolume() + ".", 5, 30);
  }
  else
  {
    text("The output doesn't have a volume control.", 5, 30);
  }

  if ( out.hasControl(Controller.BALANCE) )
  {
    text("The current balance value is " + out.getBalance() + ".", 5, 45);
  }
  else
  {
    text("The output doesn't have a balance control.", 5, 45);
  }

  if ( out.hasControl(Controller.GAIN) )
  {
    text("The current gain value is " + out.getGain() + ".", 5, 60);
  }
  else
  {
    text("The output doesn't have a gain control.", 5, 60);
  }
}

void keyReleased()
{
  if ( key == 'v' ) out.shiftVolume(0, 1, 2000);
  if ( key == 'g' ) out.shiftGain(-40, 0, 2000);
  if ( key == 'b' ) out.shiftBalance(-1, 1, 2000);
  if ( key == 'p' ) out.shiftPan(1, -1, 2000);
}

void stop()
{
  // always close Minim audio classes when you are finished with them
  out.close();
  minim.stop();

  super.stop();
}

静音

如下

isMuted()
mute()
unmute()

再次,如果静音不是一个可用的控制,静音和非静音将不起任何作用,并会在PDE控制台输出错误信息。

示范代码在线范例

import ddf.minim.*;
import ddf.minim.signals.*;

Minim minim;
AudioOutput out;
WaveformRenderer waveform;
SawWave saw;

void setup()
{
  size(512, 200);
  minim = new Minim(this);
  out = minim.getLineOut();

  waveform = new WaveformRenderer();
  // see the example Recordable >> addListener for more about this
  out.addListener(waveform);

  // see the example AudioOutput >> SawWaveSignal for more about this
  saw = new SawWave(100, 0.2, out.sampleRate());
  // see the example Polyphonic >> addSignal for more about this
  out.addSignal(saw);

  textFont(createFont("Arial", 12));
}

void draw()
{
  background(0);
  // see waveform.pde for more about this
  waveform.draw();

  if ( out.hasControl(Controller.MUTE) )
  {
    if (mousePressed)
    {
      out.mute();
    }
    else
    {
      out.unmute();
    }
    if ( out.isMuted() )
    {
      text("The output is muted.", 5, 15);
    }
    else
    {
      text("The output is not muted.", 5, 15);
    }
  }
  else
  {
    text("The output doesn't have a mute control.", 5, 15);
  }
}

void stop()
{
  // always close Minim audio classes when you are finished with them
  out.close();
  minim.stop();

  super.stop();
}

直接使用一个控制

使用控制更直接的方式是使用Controller的方法getControls。这个方法返回Control对象的数组,你可以使用方法getType测定一个控制的类型。

示范代码在线范例

import ddf.minim.*;
import ddf.minim.signals.*;
import javax.sound.sampled.Control;

Minim minim;
AudioOutput out;
Control[] controls;

void setup()
{
  size(512, 200);
  minim = new Minim(this);
  out = minim.getLineOut();
  controls = out.getControls();

  textFont(createFont("Arial", 12));
}

void draw()
{
  background(0);

  for ( int i = 0; i < controls.length; i++ )
  {
    text("Control " + (i+1) + " is a " + controls[i].toString() + ".", 5, 15 + i*15);
  }
}

void stop()
{
  // always close Minim audio classes when you are finished with them
  out.close();
  minim.stop();

  super.stop();
}

所有这些方法都返回适当的FloatControl。以下是使用pan()而不是setPan进入声相控制的例子:

示范代码在线范例

import ddf.minim.*;
import ddf.minim.signals.*;

Minim minim;
AudioOutput out;
Oscillator  osc;
WaveformRenderer waveform;

void setup()
{
  size(512, 200);
  minim = new Minim(this);
  out = minim.getLineOut();

  // see the example AudioOutput >> SawWaveSignal for more about this class
  osc = new SawWave(100, 0.2, out.sampleRate());
  // see the example Polyphonic >> addSignal for more about this
  out.addSignal(osc);

  waveform = new WaveformRenderer();
  // see the example Recordable >> addListener for more about this
  out.addListener(waveform);

  textFont(createFont("Arial", 12));
}

void draw()
{
  background(0);
  // see waveform.pde for more about this
  waveform.draw();

  if ( out.hasControl(Controller.PAN) )
  {
    // map the mouse position to the range of the pan
    float val = map(mouseX, 0, width, out.pan().getMinimum(), out.pan().getMaximum());
    out.pan().setValue(val);
    text("The current pan is " + out.pan().getValue() + ".", 5, 15);
  }
  else
  {
    text("There is no pan control for this output.", 5, 15);
  }
}

void stop()
{
  // always close Minim audio classes when you are finished with them
  out.close();
  minim.stop();

  super.stop();
}

要细讲FloatControl的所有方法实在太多,所以还是仔细读读javadoc或者研究一下在线范例们吧。

FloatControl

Minim:AudioSource(音源)

[ javadoc | 范例 ]

AudioSource定义了三个AudioBuffer成员,同时也可执行Recordable和Effectable界面。你无法直接创建一个AudioSource,它仅仅是为AudioPlayer, AudioSample, AudioOuput和AudioInput提供普通功能而存在。

采样缓冲器

三个采样缓冲器被命名为left, right和mix。它们持续更新左通道,右通道及左右混合声道的音源。即使在播放单声道音频时,三个通道都是可用的并且包含同样的采样。每个采样缓冲器都是一个AudioBuffer对象。

AudioBuffer: Get和Size

两个你在AudioBuffer中使用最频繁的方法是get(int i)和size()。方法size返回缓冲器的长度。方法get返回采样缓冲器第i个采样的浮点值,这个值将会在-1~1的范围内。因此,这常被认为是一个标准化的浮点采样。一个采样即对一个音源在某一时刻的振幅测量值。要听见一个声音,振幅必须时刻处于变化。最简单的变化辨识方波(square wave)。方法get并不做任何范围检查。因此,如果你询问一个位置小于零或大等于size返回值的采样,你将会得到一个ArrayOutOfBounds的错误。下例示范了如何使用get和size来为一个音频信号绘制波形。

范例在线看

import ddf.minim.*;

Minim minim;
AudioPlayer groove;

void setup()
{
  size(512, 200, P3D);
  minim = new Minim(this);
  groove = minim.loadFile("groove.mp3");
  groove.loop();
}

void draw()
{
  background(0);
  stroke(255);
  // 我们用50乘以值才能看到波形
  for ( int i = 0; i < groove.bufferSize() - 1; i++ )
  {
    float x1 = map(i, 0, groove.bufferSize(), 0, width);
    float x2 = map(i+1, 0, groove.bufferSize(), 0, width);
    line(x1, height/4 - groove.left.get(i)*50, x2, height/4 - groove.left.get(i+1)*50);
    line(x1, 3*height/4 - groove.right.get(i)*50, x2, 3*height/4 - groove.right.get(i+1)*50);
  }
}

void stop()
{
  // 记得关闭音频类
  groove.close();
  // 记得停止Minim
  minim.stop();

  super.stop();
}

AudioBuffer:Level(水平)

AudioBuffer的方法level返回当前缓冲器的音量水平。这个值将始终处于0~1之间,但你可能会发现返回的值常比你的预期要小。

范例在线看

import ddf.minim.*;
import ddf.minim.signals.*;

Minim minim;
AudioPlayer groove;

void setup()
{
  size(200, 200, P3D);
  minim = new Minim(this);
  groove = minim.loadFile("groove.mp3");
  groove.loop();
  rectMode(CORNERS);
}

void draw()
{
  background(0);
  fill(255);
  // 绘制当前左右采样缓冲器的音量水平
  // level()返回一个0~1之间的值, 因此我们将它放大
  rect(0, height, width/2, height - groove.left.level()*1000);
  rect(width/2, height, width, height - groove.right.level()*1000);
}

void stop()
{
  // 记得关闭音频类
  groove.close();
  // 记得停止Minim
  minim.stop();

  super.stop();
}

AudioBuffer: toArray(到数组)

第四个也是最后一个AudioBuffer可用的方法是toArray()。这个方法以一个浮动数组的形式返回一个缓冲器内值的副本。toArray返回的浮动数组长度将一直与缓冲器的大小相同。数组内的值也将一直在-1~1的范围内变化,除非你使用了AudioOutput的信号混合产生一个超过这个范围的采样值。如果那样的话你必须注意,因为这会产生声音的失真。你可以用toArray为在AudioBuffer内的音频绘制波形,并且这是这么做的首选方法。这是由于线路方面的原因。当有一个新的采样缓冲时,实际的音频输入输出发生在他自己的线路上并最后返回总线(你的sketch)。因此,当使用get来绘制波形的时候,在绘制波形的途中,在缓冲器中的采样很有可能已经改变了,这将造成一个看起来并不连续的波形。而当你使用toArray的时候,你给的是当前缓冲器里内容的副本并且是有保障的,因此在整个过程中不会出现采样的改变。

范例在线看

import ddf.minim.*;

Minim minim;
AudioPlayer groove;

void setup()
{
  size(512, 200, P3D);
  minim = new Minim(this);
  groove = minim.loadFile("groove.mp3");
  groove.loop();
}

void draw()
{
  background(0);
  stroke(255);
  float[] left = groove.left.toArray();
  float[] right = groove.right.toArray();
  // 我们仅循环至left.length - 1 因为我们正进入循环内i+1的目录
  for ( int i = 0; i < left.length - 1; i++ )
  {
    float x1 = map(i, 0, groove.bufferSize(), 0, width);
    float x2 = map(i+1, 0, groove.bufferSize(), 0, width);
    // 我们将这些值乘以50以更好的看到波形
    line(x1, height/4 - left[i]*50, x2, height/4 - left[i+1]*50);
    line(x1, 3*height/4 - right[i]*50, x2, 3*height/4 - right[i+1]*50);
  }
}

void stop()
{
  // 记得关闭音频类
  groove.close();
  // 记得停掉Minim
  minim.stop();

  super.stop();
}

关闭一个AudioSource

当你用完一个对象后,你应到呼叫close来关闭Minim里的全部音频输入输出。这使得用来回放/监听的线路完全的停止并释放那个线路占用的资源。你可以在除方法stop以外的其他地方停止一个音源,就像你在为groove配置一个新的播放器之前你必须呼叫close。如果你不这么做,上一个播放器的线路仍将继续执行而且你没有办法使其停止。

Minim:层级

Minim主要基于两个类建立:Controller 和 AudioSource,并由四个interface(界面)定义:Playable, Effectable, Polyphonic, 和 Recordable。在Java中,一个界面被定义为一个仅含函数定义的类。即,它仅含有一堆功能而并不实际执行其中任一。

在JavaSound里,当一个音频在你的系统和软件间传送时,他都会经过一条Line(线),这是JavaSound API里定义的一个界面。一条线可以控制例如声相、音量和平衡等,这些控制可以实时改变声音。你可以经由基础类Controller、AudioSnippet以及源自Controller的AudioSource来获取这些控制。

AudioSource定义了三个AudioBuffer:left, right, 和 mix。这三个采样缓冲器(一个采样缓冲器就是一个浮动数组)包含左通道、右通道和左右混合通道。AudioPlayer, AudioOutput, AudioInput, 和 AudioSample都来自AudioSource,这意味着它们都像数据成员般继承这些缓冲器,同时意味着它们都能提供进入采样的通路。

AudioSource同样执行四个界面中的两个:Recordable(可录制) 和 Effectable(可加效果)。因此,上述四个来自AudioSource的类同样可以Recordable 和 Effectable。

最后,因为方法遗传的层叠性,这四个来自AudioSource的类同样继承Controller。这么想,比如说:AudioPlayer是一个AudioSource同时也是一个Controller。因此,你能用AudioSource或Controller做的所有事,同样可以用AudioPlayer来完成。

Minim:Minim

[ javadoc | 范例 ]

Minim是你正在阅读的音频库的名字。Minim是在能提供一堆方法(method)给你获取系统音频资源的库里的一个类。要使用它,你必须在setup()之前申明一个Minm的变量,而后例示一个Minim对象。为了使Minm能够辨识一些东西并能够进入你sketch的data文件夹,你必须这么做。

示范代码

import ddf.minim.*;

Minim minim;

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

  minim = new Minim(this);
}

void draw()
{
}

获取Minim的音频资源

你可以用Minim做四件事:播放一个音频文件,播放合成音频,监听音频输入,向硬盘内录制音频。所有这些都是由库内不同的类操作的,这些类的范例通过呼叫Minim适当的方法而获得。

载入一个音频文件

有三个不同的类可用来播放一个音频文件,每种都用来配合特定的回放类型。接下来的课程会详尽的讲解每个类,现在你只需要知道他们是:AudioSnippet, AudioSample, 和 AudioPlayer。你通过呼叫如下Minim方法创建它们:

loadSnippet(String filename)
loadSample(String filename)
loadSample(String filename, int bufferSize)
loadFile(String filename)
loadFile(String filename, int bufferSize)

loadSnippet 返回 AudioSnippet,loadSample 返回 AudioSample, loadFile 返回 AudioPlayer。在任何情况下,filename(文件名)可以是你sketch里data文件夹下的音频文件的绝对路径,也可以是网路上的音频文件链接。bufferSize是你希望的缓冲器大小,这个数字越小,音频的延迟越少。

示范代码

import ddf.minim.*;

Minim minim;
AudioPlayer in;

void setup()
{
  size(512, 200);
  // 例示一个Minim对象
  minim = new Minim(this);
  // 载入一个文件,默认采样率为1024
  in = minim.loadFile("music.mp3");
  // 播放这个文件
  in.play();
}

void draw()
{
  // 做爱做的事
}

void stop()
{
  // 别忘记关闭音频输入输出类
  in.close();
  // 别忘记停止你的Minim对象
  minim.stop();

  super.stop();
}

获得一个AudioOutput(音频输出)

Minim为回放合成音频提供AudioOutput类。这是实时经由你程序创建并反馈到扬声器的音频。AudioOutput支持单声道及立体声,并且可指定你需要的合成类型。方法如下:

getLineOut()
getLineOut(int type)
getLineOut(int type, int bufferSize)
getLineOut(int type, int bufferSize, float sampleRate)
getLineOut(int type, int bufferSize, float sampleRate, int bitDepth)

type指出你想要单声道还是立体声。你应当使用Minim.MONO或Minim.STEREO作为这个引数的值,这个引数的默认值为Minim.STEREO。bufferSize如上,即你缓冲器的大小。sampleRate是你合成音频的采样率,默认值为44100(CD音频质量)。bitDepth是你合成音频的比特率,目前仅支持8和16两个值,默认值为16(CD音频质量)。注意有时你填入的值将超出你的设备允许范围,这时,这个方法将会返回null,发生这种情况并不意味着你完全无法获得一个输出,而只是表明你想要的是一个无法实现的配置。

示范代码

import ddf.minim.*;
import ddf.minim.signals.*;

Minim minim;
AudioOutput out;
SineWave sine;

void setup()
{
  size(512, 200);
  // 例示一个Minim对象
  minim = new Minim(this);
  // 通过Minim获得一个线性输出, 
  // 默认采样率为44100,采样深度16
  out = minim.getLineOut(Minim.STEREO, 512);
  // 创建一个振荡器, 频率440 Hz, 振幅0.5, 
  // 采样率44100以符合线性输出
  sine = new SineWave(440, 0.5, 44100);
  // 将振荡器加到输出上
  out.addSignal(sine);
}

void draw()
{
  // 做爱做的事
}

void stop()
{
  // always closes audio I/O classes
  out.close();
  // always stop your Minim object
  minim.stop();

  super.stop();
}

获得一个AudioInput(音频输入)

Minim为监听用户当前录音源提供了AudioInput类。目前我们无法指定这个输入的来源,比如麦克或线性输入。

getLineIn()
getLineIn(int type)
getLineIn(int type, int bufferSize)
getLineIn(int type, int bufferSize, float sampleRate)
getLineIn(int type, int bufferSize, float sampleRate, int bitDepth)

type指出你想要单声道还是立体声。你应当使用Minim.MONO或Minim.STEREO作为这个引数的值,这个引数的默认值为 Minim.STEREO。bufferSize如上,即你缓冲器的大小。sampleRate是你合成音频的采样率,默认值为44100(CD音频质 量)。bitDepth是你合成音频的比特率,目前仅支持8和16两个值,默认值为16(CD音频质量)。注意有时你填入的值将超出你的设备允许范围,这 时,这个方法将会返回null,发生这种情况并不意味着你完全无法获得一个输出,而只是表明你想要的是一个无法实现的配置。

示范代码

import ddf.minim.*;

Minim minim;
AudioInput in;

void setup()
{
  size(512, 200);
  // instatiate a Minim object
  minim = new Minim(this);
  // get a line out from Minim, 
  // default sample rate is 44100, bit depth is 16
  in = minim.getLineIn(Minim.STEREO, 512);
}

void draw()
{
  // do whatever you are going to do
}

void stop()
{
  // 别忘记关闭音频I/O类
  in.close();
  // 别忘记停止你的Minim对象
  minim.stop();

  super.stop();
}

创建一个AudioRecorder(音频录制机)

Minim为录制音频数据到硬盘提供AudioRecorder类。细节的讨论将后续说明,以下是获取一个AudioRecorder的方法:

createRecorder(Recordable source, String filename, boolean buffered)

source是一个你希望作为录音源的可记录对象。filename是要保存的音频文件的名字,包括扩展名。buffered表明你希望或不希望在录音过程中使用缓冲器。

示范代码

import ddf.minim.*;

Minim minim;
AudioInput in;
AudioRecorder fout;

void setup()
{
  size(512, 400);

  minim = new Minim(this);

  // 获得一个立体声线性输入: 缓冲器512
  in = minim.getLineIn(Minim.STEREO, 512);
  // 获取一个录音机 
  // 并使用缓冲录音的方式将声音录入指定文件名及类型的文件
  fout = minim.createRecorder(in, "myrecording.wav", true);
}

void draw()
{
  // 做爱做的事
}

void keyReleased()
{
  // 可以设置控制录音起始的键盘按键
  // 然后存至硬盘
}

void stop()
{
  // always close audio I/O classes
  in.close();
  // always stop your Minim object
  minim.stop();

  super.stop();
}

设置系统混音器(Mixer)

Javasound(Java声音)通过使用一个叫做Mixer(混音器)的对象提供进入电脑系统特定输入、输出口的能力。你可以在创建资源之前设置输入输出混音器来指定特定的输入或输出。AudioPlayer, AudioSnippet, AudioSample, 和 AudioOuput都使用输出混音器来获得音频输出。AudioInput则使用输入混音器。例如,如果你有一块多通道声卡,你可以为你的音频输出指定它特定的输出口。

示范代码在线示例

import ddf.minim.*;
// 需要SineWave信号包
import ddf.minim.signals.*;
import controlP5.*;
// 为使用Mixer 和 Mixer.Info对象,我们需要导入这个
import javax.sound.sampled.*;

Minim minim;
AudioOutput out;
// 一个展示整个音频系统混音器的info对象的数组。
// 我们用它组装我们的混音器下拉菜单,并让用户选择想要的混音器。
Mixer.Info[] mixerInfo;

// 给我们输出的一个信号
SineWave sine;

ControlP5 gui;

void setup()
{
  size(512, 275);

  minim = new Minim(this);
  gui = new ControlP5(this);

  ScrollList mixers = gui.addScrollList("Mixers", 10, 10, 475, 280);
  mixers.setLabel("Choose A Mixer");

  mixerInfo = AudioSystem.getMixerInfo();

  for(int i = 0; i < mixerInfo.length; i++)
  {
    controlP5.Button b = mixers.addItem("item"+i, i);
    b.setLabel(mixerInfo[i].getName());
  }

  sine = new SineWave(220, 0.3, 44100);

}

void draw()
{
  background(0);

  //gui.draw();

  if ( out != null )
  {
    stroke(255);
    // 绘制波形
    for(int i = 0; i < out.bufferSize() - 1; i++)
    {
      line(i, 50 + out.left.get(i)*50, i+1, 50 + out.left.get(i+1)*50);
      line(i, 150 + out.right.get(i)*50, i+1, 150 + out.right.get(i+1)*50);
    }
  }
}

public void controlEvent(ControlEvent theEvent)
{
  int mixerIndex = (int)theEvent.controller().value();

  println("User chose " + theEvent.controller().label());
  println("Using mixer info " + mixerInfo[mixerIndex].getName());

  Mixer mixer = AudioSystem.getMixer(mixerInfo[mixerIndex]);

  minim.setOutputMixer(mixer);

  if ( out != null )
  {
    out.close();
  }

  out = minim.getLineOut(Minim.STEREO);

  if ( out != null )
  {
    out.addSignal(sine);
  }
}

void stop()
{
  // 完事后记得关闭Minim音频类
  if ( out != null )
  {
    out.close();
  }
  minim.stop();

  super.stop();
}

同样的,你可以指定你声卡特定的输入:

示范代码在线示例

import ddf.minim.*;
import controlP5.*;
// need to import this so we can use Mixer and Mixer.Info objects
import javax.sound.sampled.*;

Minim minim;
AudioInput in;
// an array of info objects describing all of 
// the mixers the AudioSystem has. we'll use
// this to populate our gui scroll list and
// also to obtain an actual Mixer when the
// user clicks on an item in the list.
Mixer.Info[] mixerInfo;

ControlP5 gui;

void setup()
{
  size(512, 275);

  minim = new Minim(this);
  gui = new ControlP5(this);

  ScrollList mixers = gui.addScrollList("Mixers", 10, 10, 475, 280);
  mixers.setLabel("Choose A Mixer");

  mixerInfo = AudioSystem.getMixerInfo();

  for(int i = 0; i < mixerInfo.length; i++)
  {
    controlP5.Button b = mixers.addItem("item"+i, i);
    b.setLabel(mixerInfo[i].getName());
  }

}

void draw()
{
  background(0);

  //gui.draw();

  if ( in != null )
  {
    stroke(255);
    // draw the waveforms
    for(int i = 0; i < in.bufferSize() - 1; i++)
    {
      line(i, 50 + in.left.get(i)*50, i+1, 50 + in.left.get(i+1)*50);
      line(i, 150 + in.right.get(i)*50, i+1, 150 + in.right.get(i+1)*50);
    }
  }
}

public void controlEvent(ControlEvent theEvent)
{
  int mixerIndex = (int)theEvent.controller().value();

  println("User chose " + theEvent.controller().label());
  println("Using mixer info " + mixerInfo[mixerIndex].getName());

  Mixer mixer = AudioSystem.getMixer(mixerInfo[mixerIndex]);

  if ( in != null )
  {
    in.close();
  }

  minim.setInputMixer(mixer);

  in = minim.getLineIn(Minim.STEREO);

}

void stop()
{
  // always close Minim audio classes when you are done with them
  if ( in != null )
  {
    in.close();
  }
  minim.stop();

  super.stop();
}