原文:
docs.oracle.com/javase/tutorial/reallybigindex.html
一个基本的打印程序
原文:
docs.oracle.com/javase/tutorial/2d/printing/printable.html
本节解释了如何创建一个基本的打印程序,显示打印对话框,并将文本“Hello World”打印到所选打印机。
打印任务通常由两部分组成:
-
作业控制 — 创建打印作业,将其与打印机关联,指定副本数量,并与用户打印对话框交互。
-
页面成像 — 将内容绘制到页面上,并管理跨页的内容(分页)。
首先创建打印作业。表示打印作业和大多数其他相关类的类位于java.awt.print
包中。
import java.awt.print.*;
PrinterJob job = PrinterJob.getPrinterJob();
接下来提供代码,通过实现Printable
接口将内容呈现到页面上。
class HelloWorldPrinter
implements Printable { ... }
...
job.setPrintable(new HelloWorldPrinter());
应用程序通常会显示打印对话框,以便用户可以调整各种选项,如副本数量、页面方向或目标打印机。
boolean doPrint = job.printDialog();
此对话框会一直显示,直到用户批准或取消打印。如果doPrint
变量为 true,则用户已经下达打印命令。如果doPrint
变量为 false,则用户取消了打印作业。由于显示对话框是可选的,返回的值纯粹是信息性的。
如果doPrint
变量为 true,则应用程序将通过调用PrinterJob.print
方法请求打印作业。
if (doPrint) {
try {
job.print();
} catch (PrinterException e) {
// The job did not successfully
// complete
}
}
如果发送作业到打印机时出现问题,将抛出PrinterException
。然而,由于PrinterJob.print
方法一旦作业发送到打印机就会返回,用户应用程序无法检测到纸张卡住或缺纸等问题。这个作业控制样板对于基本的打印使用已经足够。
Printable
接口只有一个方法:
public int print(Graphics graphics,
PageFormat pf, int page)
throws PrinterException;
PageFormat
类描述了页面方向(纵向或横向)及其大小和可成像区域,单位为 1/72 英寸。可成像区域考虑了大多数打印机的边距限制(硬件边距)。可成像区域是这些边距内的空间,在实践中通常进一步限制以留出页眉或页脚的空间。
page
参数是将要呈现的基于零的页码。
以下代码表示完整的Printable
实现:
import java.awt.print.*;
import java.awt.*;
public class HelloWorldPrinter
implements Printable {
public int print(Graphics g, PageFormat pf, int page)
throws PrinterException {
// We have only one page, and 'page'
// is zero-based
if (page > 0) {
return NO_SUCH_PAGE;
}
// User (0,0) is typically outside the
// imageable area, so we must translate
// by the X and Y values in the PageFormat
// to avoid clipping.
Graphics2D g2d = (Graphics2D)g;
g2d.translate(pf.getImageableX(), pf.getImageableY());
// Now we perform our rendering
g.drawString("Hello world!", 100, 100);
// tell the caller that this page is part
// of the printed document
return PAGE_EXISTS;
}
}
此示例的完整代码在HelloWorldPrinter.java
中。
将Graphics
实例发送到打印机基本上与将其呈现到屏幕相同。在这两种情况下,您需要执行以下步骤:
-
绘制测试字符串与描述绘制到
Graphics2D
的其它操作一样简单。 -
打印机图形具有更高的分辨率,这对大多数代码来说应该是透明的。
-
Printable.print()
方法由打印系统调用,就像Component.paint()
方法被调用来在显示器上绘制组件一样。打印系统会在页面 0、1、…等等调用Printable.print()
方法,直到print()
方法返回NO_SUCH_PAGE
。 -
print()
方法可能会在文档完成之前多次以相同的页面索引被调用。当用户指定了诸如多份拷贝和逐份选项等属性时,会应用此功能。 -
PageFormat 的可打印区域决定了剪切区域。可打印区域在计算分页或者如何跨打印页面展示内容时也很重要,因为页面断点是由每页能容纳多少内容决定的。
注意: 如果用户指定了不涉及特定页面索引的不同页面范围,那么对于某些页面索引,可能会跳过对
print()
方法的调用。
使用打印设置对话框
原文:
docs.oracle.com/javase/tutorial/2d/printing/dialog.html
传统上,用户希望看到页面设置和打印对话框。从打印对话框中,您可以选择打印机,指定要打印的页面,并设置副本数量。
当用户按下与打印命令相关的按钮或从打印菜单中选择项目时,应用程序会显示打印对话框。要显示此对话框,请调用PrinterJob
类的printDialog
方法:
PrinterJob pj = PrinterJob.getPrinterJob();
...
if (pj.printDialog()) {
try {pj.print();}
catch (PrinterException exc) {
System.out.println(exc);
}
}
...
如果用户点击确定按钮离开对话框,则此方法返回true
,否则返回false
。对话框中用户的选择受限于已设置到PrinterJob
的页面的数量和格式。
上述代码片段中的printDialog
方法打开一个本机打印对话框。PrintDialogExample.java
代码示例展示了如何显示跨平台打印对话框。
你可以通过使用页面设置对话框来更改包含在PageFormat
对象中的页面设置信息。
要显示页面设置对话框,请调用PrinterJob
类的pageDialog
方法。
PrinterJob pj = PrinterJob.getPrinterJob();
PageFormat pf = pj.pageDialog(pj.defaultPage());
页面设置对话框使用传递给pageDialog
的参数进行初始化。如果用户在对话框中点击确定按钮,则将根据用户的选择创建PageFormat
实例,然后返回。如果用户取消对话框,则pageDialog
将返回原始未更改的PageFormat
。
通常,Java 2D 打印 API 要求应用程序显示打印对话框,但有时可能可以在不显示任何对话框的情况下打印。这种类型的打印称为静默打印。在特定情况下可能会很有用,例如,当您需要打印特定数据库每周报告时。在其他情况下,始终建议在打印过程开始时通知用户。
打印多页文档
原文:
docs.oracle.com/javase/tutorial/2d/printing/set.html
您已经学会了如何使用Printable
接口打印单页文档。但是,文档通常不止一页。分页是识别文档中分页位置并相应打印的过程。
如果要打印多个图形图像,每页一个,使用页面索引来遍历这些页面,并在每页上打印一个。例如,如果几个图像在以下数组中表示:
BufferedImage[] images = new BufferedImage[10];
然后使用以下代码片段中显示的print()
方法:
public int print(Graphics graphics,
PageFormat pageFormat, int pageIndex)
throws PrinterException {
if (pageIndex < images.length) {
graphics.drawImage(images[pageIndex], 100, 100, null);
return PAGE_EXISTS;
} else {
return NO_SUCH_PAGE:
}
}
如果文档是连续的,应用程序必须计算每页可以容纳多少内容,并在该点分页。如果文本文档由许多行组成,则应用程序必须计算这些行中有多少可以完全适合一页。Point
类创建一个表示位置的点(x,y)
要计算单行文本的高度,请使用FontMetrics
类。
Font font = new Font("Serif", Font.PLAIN, 10);
FontMetrics metrics = graphics.getFontMetrics(font);
int lineHeight = metrics.getHeight();
PageFormat
参数描述了页面的可打印区域。特别是,要找到页面的垂直跨度,请使用以下代码片段:
double pageHeight = pageFormat.getImageableHeight();
使用以下代码片段计算一页上适合的行数和分页数:
int linesPerPage = ((int)pageHeight)/lineHeight);
int numBreaks = (textLines.length-1)/linesPerPage;
int[] pageBreaks = new int[numBreaks];
for (int b=0; b < numBreaks; b++) {
pageBreaks[b] = (b+1)*linesPerPage;
}
使用print()
方法计算以下原因的可打印区域:
-
文本测量取决于
FontRenderContext
,这在打印机图形返回的FontMetrics
对象中是隐含的,除了在print()
方法内部不可用。 -
直到打印发生,页面格式可能不会被揭示。因为如果用户在打印对话框中选择了横向模式,则需要考虑此设置。传递给
print()
方法的PageFormat
对象提供了此信息。
分页位置如下代码片段所示:
/* Draw each line that is on this page.
* Increment 'y' position by lineHeight
* for each line.
*/
int y = 0;
int start = (pageIndex == 0) ? 0 : pageBreaks[pageIndex-1];
int end = (pageIndex == pageBreaks.length)
? textLines.length : pageBreaks[pageIndex];
for (int line=start; line<end; line++) {
y += lineHeight;
g.drawString(textLines[line], 0, y);
}
如果一个文档包含 100 行,每页只能容纳 48 行,则应用程序将打印 3 页,每页在第 48 行和第 96 行之后分页。剩余的 4 行将打印在最后一页上。此示例的完整代码在PaginationExample.java
中。
PaginationExample
代码中使用了以下简化因素:
-
每页具有相同的高度。
-
使用相同的字体。
使用打印服务和属性
原文:
docs.oracle.com/javase/tutorial/2d/printing/services.html
从之前的课程中,您已经了解到 Java 2D 打印 API 支持页面成像,显示打印和页面设置对话框,并指定打印属性。 打印服务是任何打印子系统的另一个关键组件。
Java 打印服务(JPS)API扩展了当前的 Java 2D 打印功能,提供以下功能:
-
应用程序通过动态查询打印机功能来发现满足其需求的打印机。
-
应用程序扩展了包含在 JPS API 中的属性。
-
第三方可以通过服务提供者接口插入其自己的打印服务,打印不同格式,包括 Postscript、PDF 和 SVG。
Java 打印服务 API 由四个包组成:
javax.print
包为 Java 打印服务 API 提供了主要的类和接口。 它使客户端和服务器应用程序能够:
-
根据其功能发现和选择打印服务。
-
指定打印数据的格式。
-
将打印作业提交给支持要打印的文档类型的服务。
文档类型规范
DocFlavor
类表示打印数据的格式,例如 JPEG 或 PostScript。DocFlavor
格式由两部分组成:一个是 MIME 类型,另一个是表示类名称。 MIME 类型描述了格式,文档表示类名称指示文档如何传递给打印机或输出流。 应用程序使用DocFlavor
和属性集来查找具有属性集指定功能的打印机。 此代码示例演示了获取能够将 GIF 图像转换为 PostScript 的StreamPrintService
对象的StreamPrintServiceFactory
对象数组:
DocFlavor flavor = DocFlavor.INPUT_STREAM.GIF;
String psMimeType = DocFlavor.BYTE_ARRAY.
POSTSCRIPT.getMimeType();
StreamPrintServiceFactory[] psfactories =
StreamPrintServiceFactory.
lookupStreamPrintServiceFactories(
flavor, psMimeType);
属性定义
javax.print.attribute
和javax.print.attribute.standard
包定义了描述打印服务功能、指定打印作业要求以及跟踪打印作业进度的打印属性。
例如,如果您想使用 A4 纸张格式并打印文档的三份副本,则必须创建一组实现PrintRequestAttributeSet
接口的以下属性:
PrintRequestAttributeSet attr_set =
new HashPrintRequestAttributeSet();
attr_set.add(MediaSize.ISO_A4);
attr_set.add(new Copies(3));
然后,您必须将属性集传递给打印作业的print
方法,以及DocFlavor
。
打印服务发现
应用程序调用抽象类PrintServiceLookup
的静态方法来定位具有满足应用程序打印请求能力的打印服务。例如,为了打印一份双面文档的两份副本,应用程序首先需要找到具有双面打印能力的打印机:
DocFlavor doc_flavor = DocFlavor.INPUT_STREAM.PDF;
PrintRequestAttributeSet attr_set =
new HashPrintRequestAttributeSet();
attr_set.add(new Copies(2));
attr_set.add(Sides.DUPLEX);
PrintService[] service = PrintServiceLookup.
lookupPrintServices(doc_flavor,
attr_set);
API 的常见用法
总之,Java 打印服务 API 执行以下步骤来处理打印请求:
-
选择一个
DocFlavor
。 -
创建一组属性。
-
定位一个可以处理由
DocFlavor
和属性集指定的打印请求的打印服务。 -
创建一个封装了
DocFlavor
和实际打印数据的Doc
对象。 -
从打印服务获取由
DocPrintJob
表示的打印作业。 -
调用打印作业的
print
方法。
有关 Java 打印服务的更多信息,请参阅Java 2D 打印服务 API 用户指南。
打印用户界面的内容
原文:
docs.oracle.com/javase/tutorial/2d/printing/gui.html
另一个常见的打印任务是打印窗口或框架的内容,可以是全部内容,也可以是部分内容。窗口可能包含以下组件:工具栏、按钮、滑块、文本标签、可滚动文本区域、图像和其他图形内容。所有这些组件都是使用 Java 2D 打印 API 的以下方法打印的:
java.awt.Component.print(Graphics g);
java.awt.Component.printAll(Graphics g);
以下图表示一个简单的用户界面。
创建此用户界面的代码位于示例程序PrintUIWindow.java
中。
要打印此窗口,请修改之前打印文本或图像的示例中的代码。结果代码应如下所示:
public int print(Graphics g, PageFormat pf, int page)
throws PrinterException {
if (page > 0) {
return NO_SUCH_PAGE;
}
Graphics2D g2d = (Graphics2D)g;
g2d.translate(pf.getImageableX(), pf.getImageableY());
// Print the entire visible contents of a
// java.awt.Frame.
frame.printAll(g);
return PAGE_EXISTS;
}
注意: printAll
方法的调用是此示例与打印文本或图像示例之间的唯一区别。print(Graphics g)
方法反映了用于屏幕渲染的java.awt.Component.paint(Graphics g)
方法。使用print()
方法而不是paint()
方法,因为Components
类可能已经重写了print()
方法以不同方式处理打印情况。
printAll(Graphics g)
方法打印组件及其所有子组件。通常使用此方法打印对象,例如完整窗口,而不是单个组件。
Swing 组件中的打印支持
原文:
docs.oracle.com/javase/tutorial/2d/printing/swing.html
在前一节中展示的 PrintUIWindow.java
示例表明打印出的内容与屏幕上看到的完全相同。这种外观看起来是合理的。然而,如果一个窗口是可滚动的,那么当前滚动出视图的内容不会包含在打印输出中。这会在打印机上产生一个倾倒效果。当打印大型组件(如 Swing 表格或文本组件)时,这就成为一个特殊问题。组件可能包含许多行文本,这些文本在屏幕上并不都能完全可见。在这种情况下,以与屏幕显示一致的方式打印组件显示的内容。
要解决这个问题,Swing 表格和所有文本组件都具有打印功能。以下方法直接提供了 Java 2D 打印的使用:
-
javax.swing.JTable.print();
-
javax.swing.text.JTextComponent.print();
这些方法为它们的内容提供了完整的打印实现。应用程序不需要直接创建 PrinterJob
对象并实现 Printable
接口。调用这些方法会显示打印对话框,并根据用户的选择打印组件的数据。还有其他提供更多选项的方法。
课程:Java2D 高级主题
原文:
docs.oracle.com/javase/tutorial/2d/advanced/index.html
本课程向您展示如何使用Graphics2D
显示具有花哨轮廓和填充样式的图形,当它们被渲染时如何转换图形,将渲染限制在特定区域,并一般控制图形在被渲染时的外观。您还将学习如何通过组合简单形状来创建复杂的Shape
对象,以及如何检测用户何时点击显示的图形基元。这些主题在以下部分中讨论:
变换形状、文本和图像
本节向您展示如何修改默认变换,以便在渲染时对象被平移、旋转、缩放或倾斜。
裁剪绘图区域
你可以使用任何形状作为裁剪路径—即渲染发生的区域。
合成图形
本节介绍了AlphaComposite
支持的各种合成样式,并向您展示如何在Graphics2D
渲染上下文中设置合成样式。
控制渲染质量
本节描述了Graphics2D
支持的渲染提示,并向您展示如何在渲染质量和速度之间的权衡中指定您的偏好。
从几何原语构建复杂形状
本节向您展示如何使用Area
类在Shape
对象上执行布尔运算。
支持用户交互
本节向您展示如何在图形基元上执行点击检测。
变换形状、文本和图像
原文:
docs.oracle.com/javase/tutorial/2d/advanced/transforming.html
您可以在Graphics2D
上下文中修改变换属性,以在渲染时移动、旋转、缩放和剪切图形基元。变换属性由AffineTransform
类的实例定义。仿射变换是一种变换,如平移、旋转、缩放或剪切,在变换后平行线仍保持平行。
Graphics2D
类提供了几种方法来更改变换属性。您可以构造一个新的AffineTransform
并通过调用transform
来更改Graphics2D
的变换属性。
AffineTransform
定义了以下工厂方法,以便更容易构造新的变换:
-
getRotateInstance
-
getScaleInstance
-
getShearInstance
-
getTranslateInstance
或者,您可以使用Graphics2D
的一个变换方法来修改当前变换。当您调用这些便利方法之一时,生成的变换将与当前变换连接,并在渲染期间应用:
-
rotate
— 用于指定以弧度为单位的旋转角度 -
scale
— 用于指定x和y方向的缩放因子 -
shear
— 用于指定x和y方向的剪切因子 -
translate
— 用于指定x和y方向的平移偏移量
您还可以直接构造一个AffineTransform
对象,并通过调用transform
方法将其与当前变换连接。
drawImage
方法也被重载,允许您指定一个在渲染时应用于图像的AffineTransform
。在调用drawImage
时指定变换不会影响Graphics2D
的变换属性。
示例:变换
下面的程序与StrokeandFill
相同,但还允许用户在渲染选定对象时选择要应用的变换。
注意: 如果您看不到小程序运行,则需要安装至少Java SE Development Kit (JDK) 7版本。
Transform.java
包含此小程序的完整代码。
当从变换菜单中选择一个变换时,该变换将连接到AffineTransform
at
上:
public void setTrans(int transIndex) {
// Sets the AffineTransform.
switch ( transIndex ) {
case 0 :
at.setToIdentity();
at.translate(w/2, h/2);
break;
case 1 :
at.rotate(Math.toRadians(45));
break;
case 2 :
at.scale(0.5, 0.5);
break;
case 3 :
at.shear(0.5, 0.0);
break;
}
}
在显示与菜单选择对应的形状之前,应用程序首先从Graphics2D
对象中检索当前变换:
AffineTransform saveXform = g2.getTransform();
此变换将在渲染后恢复到Graphics2D
中。
在检索当前变换后,创建另一个AffineTransform
,toCenterAt
,使形状在面板中心渲染。at
AffineTransform
被连接到toCenterAt
上:
AffineTransform toCenterAt = new AffineTransform();
toCenterAt.concatenate(at);
toCenterAt.translate(-(r.width/2), -(r.height/2));
使用transform
方法将toCenterAt
变换连接到Graphics2D
变换上:
g2.transform(toCenterAt);
渲染完成后,使用setTransform
方法恢复原始变换:
g2.setTransform(saveXform);
**注意:**永远不要使用setTransform
方法将坐标变换连接到现有的变换上。setTransform
方法会覆盖Graphics2D
对象的当前变换,这可能会因其他原因而需要,比如在窗口中定位 Swing 和轻量级组件。执行变换的步骤如下:
-
使用
getTransform
方法获取当前变换。 -
使用
transform
、translate
、scale
、shear
或rotate
来连接一个变换。 -
执行渲染。
-
使用
setTransform
方法恢复原始变换。
绘制区域剪切
原文:
docs.oracle.com/javase/tutorial/2d/advanced/clipping.html
任何Shape
对象都可以用作限制将呈现的绘图区域的剪切路径。剪切路径是Graphics2D
上下文的一部分;要设置剪切属性,调用Graphics2D.setClip
并传入定义要使用的剪切路径的Shape
。可以通过调用clip
方法并传入另一个Shape
来缩小剪切路径;剪切设置为当前剪切和指定Shape
的交集。
示例:ClipImage
该示例通过动画剪切路径来显示图像的不同部分。
注意: 如果看不到小程序运行,请至少安装Java SE Development Kit (JDK) 7版本。
ClipImage.java
包含此小程序的完整代码。小程序需要clouds.jpg
图像文件。
剪切路径由椭圆和随机设置尺寸的矩形的交集定义。将椭圆传递给setClip
方法,然后调用clip
将剪切路径设置为椭圆和矩形的交集。
private Ellipse2D ellipse = new Ellipse2D.Float();
private Rectangle2D rect = new Rectangle2D.Float();
...
ellipse.setFrame(x, y, ew, eh);
g2.setClip(ellipse);
rect.setRect(x+5, y+5, ew-10, eh-10);
g2.clip(rect);
示例:Starry
也可以从文本字符串创建一个剪切区域。以下示例使用字符串The Starry Night创建一个TextLayout
。然后,获取TextLayout
的轮廓。TextLayout.getOutline
方法返回一个Shape
对象,并从该Shape
对象的边界创建一个Rectangle
。边界包含布局可以绘制的所有像素。将图形上下文中的颜色设置为蓝色,并绘制轮廓形状,如下图和代码片段所示。
FontRenderContext frc = g2.getFontRenderContext();
Font f = new Font("Helvetica", 1, w/10);
String s = new String("The Starry Night");
TextLayout textTl = new TextLayout(s, f, frc);
AffineTransform transform = new AffineTransform();
Shape outline = textTl.getOutline(null);
Rectangle r = outline.getBounds();
transform = g2.getTransform();
transform.translate(w/2-(r.width/2), h/2+(r.height/2));
g2.transform(transform);
g2.setColor(Color.blue);
g2.draw(outline);
接下来,使用从getOutline
创建的Shape
对象在图形上下文中设置一个剪切区域。将梵高的著名绘画作品The Starry Night
的starry.gif
图像绘制到从Rectangle
对象的左下角开始的剪切区域中。
g2.setClip(outline);
g2.drawImage(img, r.x, r.y, r.width, r.height, this);
注意: 如果你看不到小程序在运行,你需要安装至少 Java SE Development Kit (JDK) 7 版本。
Starry.java
包含了这个程序的完整代码。这个小程序需要 Starry.gif
图像文件。
合成图形
原文:
docs.oracle.com/javase/tutorial/2d/advanced/compositing.html
AlphaComposite
类封装了各种合成样式,确定重叠对象如何被渲染。AlphaComposite
还可以具有指定透明度的 alpha 值:alpha = 1.0 完全不透明,alpha = 0.0 完全透明(清除)。AlphaComposite
支持下表中显示的大多数标准 Porter-Duff 合成规则。
合成规则 | 描述 |
---|---|
源覆盖 (SRC_OVER ) | 如果正在渲染的对象(源)中的像素与先前渲染的像素(目标)位于相同位置,则源像素将覆盖目标像素。 |
源中 (SRC_IN ) | 如果源和目标重叠,只有源区域的像素被渲染。 |
源减去 (SRC_OUT ) | 如果源和目标重叠,只有源区域外的像素被渲染。重叠区域的像素被清除。 |
目标覆盖 (DST_OVER ) | 如果源和目标重叠,只有源区域外的像素被渲染。重叠区域的像素不会改变。 |
目标中 (DST_IN ) | 如果源和目标重叠,源的 alpha 值将应用于重叠区域的目标像素。如果 alpha = 1.0,则重叠区域的像素保持不变;如果 alpha 为 0.0,则清除重叠区域的像素。 |
目标减去 (DST_OUT ) | 如果源和目标重叠,源的 alpha 值将应用于重叠区域的目标像素。如果 alpha = 1.0,则清除重叠区域的像素;如果 alpha 为 0.0,则重叠区域的像素保持不变。 |
清除 (CLEAR ) | 如果源和目标重叠,清除重叠区域的像素。 |
要更改 Graphics2D
类使用的合成样式,请创建一个 AlphaComposite
对象并将其传递给 setComposite
方法。
示例:合成
该程序演示了各种合成样式和 alpha 组合的效果。
注意: 如果您看不到 applet 运行,请至少安装Java SE Development Kit (JDK) 7版本。
Composite.java
包含了此 applet 的完整代码。
通过调用 AlphaComposite.getInstance
并指定所需的合成规则来构造一个新的 AlphaComposite
对象 ac。
AlphaComposite ac =
AlphaComposite.getInstance(AlphaComposite.SRC);
当选择不同的合成规则或 alpha 值时,会再次调用 AlphaComposite.getInstance
,并将新的 AlphaComposite
分配给 ac。所选的 alpha 值除了每个像素的 alpha 值外还会应用,并作为第二个参数传递给 AlphaComposite
.getInstance
。
ac = AlphaComposite.getInstance(getRule(rule), alpha);
通过将 AlphaComposite
对象传递给 Graphics 2D
的 setComposite
方法来修改合成属性。对象被渲染到 BufferedImage
中,然后复制到屏幕上,因此合成属性被设置在 BufferedImage
的 Graphics2D
上下文中:
BufferedImage buffImg = new BufferedImage(w, h,
BufferedImage.TYPE_INT_ARGB);
Graphics2D gbi = buffImg.createGraphics();
...
gbi.setComposite(ac);
控制渲染质量
原文:
docs.oracle.com/javase/tutorial/2d/advanced/quality.html
使用Graphics2D
类的渲染提示属性来指定您是希望对象尽可能快地呈现还是您更喜欢呈现质量尽可能高。
要设置或更改Graphics2D
上下文中的渲染提示属性,请构造一个RenderingHints
对象,并通过使用setRenderingHints
方法将其传递给Graphics2D
。如果您只想设置一个提示,可以调用Graphics2D
的setRenderingHint
并指定要设置的提示的键值对。(键值对在RenderingHints
类中定义。)
例如,要设置抗锯齿首选项(如果可能的话),您可以使用setRenderingHint
:
public void paint (graphics g){
Graphics2D g2 = (Graphics2D)g;
RenderingHints rh = new RenderingHints(
RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2.setRenderingHints(rh);
...
}
**注意:**并非所有平台都支持修改渲染模式,因此指定渲染提示并不保证它们会被使用。
RenderingHints
支持以下类型的提示:
提示 | 键 | 值 |
---|
| 抗锯齿 | KEY_ANTIALIASING
| VALUE_ANTIALIAS_ON
VALUE_ANTIALIAS_OFF
VALUE_ANTIALIAS_DEFAULT
|
| Alpha 插值 | KEY_ALPHA_INTERPOLATION
| VALUE_ALPHA_INTERPOLATION_QUALITY
VALUE_ALPHA_INTERPOLATION_SPEED
VALUE_ALPHA_INTERPOLATION_DEFAULT
|
| 颜色渲染 | KEY_COLOR_RENDERING
| VALUE_COLOR_RENDER_QUALITY
VALUE_COLOR_RENDER_SPEED
VALUE_COLOR_RENDER_DEFAULT
|
| 抖动 | KEY_DITHERING
| VALUE_DITHER_DISABLE
VALUE_DITHER_ENABLE
VALUE_DITHER_DEFAULT
|
| 分数文本度量 | KEY_FRACTIONALMETRICS
| VALUE_FRACTIONALMETRICS_ON
VALUE_FRACTIONALMETRICS_OFF
VALUE_FRACTIONALMETRICS_DEFAULT
|
| 图像插值 | KEY_INTERPOLATION
| VALUE_INTERPOLATION_BICUBIC
VALUE_INTERPOLATION_BILINEAR
VALUE_INTERPOLATION_NEAREST_NEIGHBOR
|
| 渲染 | KEY_RENDERING
| VALUE_RENDER_QUALITY
VALUE_RENDER_SPEED
VALUE_RENDER_DEFAULT
|
| 笔画规范化控制 | KEY_STROKE_CONTROL
| VALUE_STROKE_NORMALIZE
VALUE_STROKE_DEFAULT
VALUE_STROKE_PURE
|
| 文本抗锯齿 | KEY_TEXT_ANTIALIASING
| VALUE_TEXT_ANTIALIAS_ON
VALUE_TEXT_ANTIALIAS_OFF
VALUE_TEXT_ANTIALIAS_DEFAULT
VALUE_TEXT_ANTIALIAS_GASP
VALUE_TEXT_ANTIALIAS_LCD_HRGB
VALUE_TEXT_ANTIALIAS_LCD_HBGR
VALUE_TEXT_ANTIALIAS_LCD_VRGB
VALUE_TEXT_ANTIALIAS_LCD_VBGR
|
LCD 文本对比度 | KEY_TEXT_LCD_CONTRAST | 值应为范围在 100 到 250 之间的正整数。较低的值(如 100)对应于在浅色背景上显示深色文本时更高的对比度。较高的值(如 200)对应于在浅色背景上显示深色文本时较低的对比度。一个典型有用的值在 140-180 的狭窄范围内。如果未指定值,则将应用系统或实现默认值。 |
---|
当提示设置为默认时,使用平台渲染默认值。
从几何原语构建复杂形状
原文:
docs.oracle.com/javase/tutorial/2d/advanced/complexshapes.html
构造区域几何(CAG)是通过对现有几何形状执行布尔运算来创建新几何形状的过程。在 Java 2D API 中,Area
类实现了 Shape
接口,并支持以下布尔运算。
并集 | 减法 | ||
---|---|---|---|
交集 | 异或 (XOR ) |
例子:区域
在这个例子中,Area
对象从几个椭圆构建了一个梨形。
注意: 如果你看不到小程序运行,你需要至少安装 Java SE Development Kit (JDK) 7 版本。
Pear.java
包含了这个小程序的完整代码。
每片叶子是通过在两个重叠圆上执行交集操作创建的。
leaf = new Ellipse2D.Double();
...
leaf1 = new Area(leaf);
leaf2 = new Area(leaf);
...
leaf.setFrame(ew-16, eh-29, 15.0, 15.0);
leaf1 = new Area(leaf);
leaf.setFrame(ew-14, eh-47, 30.0, 30.0);
leaf2 = new Area(leaf);
leaf1.intersect(leaf2);
g2.fill(leaf1);
...
leaf.setFrame(ew+1, eh-29, 15.0, 15.0);
leaf1 = new Area(leaf);
leaf2.intersect(leaf1);
g2.fill(leaf2);
重叠的圆也被用来通过减法操作构建茎。
stem = new Ellipse2D.Double();
...
stem.setFrame(ew, eh-42, 40.0, 40.0);
st1 = new Area(stem);
stem.setFrame(ew+3, eh-47, 50.0, 50.0);
st2 = new Area(stem);
st1.subtract(st2);
g2.fill(st1);
梨的主体是通过在一个圆和一个椭圆上执行并集操作构建的。
circle = new Ellipse2D.Double();
oval = new Ellipse2D.Double();
circ = new Area(circle);
ov = new Area(oval);
...
circle.setFrame(ew-25, eh, 50.0, 50.0);
oval.setFrame(ew-19, eh-20, 40.0, 70.0);
circ = new Area(circle);
ov = new Area(oval);
circ.add(ov);
g2.fill(circ);
支持用户交互
原文:
docs.oracle.com/javase/tutorial/2d/advanced/user.html
为了让用户与您显示的图形进行交互,您需要能够确定用户何时点击其中一个图形。Graphics2D
类的 hit
方法提供了一种简单确定鼠标点击是否发生在特定 Shape
对象上的方法。或者,您可以获取鼠标点击的位置并在 Shape
上调用 contains
方法来确定点击是否在 Shape
的边界内。
如果您使用基本文本,可以通过获取与文本对应的轮廓 Shape
,然后使用该 Shape
调用 hit
或 contains
方法来执行简单的点击测试。支持文本编辑需要更复杂的点击测试。如果要允许用户编辑文本,通常应使用 Swing 可编辑文本组件之一。如果您使用基本文本并使用 TextLayout
类来管理文本的形状和位置,则还可以使用 TextLayout
来执行文本编辑的点击测试。有关更多信息,请参阅 Java 2D 程序员指南 中的文本和字体章节,或查看下面的 HitTestSample 示例,该示例使用 TextLayout
执行简单的点击测试。
示例:ShapeMover
此小程序允许用户在小程序窗口内拖动 Shape
。Shape
在每个鼠标位置重新绘制,以提供用户拖动时的反馈。
注意: 如果您看不到小程序运行,请至少安装 Java SE Development Kit (JDK) 7 版本。
ShapeMover.java
包含了此小程序的完整代码。
当鼠标按下时,调用 contains
方法来确定光标是否在矩形的边界内。如果是,则更新矩形的位置。
public void mousePressed(MouseEvent e){
last_x = rect.x - e.getX();
last_y = rect.y - e.getY();
if(rect.contains(e.getX(),
e.getY())) updateLocation(e);
...
public void updateLocation(MouseEvent e){
rect.setLocation(last_x + e.getX(),
last_y + e.getY());
...
repaint();
您可能会注意到,在每个鼠标位置重新绘制 Shape
是很慢的,因为填充的矩形每次移动时都会重新渲染。使用双缓冲可以消除这个问题。如果使用 Swing,绘图将自动双缓冲;您无需更改渲染代码。这个程序的 Swing 版本的代码是 SwingShapeMover.java
。
示例:HitTestSample
这个应用程序通过在用户点击 TextLayout
上的位置绘制默认插入符来说明点击测试,如下图所示。
注意: 如果你看不到小程序在运行,你需要至少安装 Java SE Development Kit (JDK) 7 版本。
HitTestSample.java
包含了这个小程序的完整代码。
mouseClicked
方法使用 TextLayout.hitTestChar
返回一个包含鼠标点击位置(插入索引)的 java.awt.font.TextHitInfo
对象在 TextLayout
对象中。
TextLayout
的 getAscent
、getDescent
和 getAdvance
方法返回的信息被用来计算 TextLayout
对象的原点位置,使其水平和垂直居中。
...
private Point2D computeLayoutOrigin() {
Dimension size = getPreferredSize();
Point2D.Float origin = new Point2D.Float();
origin.x = (float) (size.width -
textLayout.getAdvance()) / 2;
origin.y =
(float) (size.height -
textLayout.getDescent() +
textLayout.getAscent())/2;
return origin;
}
...
public void paintComponent(Graphics g) {
super.paintComponent(g);
setBackground(Color.white);
Graphics2D graphics2D = (Graphics2D) g;
Point2D origin = computeLayoutOrigin();
graphics2D.translate(origin.getX(),
origin.getY());
// Draw textLayout.
textLayout.draw(graphics2D, 0, 0);
// Retrieve caret Shapes for insertionIndex.
Shape[] carets =
textLayout.getCaretShapes(insertionIndex);
// Draw the carets. carets[0] is the strong
// caret and carets[1] is the weak caret.
graphics2D.setColor(STRONG_CARET_COLOR);
graphics2D.draw(carets[0]);
if (carets[1] != null) {
graphics2D.setColor(WEAK_CARET_COLOR);
graphics2D.draw(carets[1]);
}
}
...
private class HitTestMouseListener
extends MouseAdapter {
/**
* Compute the character position of the
* mouse click.
*/
public void mouseClicked(MouseEvent e) {
Point2D origin = computeLayoutOrigin();
// Compute the mouse click location
// relative to textLayout's origin.
float clickX =
(float) (e.getX() - origin.getX());
float clickY =
(float) (e.getY() - origin.getY());
// Get the character position of the
// mouse click.
TextHitInfo currentHit =
textLayout.hitTestChar(clickX, clickY);
insertionIndex =
currentHit.getInsertionIndex();
// Repaint the Component so the new
// caret(s) will be displayed.
hitPane.repaint();
}
路径:声音
原文:
docs.oracle.com/javase/tutorial/sound/index.html
Java Sound API 是一个低级 API,用于影响和控制声音媒体的输入和输出,包括音频和 MIDI(Musical Instrument Digital Interface)数据。 Java Sound API 提供了对通常需要的声音输入和输出功能的明确控制,以促进可扩展性和灵活性。
Java Sound API 满足了各种应用程序开发人员的需求。潜在的应用领域包括:
-
通信框架,如会议和电话会议
-
最终用户内容传递系统,如使用流媒体内容的媒体播放器和音乐
-
交互式应用程序,如使用动态内容的游戏和网站
-
内容创建和编辑
-
工具、工具包和实用程序
Java Sound API 在 Java 平台上提供了最低级别的声音支持。它为应用程序提供了对声音操作的大量控制,并且是可扩展的。例如,Java Sound API 提供了安装、访问和操作系统资源的机制,如音频混音器、MIDI 合成器、其他音频或 MIDI 设备、文件读取器和写入器以及声音格式转换器。 Java Sound API 不包括复杂的声音编辑器或图形工具,但它提供了可以构建此类程序的功能。它强调低于最终用户通常期望的低级别控制。
Java Sound API 包括对数字音频和 MIDI 数据的支持。这两个主要功能模块分别在不同的包中提供:
-
javax.sound.sampled
– 该包指定了用于捕获、混合和播放数字(采样)音频的接口。 -
javax.sound.midi
– 该包提供了用于 MIDI 合成、序列化和事件传输的接口。
另外两个包允许服务提供商(而不是应用程序开发人员)创建自定义软件组件,以扩展 Java Sound API 的实现的功能:
-
javax.sound.sampled.spi
-
javax.sound.midi.spi
本页介绍了采样音频系统、MIDI 系统和 SPI 包。每个包稍后在教程中进行了更详细的讨论。
注意:
还有其他 Java 平台 API 也涉及声音相关元素。Java 媒体框架 API (JMF) 是当前作为 Java 平台标准扩展可用的更高级 API。JMF 指定了用于捕获和播放基于时间的媒体的统一架构、消息协议和编程接口。JMF 为基本媒体播放器应用程序提供了更简单的解决方案,并且它实现了不同媒体类型之间的同步,例如音频和视频。另一方面,专注于声音的程序可以从 Java Sound API 中受益,特别是如果它们需要更高级的功能,例如精细控制缓冲音频播放或直接操作 MIDI 合成器。其他具有声音方面的 Java API 包括 Java 3D 和用于电话和语音的 API。这些 API 的任何实现可能在内部使用 Java Sound API 的实现,但不是必须的。
什么是采样音频?
javax.sound.sampled
包处理数字音频数据,Java Sound API 将其称为采样音频。样本 是信号的连续快照。在音频的情况下,信号是声波。麦克风将声学信号转换为相应的模拟电信号,模拟-数字转换器将该模拟信号转换为采样数字形式。以下图显示了声音录制中的一个瞬间。
采样声波
这个图表将声压(振幅)绘制在垂直轴上,时间绘制在水平轴上。模拟声波的振幅以一定速率周期性地进行测量,导致离散样本(图中的红色数据点)构成数字音频信号。中心水平线表示零振幅;线上的点为正值样本,线下的点为负值。模拟信号的数字近似精度取决于其时间分辨率(采样率)和振幅分辨率(量化),即用于表示每个样本的位数。作为参考,用于存储在光盘上的音频每秒采样 44,100 次,并以每个样本 16 位表示。
这里稍微宽松地使用了“采样音频”这个术语。声波可以在被保留为模拟形式的同时以离散间隔进行采样。然而,对于 Java Sound API 来说,“采样音频”等同于“数字音频”。
通常,计算机上的采样音频来自声音录制,但声音也可以被合成生成(例如,创建触摸电话的声音)。术语“采样音频”指的是数据类型,而不是其来源。
Java Sound API 不假设特定的音频硬件配置;它设计为允许在系统上安装不同类型的音频组件,并通过 API 访问。Java Sound API 支持常见功能,例如从声卡输入和输出(例如,用于录制和播放声音文件)以及混合多个音频流。以下是一个典型音频架构的示例:
典型音频架构
在这个例子中,像声卡这样的设备具有各种输入和输出端口,并且混音是在软件中提供的。混音器可能接收从文件中读取的数据,从网络流式传输的数据,应用程序动态生成的数据,或者由 MIDI 合成器产生的数据。混音器将所有音频输入组合成一个流,可以发送到输出设备进行渲染。
什么是 MIDI?
javax.sound.midi包含用于传输和排序 MIDI 事件以及从这些事件中合成声音的 API。
而采样音频是声音本身的直接表示,MIDI 数据可以被视为创建声音的配方,特别是音乐声音的配方。与音频数据不同,MIDI 数据不直接描述声音。相反,它描述影响由 MIDI 启用的设备或乐器执行的声音(或动作)的事件,例如合成器。 MIDI 数据类似于图形用户界面的键盘和鼠标事件。在 MIDI 的情况下,这些事件可以被视为对音乐键盘以及乐器上的各种踏板、滑块、开关和旋钮的动作。这些事件不一定实际起源于硬件乐器;它们可以在软件中模拟,并且可以存储在 MIDI 文件中。一个可以创建、编辑和执行这些文件的程序被称为序列器。许多计算机声卡包括可以通过序列器发送其 MIDI 事件的 MIDI 可控音乐合成器芯片。合成器也可以完全在软件中实现。合成器解释它们接收到的 MIDI 事件并产生音频输出。通常,从 MIDI 数据合成的声音是音乐声音(例如,与语音相对)。MIDI 合成器还能够生成各种声音效果。
一些声卡包括 MIDI 输入和输出端口,可以连接外部 MIDI 硬件设备(如键盘合成器或其他乐器)。从 MIDI 输入端口,应用程序可以接收外部 MIDI 设备生成的事件。程序可以使用计算机的内部合成器演奏音乐表演,将其保存为 MIDI 文件,或将其渲染为音乐符号。程序可以使用 MIDI 输出端口来演奏外部乐器,或控制其他外部设备,如录音设备。
以下图示了基于 Java Sound API 的可能 MIDI 配置中主要组件之间的功能关系。(与音频一样,Java Sound API 允许安装和连接各种 MIDI 软件设备。此处显示的系统只是一个潜在的场景。)组件之间的数据流由箭头表示。数据可以是标准文件格式,或者(如图示右下角的关键所示),可以是音频、原始 MIDI 字节或时间标记 MIDI 消息。
可能的 MIDI 配置
在此示例中,应用程序通过加载存储在磁盘上的标准 MIDI 文件的音乐乐谱(图中的左侧)来准备音乐表演。标准 MIDI 文件包含轨道,每个轨道都是一个时间标记 MIDI 事件列表。大多数事件代表音乐音符(音高和节奏)。这个 MIDI 文件被读取,然后由软件音序器“演奏”。音序器通过向其他设备发送 MIDI 消息来演奏音乐,例如内部或外部合成器。合成器本身可能会读取包含模拟某些乐器声音指令的声音库文件。如果没有,合成器将使用已加载的任何乐器声音来播放 MIDI 文件中存储的音符。
如图所示,MIDI 事件必须在通过 MIDI 输出端口发送到外部 MIDI 乐器之前被转换为原始(非时间标记)MIDI。同样,从外部 MIDI 源(图中的键盘乐器)进入计算机的原始 MIDI 数据被转换为可以控制合成器的时间标记 MIDI 消息,或者可以由音序器存储以供以后使用。
服务提供者接口
javax.sound.sampled.spi
和 javax.sound.midi.spi
包含的 API 允许软件开发人员创建新的音频或 MIDI 资源,可以单独提供给用户并“插入”到 Java Sound API 的现有实现中。以下是可以以这种方式添加的一些服务(资源)的示例:
-
音频混音器
-
MIDI 合成器
-
一个可以读取或写入新类型音频或 MIDI 文件的文件解析器
-
在不同声音数据格式之间进行转换的转换器
在某些情况下,服务是软件接口,用于访问硬件设备的功能,比如声卡,而服务提供者可能与硬件供应商相同。在其他情况下,服务纯粹存在于软件中。例如,合成器或混音器可以是声卡上的芯片的接口,也可以在没有任何硬件支持的情况下实现。
Java Sound API 的实现包含一组基本服务,但服务提供者接口(SPI)包允许第三方创建新服务。这些第三方服务以与内置服务相同的方式集成到系统中。AudioSystem
类和MidiSystem
类充当协调器,让应用程序明确或隐式地访问服务。对于使用它的应用程序来说,服务的存在通常对其完全透明。服务提供者机制使基于 Java Sound API 的应用程序的用户受益,因为可以向程序添加新的声音功能,而无需新版本的 JDK 或运行时环境,甚至在许多情况下,甚至无需新版本的应用程序本身。
采样包概述
原文:
docs.oracle.com/javase/tutorial/sound/sampled-overview.html
javax.sound.sampled
包主要涉及音频传输 - 换句话说,Java Sound API 专注于播放和捕获。 Java Sound API 解决的核心任务是如何将格式化音频数据的字节移入和移出系统。 这项任务涉及打开音频输入和输出设备以及管理填充实时音频数据的缓冲区。 它还可以涉及将多个音频流混合成一个流(无论是用于输入还是输出)。 当用户请求启动、暂停、恢复或停止声音流时,系统内部的声音传输必须得到正确处理。
为了支持对基本音频输入和输出的关注,Java Sound API 提供了在各种音频数据格式之间转换的方法,并且提供了读取和写入常见类型的声音文件的方法。 但是,它并不试图成为一个全面的声音文件工具包。 Java Sound API 的特定实现不必支持广泛的文件类型或数据格式转换。 第三方服务提供商可以提供模块,这些模块可以“插入”到现有实现中,以支持额外的文件类型和转换。
Java Sound API 可以以流式缓冲方式和内存中非缓冲方式处理音频传输。 这里所说的“流式”是指实时处理音频字节; 它并不是指以某种特定格式通过互联网发送音频的众所周知的情况。 换句话说,音频流只是一组连续的音频字节,它们以更多或更少的相同速率到达,以便处理(播放、录制等)。 操作在所有数据到达之前开始。 在流式模型中,特别是在音频输入而不是音频输出的情况下,您不一定事先知道声音的持续时间以及何时会完成到达。 您只需一次处理一个音频数据缓冲区,直到操作停止。 在音频输出(播放)的情况下,如果要播放的声音太大而无法一次性放入内存中,则还需要缓冲数据。 换句话说,您以块的形式将音频字节传递给声音引擎,它会在正确的时间播放每个样本。 提供了机制,使得很容易知道每个块中要传递多少数据。
Java Sound API 还允许在仅播放的情况下进行无缓冲传输,假设您已经拥有所有音频数据并且数据量不太大以适应内存。在这种情况下,应用程序无需缓冲音频,尽管如果需要,仍然可以使用缓冲的实时方法。相反,整个声音可以一次性预加载到内存中以供后续播放。由于所有声音数据都是预先加载的,因此播放可以立即开始,例如,用户点击“开始”按钮时。与缓冲模型相比,这可能是一个优势,因为在缓冲填满之前,播放必须等待第一个缓冲区。此外,内存中的无缓冲模型允许轻松循环(循环)声音或将其设置为数据中的任意位置。
使用 Java Sound API 播放或捕获声音时,您至少需要三样东西:格式化的音频数据,混音器和线路。以下是这些概念的概述。
什么是格式化音频数据?
格式化的音频数据指的是任何一种标准格式的声音。Java Sound API 区分数据格式和文件格式。
数据格式
数据格式告诉您如何解释一系列“原始”采样音频数据的字节,例如已从声音文件中读取的样本,或者已从麦克风输入捕获的样本。例如,您可能需要知道一个样本包含多少比特(表示声音的最短瞬间的表示),同样您可能需要知道声音的采样率(样本应该多快地跟随彼此)。在设置播放或捕获时,您指定正在捕获或播放的声音的数据格式。
在 Java Sound API 中,数据格式由一个AudioFormat
对象表示,其中包括以下属性:
-
编码技术,通常是脉冲编码调制(PCM)
-
通道数(单声道为 1,立体声为 2,等等)
-
采样率(每秒每个通道的样本数)
-
每个样本(每个通道)的比特数
-
帧率
-
每帧大小(以字节为单位)
-
字节顺序(大端或小端)
PCM 是声波的一种编码方式。Java Sound API 包括两种使用线性幅度量化的 PCM 编码,以及带符号或无符号整数值。线性量化意味着每个样本中存储的数字与该瞬间的原始声压(除了任何失真)成正比,类似地与振动声音的扬声器或鼓膜的位移成正比,该振动声音在该瞬间发生。例如,CD 使用线性 PCM 编码的声音。 mu-law 编码和 a-law 编码是常见的非线性编码,它们提供音频数据的更紧缩版本;这些编码通常用于电话或语音录音。非线性编码将原始声音的幅度通过非线性函数映射到存储值,该函数可以设计为给予安静声音比响亮声音更多的幅度分辨率。
一个帧包含特定时间所有通道的数据。对于 PCM 编码的数据,帧只是所有通道在给定时间点的同时样本集,没有任何附加信息。在这种情况下,帧速率等于采样率,帧大小以字节为单位是通道数乘以位采样大小,再除以字节的位数。
对于其他类型的编码,一个帧可能包含除样本之外的附加信息,并且帧速率可能与采样率完全不同。例如,考虑 MP3(MPEG-1 音频第三层)编码,它在当前版本的 Java Sound API 中没有明确提到,但可以由 Java Sound API 的实现或第三方服务提供商支持。在 MP3 中,每个帧包含一系列样本的压缩数据包,而不仅仅是每个通道的一个样本。由于每个帧封装了一整个系列的样本,因此帧速率比采样率慢。帧还包含一个头部。尽管有头部,但帧的字节大小比等量的 PCM 帧的字节大小要小。(毕竟,MP3 的目的是比 PCM 数据更紧凑。)对于这种编码,采样率和采样大小指的是编码后的声音最终将被转换成的 PCM 数据,然后传递给数字模拟转换器(DAC)。
文件格式
文件格式指定了声音文件的结构,包括文件中原始音频数据的格式,以及可以存储在文件中的其他信息。声音文件有各种标准品种,如 WAVE(也称为 WAV,通常与 PC 关联)、AIFF(通常与 Macintosh 关联)和 AU(通常与 UNIX 系统关联)。不同类型的声音文件具有不同的结构。例如,它们可能在文件的“头部”中具有不同的数据排列。头部包含描述性信息,通常在文件的实际音频样本之前,尽管一些文件格式允许连续的描述性和音频数据“块”。头部包括规范用于存储声音文件中音频的数据格式。任何这些类型的声音文件都可以包含各种数据格式(尽管通常在给定文件中只有一个数据格式),并且相同的数据格式可以在具有不同文件格式的文件中使用。
在 Java Sound API 中,文件格式由一个AudioFileFormat
对象表示,其中包含:
-
文件类型(WAVE、AIFF 等)
-
文件的字节长度
-
文件中包含的音频数据的帧数长度
-
一个指定文件中包含的音频数据的数据格式的 AudioFormat 对象
AudioSystem
类提供了用于读取和写入不同文件格式的声音以及在不同数据格式之间转换的方法。其中一些方法允许您通过一种称为AudioInputStream
的流来访问文件的内容。AudioInputStream
是InputStream
类的子类,封装了可以按顺序读取的一系列字节。AudioInputStream
类添加了有关字节音频数据格式的知识(由AudioFormat
对象表示)到其超类。通过将声音文件作为AudioInputStream
读取,您可以立即访问样本,而无需担心声音文件的结构(其头部、块等)。单个方法调用将为您提供有关数据格式和文件类型的所有信息。
什么是混音器?
许多声音应用程序编程接口(API)使用音频设备的概念。设备通常是对物理输入/输出设备的软件接口。例如,声音输入设备可能代表声卡的输入功能,包括麦克风输入、线路级模拟输入,以及可能的数字音频输入。
在 Java Sound API 中,设备由Mixer
对象表示。混音器的目的是处理一个或多个音频输入流和一个或多个音频输出流。在典型情况下,它实际上将多个传入流混合成一个传出流。一个Mixer
对象可以表示物理设备(如声卡)的声音混合功能,该设备可能需要混合从各种输入到计算机的声音,或者从应用程序到输出的声音。
或者,一个Mixer
对象可以表示完全在软件中实现的声音混合功能,而没有与物理设备的固有接口。
在 Java Sound API 中,诸如声卡上的麦克风输入之类的组件本身并不被视为设备——也就是混音器——而是混音器内或外的端口。一个端口通常提供一个音频流进入或离开混音器(尽管该流可以是多声道的,比如立体声)。混音器可能有几个这样的端口。例如,代表声卡输出功能的混音器可能将几个音频流混合在一起,然后将混合信号发送到连接到混音器的任何或所有各种输出端口。这些输出端口可以是(例如)耳机插孔、内置扬声器或线路级输出。
要理解 Java Sound API 中混音器的概念,有助于想象一个物理混音控制台,比如在现场音乐会和录音室中使用的那种。
物理混音控制台
物理混音器有“条带”(也称为“切片”),每个条带代表一个音频信号通过混音器进行处理的路径。该条带有旋钮和其他控件,通过这些控件可以控制该条带中信号的音量和声像(在立体声图像中的位置)。此外,混音器可能有一个用于混响等效果的单独总线,该总线可以连接到内部或外部混响单元。每个条带都有一个电位器,用于控制该条带信号的多少进入混响混合中。混响(“湿”)混合然后与来自条带的“干”信号混合。物理混音器将这个最终混合发送到一个输出总线,通常连接到磁带录音机(或基于磁盘的录音系统)和/或扬声器。
想象一场正在立体录制的现场音乐会。来自舞台上许多麦克风和电子乐器的电缆(或无线连接)插入混音台的输入。每个输入都进入混音器的一个单独条道,如图所示。音响工程师决定增益、声像和混响控件的设置。所有条道和混响单元的输出混合成两个声道。这两个声道进入混音器的两个输出,插入连接到立体磁带录音机输入的电缆中。这两个声道可能也通过放大器发送到大厅的扬声器,这取决于音乐类型和大厅的大小。
现在想象一个录音室,在录音室中,每个乐器或歌手都被录制到多轨磁带录音机的单独轨道上。在所有乐器和歌手都被录制后,录音工程师执行“混音”操作,将所有录制的轨道组合成可以分发到 CD 上的两声道(立体声)录音。在这种情况下,混音器条的每个输入不是麦克风,而是多轨录音的一个轨道。工程师可以再次使用条上的控件来决定每个轨道的音量、声像和混响量。混音器的输出再次进入立体录音机和立体扬声器,就像现场音乐会的例子一样。
这两个例子说明了混音器的两种不同用途:捕获多个输入通道,将它们组合成较少的轨道并保存混合物,或者播放多个轨道同时将它们混合成较少的轨道。
在 Java Sound API 中,混音器可以类似地用于输入(捕获音频)或输出(播放音频)。在输入的情况下,混音器获取音频进行混音的源是一个或多个输入端口。混音器将捕获和混合的音频流发送到其目标,这是一个带有缓冲区的对象,应用程序可以从中检索这些混合音频数据。在音频输出的情况下,情况则相反。混音器的音频源是一个或多个包含缓冲区的对象,其中一个或多个应用程序将其声音数据写入其中;混音器的目标是一个或多个输出端口。
什么是一条线?
一个物理混音台的隐喻也有助于理解 Java Sound API 对线路概念的理解。
一条线是数字音频“管道”的一个元素,即将音频移入或移出系统的路径。通常,该线路是进入或离开混音器的路径(尽管从技术上讲,混音器本身也是一种线路)。
音频输入和输出端口是线路。这些类似于连接到物理混音台的麦克风和扬声器。另一种线路是应用程序可以通过其中获取输入音频或将输出音频发送到混音器的数据路径。这些数据路径类似于连接到物理混音台的多轨录音机的轨道。
Java Sound API 中的线路与物理混音器的一个区别是,通过 Java Sound API 中的线路流动的音频数据可以是单声道或多声道(例如,立体声)。相比之下,物理混音器的每个输入和输出通常是单声道的声音。要从物理混音器获得两个或更多声道的输出,通常会使用两个或更多个物理输出(至少在模拟声音的情况下;数字输出插孔通常是多声道的)。在 Java Sound API 中,线路中的声道数由当前流经线路的数据的AudioFormat
指定。
现在让我们来看一些特定类型的线路和混音器。以下图表显示了 Java Sound API 实现的简单音频输出系统中不同类型的线路:
音频输出的可能配置线路
在这个例子中,一个应用程序已经获得了音频输入混音器的一些可用输入:一个或多个片段和源数据线路。片段是一个混音器输入(一种线路),你可以在播放之前将音频数据加载到其中;源数据线路是一个接受实时音频数据流的混音器输入。应用程序将音频数据从声音文件预加载到片段中。然后,它将其他音频数据一次一个缓冲区地推送到源数据线路中。混音器从所有这些线路中读取数据,每个线路可能有自己的混响、增益和声像控制,并将干净的音频信号与湿润(混响)混合。混音器将最终输出传送到一个或多个输出端口,例如扬声器、耳机插孔和线路输出插孔。
尽管在图中各个线路被描绘为单独的矩形,但它们都是混音器的“所有权”,可以被视为混音器的组成部分。混响、增益和声像矩形代表混音器可以应用于流经线路的数据的处理控制(而不是线路)。
请注意,这只是 API 支持的可能混音器的一个示例。并非所有音频配置都具有所示的所有功能。个别源数据线路可能不支持声像控制,混音器可能不实现混响等。
一个简单的音频输入系统可能类似:
音频输入线路的可能配置
在这里,数据从一个或多个输入端口流入混音器,通常是麦克风或线路输入插孔。增益和声像被应用,混音器通过混音器的目标数据线将捕获的数据传递给应用程序。目标数据线是混音器的输出,包含流式输入声音的混合物。最简单的混音器只有一个目标数据线,但有些混音器可以同时将捕获的数据传递给多个目标数据线。
线接口层次结构
现在我们已经看到了一些关于线路和混音器的功能图片,让我们从稍微更具编程视角的角度来讨论它们。通过基本Line
接口的子接口定义了几种类型的线路。接口层次结构如下所示。
线接口层次结构
基本接口Line
描述了所有线路共有的最小功能:
-
控件 - 数据线和端口通常具有一组控件,影响通过线路传递的音频信号。Java Sound API 指定了可以用于操纵声音方面的控件类,例如:增益(影响信号的分贝音量)、声像(影响声音的左右定位)、混响(为声音添加混响以模拟不同种类的房间声学)和采样率(影响播放速率以及声音的音调)。
-
打开或关闭状态 - 成功打开线路保证已为线路分配了资源。混音器具有有限数量的线路,因此在某些时候,多个应用程序(或同一个应用程序)可能会竞争使用混音器的线路。关闭线路表示线路使用的任何资源现在可以被释放。
-
事件 - 当线路打开或关闭时,线路会生成事件。
Line
的子接口可以引入其他类型的事件。当线路生成事件时,事件会发送给所有已注册在该线路上“监听”事件的对象。应用程序可以创建这些对象,将它们注册为监听线路事件,并根据需要对事件做出反应。
现在我们将检查Line
接口的子接口。
端口
是用于音频输入或输出到音频设备的简单线路。如前所述,一些常见类型的端口是麦克风、线路输入、CD-ROM 驱动器、扬声器、耳机和线路输出。
Mixer
接口代表一个混音器,当然,正如我们所见,它代表一个硬件或软件设备。Mixer
接口提供了获取混音器线的方法。这些包括源线,将音频馈送到混音器,以及目标线,混音器将其混合音频传递给的线。对于音频输入混音器,源线是输入端口,如麦克风输入,目标线是TargetDataLines
(下文描述),它会将音频传递给应用程序。另一方面,对于音频输出混音器,源线是Clips
或SourceDataLines
(下文描述),应用程序向其馈送音频数据,目标线是输出端口,如扬声器。
一个Mixer
被定义为具有一个或多个源线和一个或多个目标线。请注意,这个定义意味着一个混音器不一定实际混合数据;它可能只有一个单一的源线。Mixer
API 旨在涵盖各种设备,但典型情况下支持混音。
Mixer
接口支持同步;也就是说,您可以指定一个混音器的两个或多个线被视为同步组。然后,您可以通过向组中的任何线发送单个消息来启动、停止或关闭所有这些数据线,而不必单独控制每条线。使用支持此功能的混音器,您可以在线之间获得样本精确的同步。
通用的Line
接口不提供启动和停止播放或录制的方法。为此,您需要一个数据线。DataLine
接口提供了以下额外的与媒体相关的功能,超出了Line
的功能:
-
音频格式 – 每个数据线都有与其数据流相关联的音频格式。
-
媒体位置 – 数据线可以报告其在媒体中的当前位置,以采样帧表示。这代表自数据线打开以来捕获或渲染的采样帧数量。
-
缓冲区大小 – 这是数据线内部缓冲区的大小,以字节为单位。对于源数据线,内部缓冲区是可以写入数据的,对于目标数据线,它是可以读取数据的。
-
音量(音频信号的当前振幅)
-
启动和停止播放或捕获
-
暂停和恢复播放或捕获
-
刷新(丢弃队列中的未处理数据)
-
排空(阻塞直到队列中的所有未处理数据都被排空,并且数据线的缓冲区变为空)
-
活动状态 – 如果数据线参与从混音器捕获音频数据或向混音器捕获音频数据,则被视为活动状态。
-
事件 –
START
和STOP
事件在从数据线开始或停止活动演示或捕获数据时产生。
TargetDataLine
从混音器接收音频数据。通常,混音器从诸如麦克风之类的端口捕获音频数据;在将数据放入目标数据线缓冲区之前,它可能会处理或混合此捕获的音频。TargetDataLine
接口提供了从目标数据线缓冲区读取数据的方法,并确定当前可用于读取的数据量。
SourceDataLine
接收用于播放的音频数据。它提供了将数据写入源数据线缓冲区以进行播放的方法,并确定数据线准备接收多少数据而不会阻塞。
Clip
是一个数据线,可以在播放之前加载音频数据。由于数据是预加载而不是流式传输,因此在播放之前可以知道剪辑的持续时间,并且可以选择媒体中的任何起始位置。剪辑可以循环播放,意味着在播放时,两个指定循环点之间的所有数据将重复指定次数,或者无限循环。
本节介绍了采样音频 API 中大部分重要的接口和类。后续章节将展示如何在应用程序中访问和使用这些对象。
访问音频系统资源
原文:
docs.oracle.com/javase/tutorial/sound/accessing.html
Java Sound API 对系统配置采取了灵活的方法。计算机上可以安装不同类型的音频设备(混音器)。该 API 对已安装的设备及其功能能力几乎不做任何假设。相反,它提供了系统报告可用音频组件的方法,以及您的程序访问它们的方法。
以下部分展示了您的程序如何了解计算机上已安装的采样音频资源以及如何访问可用资源。资源包括混音器和混音器拥有的各种类型的线路等。
AudioSystem
类
AudioSystem
类充当音频组件的集散地,包括来自第三方提供商的内置服务和单独安装的服务。 AudioSystem
作为应用程序访问这些已安装的采样音频资源的入口点。您可以查询 AudioSystem
以了解已安装了哪些资源,然后可以访问这些资源。例如,应用程序可能首先询问 AudioSystem
类是否有具有特定配置的混音器,例如在前面讨论线路时所示的输入或输出配置之一。然后,程序将从混音器获取数据线路,依此类推。
以下是应用程序可以从 AudioSystem
获取的一些资源:
-
混音器 — 系统通常安装了多个混音器。通常至少有一个用于音频输入和一个用于音频输出。还可能有一些混音器没有 I/O 端口,而是接受应用程序的音频并将混合后的音频传递回程序。
AudioSystem
类提供了所有已安装混音器的列表。 -
线路 — 即使每条线路都与混音器相关联,应用程序也可以直接从
AudioSystem
获取线路,而无需明确处理混音器。 -
格式转换 — 应用程序可以使用格式转换将音频数据从一种格式转换为另一种格式。
-
文件和流 —
AudioSystem
类提供了在音频文件和音频流之间进行转换的方法。它还可以报告声音文件的文件格式,并且可以以不同格式写入文件。
信息对象
Java Sound API 中的几个类提供有关相关接口的有用信息。例如,Mixer.Info
提供有关已安装混音器的详细信息,如混音器的供应商、名称、描述和版本。Line.Info
获取特定线路的类。Line.Info
的子类包括Port.Info
和DataLine.Info
,分别获取与特定端口和数据线相关的详细信息。这些类中的每一个在下面的适当部分中进一步描述。重要的是不要混淆Info
对象与其描述的混音器或线路对象。
获取混音器
通常,使用 Java Sound API 的程序需要做的第一件事情之一是获取一个混音器,或者至少获取一个混音器的一条线路,以便将声音输入或输出计算机。您的程序可能需要特定类型的混音器,或者您可能希望显示所有可用混音器的列表,以便用户可以选择一个。在任何情况下,您需要了解安装了哪些类型的混音器。AudioSystem
提供以下方法:
static Mixer.Info[] getMixerInfo()
由此方法返回的每个Mixer.Info
对象标识安装的一种混音器类型。(通常系统最多只有一个给定类型的混音器。如果恰好有多个给定类型的混音器,则返回的数组仍然只有一个该类型的Mixer.Info
。)应用程序可以遍历Mixer.Info
对象,根据自身需求找到合适的混音器。Mixer.Info
包括以下字符串来标识混音器的类型:
-
名称
-
版本
-
供应商
-
描述
这些都是任意字符串,因此需要特定混音器的应用程序必须知道可以期望什么以及将字符串与什么进行比较。提供混音器的公司应在其文档中包含此信息。或者,也许更典型的是,应用程序将向用户显示所有Mixer.Info
对象的字符串,让用户选择相应的混音器。
一旦找到合适的混音器,应用程序调用以下AudioSystem
方法来获取所需的混音器:
static Mixer getMixer(Mixer.Info info)
如果您的程序需要具有某些功能的混音器,但不需要特定供应商制造的特定混音器怎么办?如果您不能依赖用户知道应选择哪个混音器怎么办?在这种情况下,Mixer.Info
对象中的信息将没有太大用处。相反,您可以通过调用getMixer
为每个Mixer.Info
对象返回的所有Mixer.Info
对象迭代,获取每个混音器,并查询每个混音器的功能。例如,您可能需要一个可以将其混合音频数据同时写入一定数量的目标数据线的混音器。在这种情况下,您将使用此Mixer
方法查询每个混音器:
int getMaxLines(Line.Info info)
在这里,Line.Info
将指定一个TargetDataLine
。Line.Info
类将在下一节中讨论。
获取所需类型的线
有两种方法可以获取一条线:
-
直接从
AudioSystem
对象 -
从您已从
AudioSystem
对象获取的混音器获取
直接从AudioSystem
获取一条线
假设您尚未获得混音器,并且您的程序是一个真正只需要某种类型的线的简单程序;混音器的细节对您并不重要。您可以使用AudioSystem
方法:
static Line getLine(Line.Info info)
这类似于先前讨论的getMixer
方法。与Mixer.Info
不同,作为参数使用的Line.Info
不存储文本信息以指定所需的线。相反,它存储有关所需线类的信息。
Line.Info
是一个抽象类,因此您可以使用其子类(Port.Info
或DataLine.Info
)来获取一条线。以下代码摘录使用DataLine.Info
子类来获取和打开目标数据线:
TargetDataLine line;
DataLine.Info info = new DataLine.Info(TargetDataLine.class,
format); // format is an AudioFormat object
if (!AudioSystem.isLineSupported(info)) {
// Handle the error.
}
// Obtain and open the line.
try {
line = (TargetDataLine) AudioSystem.getLine(info);
line.open(format);
} catch (LineUnavailableException ex) {
// Handle the error.
//...
}
此代码获取一个TargetDataLine
对象,除了其类和音频格式之外,没有指定任何属性。您可以使用类似的代码来获取其他类型的线。对于SourceDataLine
或Clip
,只需将该类替换为TargetDataLine
作为线变量的类,并且在DataLine.Info
构造函数的第一个参数中也进行替换。
对于Port
,您可以在以下代码中使用Port.Info
的静态实例:
if (AudioSystem.isLineSupported(Port.Info.MICROPHONE)) {
try {
line = (Port) AudioSystem.getLine(
Port.Info.MICROPHONE);
}
}
请注意使用isLineSupported
方法来查看混音器是否具有所需类型的线。
请记住,源线是混音器的输入,即,如果混音器代表音频输入设备,则是一个Port
对象,如果混音器代表音频输出设备,则是一个SourceDataLine
或Clip
对象。同样,目标线是混音器的输出:对于音频输出混音器,是一个Port
对象,对于音频输入混音器,是一个TargetDataLine
对象。如果一个混音器根本没有连接到任何外部硬件设备怎么办?例如,考虑一个仅从应用程序获取音频并将混合音频传递回程序的内部或仅软件混音器。这种混音器的输入线有SourceDataLine
或Clip
对象,输出线有TargetDataLine
对象。
您还可以使用以下AudioSystem
方法了解任何已安装混音器支持的指定类型的源和目标线路:
static Line.Info[] getSourceLineInfo(Line.Info info)
static Line.Info[] getTargetLineInfo(Line.Info info)
请注意,每个方法返回的数组表示唯一类型的线路,不一定是所有线路。例如,如果一个混音器的两条线路,或者两个不同混音器的两条线路,具有相同的Line.Info
对象,则返回的数组中只会表示一个Line.Info
。
从混音器获取线路
Mixer
接口包括上述AudioSystem
访问方法的变体,用于源和目标线路。这些Mixer
方法包括接受Line.Info
参数的方法,就像AudioSystem
的方法一样。但是,Mixer
还包括这些不带参数的变体:
Line.Info[] getSourceLineInfo()
Line.Info[] getTargetLineInfo()
这些方法返回特定混音器的所有Line.Info
对象的数组。一旦获得数组,您可以遍历它们,调用Mixer
的getLine
方法获取每条线路,然后调用Line
的open
方法为您的程序保留每条线路的使用权。
选择输入和输出端口
关于如何获取所需类型的线路的上一节也适用于端口以及其他类型的线路。您可以通过将Port.Info
对象传递给接受Line.Info
参数的AudioSystem
(或Mixer
)方法getSourceLineInfo
和getTargetLineInfo
来获取所有源(即输入)和目标(即输出)端口。然后,您遍历返回的对象数组并调用 Mixer 的getLine
方法以获取每个端口。
然后,通过调用Line
的open
方法打开每个Port
。打开端口意味着您打开它 - 也就是说,您允许声音进出端口。同样,您可以关闭您不希望声音通过的端口,因为有些端口可能在您获取它们之前已经打开。一些平台默认打开所有端口;或者用户或系统管理员可能已经选择使用另一个应用程序或操作系统软件打开或关闭某些端口。
警告: 如果您想选择特定端口并确保声音实际上进出该端口,可以按照描述打开端口。但是,这可能被视为对用户不友好的行为!例如,用户可能已关闭扬声器端口以免打扰同事。如果您的程序突然推翻了她的意愿并开始播放音乐,她会感到非常沮丧。另一个例子,用户可能希望确保计算机的麦克风在没有他知情的情况下永远不会打开,以避免窃听。一般来说,建议不要打开或关闭端口,除非您的程序响应用户通过用户界面表达的意图。相反,尊重用户或操作系统已选择的设置。
在连接到混音器之前,不需要打开或关闭端口才能使其正常工作。例如,即使所有输出端口关闭,您也可以开始将声音播放到音频输出混音器中。数据仍然流入混音器;播放不会被阻止。用户只是听不到任何声音。一旦用户打开输出端口,声音将通过该端口听到,从媒体播放已经到达的任何点开始。
此外,您不需要访问端口来了解混音器是否具有某些端口。例如,要了解混音器是否实际上是音频输出混音器,可以调用getTargetLineInfo
来查看它是否具有输出端口。除非您想更改其设置(例如打开或关闭状态或它们可能具有的任何控件的设置),否则没有理由访问端口本身。
使用音频资源的权限
Java Sound API 包括一个AudioPermission
类,指示 applet(或在安全管理器下运行的应用程序)对采样音频系统可以具有哪些访问权限。录制声音的权限是单独控制的。应谨慎授予此权限,以帮助防止未经授权的窃听等安全风险。默认情况下,applet 和应用程序被授予以下权限:
-
使用 applet 安全管理器运行的applet可以播放音频,但不能录制。
-
没有安全管理器运行的应用程序可以播放和录制音频。
-
使用默认安全管理器运行的应用程序可以播放音频,但不能录制。
一般来说,applet 在安全管理器的监督下运行,不允许录制声音。另一方面,应用程序不会自动安装安全管理器,并且可以录制声音。(但是,如果为应用程序显式调用默认安全管理器,则不允许应用程序录制声音。)
即使在安全管理器下运行,只要已被明确授予权限,applet 和应用程序都可以录制声音。
如果您的程序没有录制(或播放)声音的权限,在尝试打开线路时会抛出异常。在程序中,除了捕获异常并向用户报告问题外,您无法做任何事情,因为权限无法通过 API 更改。(如果可以的话,它们将毫无意义,因为没有任何东西是安全的!)通常,权限是在一个或多个策略配置文件中设置的,用户或系统管理员可以使用文本编辑器或策略工具程序进行编辑。
播放音频
原文:
docs.oracle.com/javase/tutorial/sound/playing.html
播放有时被称为演示或渲染。这些是适用于声音以外的其他媒体的通用术语。其关键特征是一系列数据被传送到某个地方,最终由用户感知。如果数据是基于时间的,如声音,它必须以正确的速率传送。与视频相比,对于声音来说,数据流速率的维持更为重要,因为声音播放中断通常会产生响亮的点击声或刺耳的失真。Java Sound API 旨在帮助应用程序平稳连续地播放声音,即使是非常长的声音。
之前您已经看到如何从音频系统或混音器中获取线路。在这里,您将学习如何通过线路播放声音。
如您所知,有两种可以用于播放声音的线路:Clip
和SourceDataLine
。两者之间的主要区别在于,使用Clip
时,您在播放之前一次性指定所有声音数据,而使用SourceDataLine
时,在播放过程中持续写入新的数据缓冲区。虽然有许多情况可以使用Clip
或SourceDataLine
,但以下标准有助于确定哪种线路更适合特定情况:
-
当您有非实时声音数据可以预加载到内存中时,请使用
Clip
。例如,您可以将短声音文件读入剪辑中。如果您希望声音重复播放多次,则
Clip
比SourceDataLine
更方便,特别是如果您希望播放循环(重复通过声音的全部或部分)。如果您需要在声音中的任意位置开始播放,Clip
接口提供了一种轻松实现的方法。最后,与从SourceDataLine
缓冲播放相比,从Clip
播放通常具有更少的延迟。换句话说,因为声音已经预加载到剪辑中,播放可以立即开始,而不必等待缓冲区填充。 -
使用
SourceDataLine
来流式传输数据,比如无法一次性全部放入内存的长声音文件,或者在播放之前无法预先知道数据的声音。作为后一种情况的示例,假设您正在监视声音输入,即在捕获声音时播放声音。如果您没有一个可以将输入音频发送回输出端口的混音器,您的应用程序将不得不获取捕获的数据并将其发送到音频输出混音器。在这种情况下,使用
SourceDataLine
比使用Clip
更合适。另一个无法事先知道的声音示例是当您根据用户的输入合成或操作声音数据时。例如,想象一个游戏通过在用户移动鼠标时从一个声音“变形”到另一个声音来提供听觉反馈。声音转换的动态性要求应用程序在播放过程中持续更新声音数据,而不是在播放开始之前提供所有数据。
使用 Clip
如前所述,在获取所需类型的行下获取一个Clip
;用Clip.class
作为第一个参数构造一个DataLine.Info
对象,并将此DataLine.Info
作为参数传递给AudioSystem
或Mixer
的getLine
方法。
获取一条线只是意味着您已经找到了一个引用它的方法;getLine
实际上并没有为您保留该线路。因为混音器可能只有有限数量的所需类型的线路可用,所以在您调用getLine
获取剪辑后,可能会发生另一个应用程序在您准备开始播放之前抢走剪辑的情况。要实际使用剪辑,您需要通过调用以下Clip
方法之一来为您的程序独占地保留它:
void open(AudioInputStream stream)
void open(AudioFormat format, byte[] data, int offset, int bufferSize)
尽管上述第二个open
方法中有bufferSize
参数,但Clip
(不像SourceDataLine
)不包括用于向缓冲区写入新数据的方法。这里的bufferSize
参数只指定要加载到剪辑中的字节数组的大小。它不是一个可以随后加载更多数据的缓冲区,就像您可以使用SourceDataLine
的缓冲区一样。
打开剪辑后,您可以使用Clip
的setFramePosition
或setMicroSecondPosition
方法指定数据中应开始播放的位置。否则,它将从开头开始。您还可以使用setLoopPoints
方法配置循环播放。
当您准备开始播放时,只需调用start
方法。要停止或暂停剪辑,请调用stop
方法,要恢复播放,请再次调用start
。剪辑会记住停止播放的媒体位置,因此不需要显式的暂停和恢复方法。如果您不希望它在停止播放的位置继续播放,可以使用上述提到的帧或微秒定位方法将剪辑“倒带”到开头(或任何其他位置)。
可以通过调用DataLine
方法getLevel
和isActive
来监视Clip
的音量级别和活动状态(活动与非活动)。活动的Clip
是当前正在播放声音的Clip
。
使用 SourceDataLine
获取SourceDataLine
类似于获取Clip
。打开SourceDataLine
也类似于打开Clip
,因为目的再次是为了保留该线路。但是,您使用从DataLine
继承的不同方法:
void open(AudioFormat format)
请注意,当您打开SourceDataLine
时,尚未将任何声音数据与该线路关联,与打开Clip
不同。相反,您只需指定要播放的音频数据的格式。系统会选择默认的缓冲区长度。
您还可以使用以下变体指定特定的字节缓冲区长度:
void open(AudioFormat format, int bufferSize)
为了与类似方法保持一致,bufferSize
参数以字节表示,但必须对应于整数帧数。
除了使用上述描述的 open 方法,还可以使用Line
的open()
方法打开SourceDataLine
,而无需参数。在这种情况下,该线路将以其默认音频格式和缓冲区大小打开。但是,您以后无法更改这些。如果您想知道线路的默认音频格式和缓冲区大小,甚至在线路尚未打开之前,可以调用DataLine
的getFormat
和getBufferSize
方法。
一旦SourceDataLine
打开,您就可以开始播放声音。您可以通过调用DataLine
的 start 方法来实现这一点,然后将数据重复写入线路的播放缓冲区。
start 方法允许线路在其缓冲区中有任何数据时开始播放声音。您可以通过以下方法将数据放入缓冲区:
int write(byte[] b, int offset, int length)
数组中的偏移量以字节表示,数组的长度也是以字节表示。
该线路尽快将数据发送到其混音器。当混音器将数据传递给其目标时,SourceDataLine
会生成START
事件。 (在 Java Sound API 的典型实现中,源线将数据传递给混音器的时间延迟与混音器将数据传递给其目标的时间延迟可以忽略不计,即远小于一个样本的时间。)此START
事件将发送给线路的侦听器,如下所述监视线路状态。现在该线路被视为活动的,因此DataLine
的isActive
方法将返回true
。请注意,所有这些仅在缓冲区包含要播放的数据时才会发生,不一定在调用 start 方法时立即发生。如果您在新的SourceDataLine
上调用了start
但从未向缓冲区写入数据,则该线路永远不会处于活动状态,并且START
事件永远不会发送。(但是,在这种情况下,DataLine
的isRunning
方法将返回true
。)
那么你如何知道要向缓冲区写入多少数据,以及何时发送第二批数据呢?幸运的是,你不需要计时第二次调用 write 以与第一个缓冲区的结束同步!相反,你可以利用write
方法的阻塞行为:
-
该方法在数据被写入缓冲区后立即返回。它不会等到缓冲区中的所有数据都播放完毕。(如果等待的话,你可能没有时间写入下一个缓冲区,从而导致音频中断。)
-
尝试写入的数据量超过缓冲区容量是可以的。在这种情况下,该方法会阻塞(不返回),直到你请求的所有数据实际上都被放入缓冲区中。换句话说,每次只会写入一个缓冲区的数据并进行播放,直到剩余数据全部适应缓冲区为止,此时该方法才会返回。无论该方法是否阻塞,它都会在此次调用中最后一个缓冲区的数据被写入时立即返回。这意味着你的代码很可能在最后一个缓冲区的数据播放完成之前就重新获得了控制权。
-
在许多情况下,写入的数据量超过缓冲区容量是可以的,但如果你想确保下一个写入不会阻塞,你可以将写入的字节数限制为
DataLine
的available
方法返回的数量。
下面是一个示例,迭代从流中读取的数据块,一次将一个数据块写入SourceDataLine
进行播放:
// read chunks from a stream and write them to a source data
line
line.start();
while (total < totalToRead && !stopped)}
numBytesRead = stream.read(myData, 0, numBytesToRead);
if (numBytesRead == -1) break;
total += numBytesRead;
line.write(myData, 0, numBytesRead);
}
如果你不希望write
方法阻塞,你可以首先在循环内调用available
方法来查找可以无阻塞写入的字节数,然后在从流中读取之前将numBytesToRead
变量限制为这个数字。然而,在给定的示例中,阻塞并不重要,因为 write 方法是在一个循环内调用的,直到最后一个缓冲区在最后一个循环迭代中被写入。无论你是否使用阻塞技术,你可能会希望在应用程序的其余部分之外的一个单独线程中调用这个播放循环,这样当播放长声音时,你的程序不会出现冻结的情况。在循环的每次迭代中,你可以测试用户是否请求停止播放。这样的请求需要将上面代码中使用的stopped
布尔值设置为true
。
由于write
在所有数据完成播放之前返回,那么你如何知道播放实际上已经完成了呢?一种方法是在写入最后一个缓冲区数据后调用DataLine
的drain
方法。该方法会阻塞,直到所有数据都已经播放完毕。当控制返回到你的程序时,如果需要的话,你可以释放该线路,而不必担心会过早中断任何音频样本的播放:
line.write(b, offset, numBytesToWrite);
//this is the final invocation of write
line.drain();
line.stop();
line.close();
line = null;
你可以有意提前停止播放,当然。例如,应用程序可能会为用户提供一个停止按钮。调用DataLine
的stop
方法可以立即停止播放,即使在缓冲区的中间。这会使缓冲区中的任何未播放数据保留下来,因此如果随后调用start
,播放将从停止的地方恢复。如果这不是你想要发生的事情,你可以通过调用flush
来丢弃缓冲区中剩余的数据。
当数据流的流动已停止时,SourceDataLine
会生成一个STOP
事件,无论此停止是由drain
方法、stop
方法、flush
方法引起的,还是因为在应用程序调用write
提供新数据之前已到达播放缓冲区的末尾。STOP
事件并不一定意味着已调用stop
方法,并且也不一定意味着随后调用isRunning
将返回false
。但是,它确实意味着isActive
将返回false
。(当调用start
方法时,即使生成STOP
事件,isRunning
方法也将返回true
,并且只有在调用stop
方法后才会开始返回false
。)重要的是要意识到START
和STOP
事件对应于isActive
,而不是isRunning
。
监控线路的状态
一旦开始播放声音,如何确定何时完成?我们在上面看到了一种解决方案,在写入最后一个数据缓冲区后调用drain
方法,但这种方法仅适用于SourceDataLine
。另一种途径,适用于SourceDataLines
和Clips
,是注册以接收线路在改变其状态时发出的通知。这些通知以LineEvent
对象的形式生成,其中有四种类型:OPEN
、CLOSE
、START
和STOP
。
在程序中实现LineListener
接口的任何对象都可以注册以接收此类通知。要实现LineListener
接口,对象只需要一个接受LineEvent
参数的update
方法。要将此对象注册为线路的侦听器之一,您需要调用以下Line
方法:
public void addLineListener(LineListener listener)
每当线路打开、关闭、启动或停止时,它会向所有侦听器发送一个update
消息。您的对象可以查询接收到的LineEvent
。首先,您可能会调用LineEvent.getLine
来确保停止的线路是您关心的线路。在我们讨论的情况下,您想知道声音是否已完成,因此您可以查看LineEvent
是否为STOP
类型。如果是,您可以检查声音的当前位置,该位置也存储在LineEvent
对象中,并将其与声音的长度(如果已知)进行比较,以查看是否已达到结束并且没有被其他方式停止(例如用户点击停止按钮,尽管您可能能够在代码的其他地方确定该原因)。
同样,如果您需要知道何时打开、关闭或启动线路,可以使用相同的机制。LineEvents
由不同类型的线路生成,不仅仅是Clips
和SourceDataLines
。但是,在Port
的情况下,您不能指望获得事件来了解线路的打开或关闭状态。例如,当创建Port
时,可能会最初打开Port
,因此您不会调用open
方法,而Port
也不会生成OPEN
事件。 (请参阅之前关于选择输入和输出端口的讨论。)
同步多条线路的播放
如果您同时播放多个音轨,您可能希望它们都在完全相同的时间开始和停止。一些混音器通过其synchronize
方法促进此行为,该方法允许您对一组数据线应用操作,如open
、close
、start
和stop
,而不是必须单独控制每条线路。此外,可以控制对线路应用操作的精度程度。
要了解特定数据线组的特定混音器是否提供此功能,调用Mixer
接口的isSynchronizationSupported
方法:
boolean isSynchronizationSupported(Line[] lines, boolean maintainSync)
第一个参数指定了一组特定的数据线,第二个参数表示必须保持同步的精度。如果第二个参数是true
,则查询是在询问混音器是否能够始终在所有时间内保持对指定线路的样本精确控制;否则,精确同步仅在启动和停止操作期间需要,而不是在整个播放过程中。
处理传出音频
一些源数据线具有信号处理控件,如增益、声像、混响和采样率控件。类似的控件,尤其是增益控件,也可能存在于输出端口上。有关如何确定一条线路是否具有此类控件以及如何在有此类控件的情况下使用它们的更多信息,请参阅使用控件处理音频。
音频捕获
原文:
docs.oracle.com/javase/tutorial/sound/capturing.html
捕获指的是从计算机外部获取信号的过程。音频捕获的常见应用是录音,比如将麦克风输入录制到声音文件中。然而,捕获并不等同于录制,因为录制意味着应用程序始终保存正在输入的声音数据。捕获音频的应用程序不一定存储音频。相反,它可能在音频到达时对数据进行处理,比如将语音转录为文本,但在完成对每个缓冲区的处理后立即丢弃每个缓冲区的音频。
如示例包概述中所讨论的,在 Java Sound API 的实现中,典型的音频输入系统包括:
-
一个输入端口,比如麦克风端口或线路输入端口,将其传入的音频数据输入到:
-
一个混音器,将输入数据放入其中:
-
一个或多个目标数据线,应用程序可以从中检索数据。
通常情况下,一次只能打开一个输入端口,但也可以有一个混音器,从多个端口混合音频。另一种情况是一个没有端口的混音器,而是通过网络获取音频输入。
TargetDataLine
接口在线接口层次结构下简要介绍过。TargetDataLine
与SourceDataLine
接口直接类似,后者在播放音频中有详细讨论。回想一下,SourceDataLine
接口包括:
-
一个
write
方法将音频发送到混音器 -
一个
available
方法,用于确定可以向缓冲区写入多少数据而不会阻塞
同样,TargetDataLine
包括:
-
一个
read
方法从混音器获取音频 -
一个
available
方法,用于确定可以从缓冲区读取多少数据而不会阻塞
设置 TargetDataLine
获取目标数据线的过程在访问音频系统资源中已经描述,但为了方便起见,我们在这里重复一遍:
TargetDataLine line;
DataLine.Info info = new DataLine.Info(TargetDataLine.class,
format); // format is an AudioFormat object
if (!AudioSystem.isLineSupported(info)) {
// Handle the error ...
}
// Obtain and open the line.
try {
line = (TargetDataLine) AudioSystem.getLine(info);
line.open(format);
} catch (LineUnavailableException ex) {
// Handle the error ...
}
你可以调用Mixer
的getLine
方法,而不是AudioSystem
的。
正如本例所示,一旦获得目标数据线,您可以通过调用SourceDataLine
方法open
来为应用程序保留它的使用权,就像在播放音频中描述的那样。open
方法的单参数版本使线的缓冲区具有默认大小。您可以通过调用两个参数版本根据应用程序的需要设置缓冲区大小:
void open(AudioFormat format, int bufferSize)
从 TargetDataLine 读取数据
一旦线路打开,它就准备开始捕获数据,但它还没有激活。要实际开始音频捕获,使用DataLine
的start
方法。这将开始将输入音频数据传递到线路的缓冲区,以供您的应用程序读取。只有当应用程序准备好从线路读取时,应用程序才应该调用 start;否则,将浪费大量处理时间来填充捕获缓冲区,只会导致溢出(即,丢弃数据)。
要开始从缓冲区检索数据,请调用TargetDataLine
的read
方法:
int read(byte[] b, int offset, int length)
这种方法尝试将length
字节的数据读入数组b
,从数组中的字节位置offset
开始。该方法返回实际读取的字节数。
与SourceDataLine
的write
方法一样,您可以请求比缓冲区实际容量更多的数据,因为该方法会阻塞,直到请求的数据量已经传递,即使您请求了许多缓冲区的数据量。
为了避免在录制过程中使您的应用程序挂起,您可以在循环中调用 read 方法,直到检索到所有音频输入,就像这个例子中所示的那样:
// Assume that the TargetDataLine, line, has already
// been obtained and opened.
ByteArrayOutputStream out = new ByteArrayOutputStream();
int numBytesRead;
byte[] data = new byte[line.getBufferSize() / 5];
// Begin audio capture.
line.start();
// Here, stopped is a global boolean set by another thread.
while (!stopped) {
// Read the next chunk of data from the TargetDataLine.
numBytesRead = line.read(data, 0, data.length);
// Save this chunk of data.
out.write(data, 0, numBytesRead);
}
请注意,在这个例子中,将数据读入的字节数组的大小设置为线路缓冲区大小的五分之一。如果您将其设置为与线路缓冲区一样大,并尝试读取整个缓冲区,您需要在时间上非常准确,因为如果混音器需要在您从中读取数据时向线路传递数据,数据将被丢弃。通过使用线路缓冲区大小的一部分,如此处所示,您的应用程序将更成功地与混音器共享对线路缓冲区的访问。
TargetDataLine
的read
方法接受三个参数:一个字节数组,一个数组中的偏移量,以及您想要读取的输入数据的字节数。在这个例子中,第三个参数就是你的字节数组的长度。read
方法返回实际读取到数组中的字节数。
通常,您会像这个例子中一样在循环中从线路中读取数据。在while
循环中,每个检索到的数据块都会根据应用程序的适当方式进行处理——在这里,它被写入ByteArrayOutputStream
。这里没有显示的是使用单独的线程来设置布尔值stopped
,该值在循环终止时终止。当用户点击停止按钮时,该布尔值的值可能被设置为true
,并且当监听器从线路接收到CLOSE
或STOP
事件时也是如此。监听器对于CLOSE
事件是必需的,对于STOP
事件是推荐的。否则,如果线路在没有将stopped
设置为true
的情况下以某种方式停止,while
循环将在每次迭代中捕获零字节,运行速度快,浪费 CPU 周期。一个更全面的代码示例将展示如果捕获再次激活,则重新进入循环。
与源数据线一样,可以排空或清空目标数据线。例如,如果您正在将输入录制到文件中,当用户点击停止按钮时,您可能希望调用drain
方法。drain
方法将导致混音器的剩余数据传递到目标数据线的缓冲区。如果您不排空数据,捕获的声音可能在末尾被截断。
也许有一些情况下,您希望清空数据。无论如何,如果您既不清空也不排空数据,数据将保留在混音器中。这意味着当重新开始捕获时,新录音的开头会有一些残留声音,这可能是不希望的。因此,在重新开始捕获之前清空目标数据线可能是有用的。
监控线路状态
因为TargetDataLine
接口扩展了DataLine
,目标数据线生成事件的方式与源数据线相同。您可以注册一个对象,以便在目标数据线打开、关闭、启动或停止时接收事件。有关更多信息,请参阅之前关于监控线路状态的讨论。
处理传入音频
像一些源数据线一样,一些混音器的目标数据线具有信号处理控件,例如增益、声像、混响或采样率控件。输入端口可能具有类似的控件,特别是增益控件。在下一节中,您将学习如何确定一条线是否具有这些控件,以及如何在有这些控件的情况下使用它们。
使用控件处理音频
原文:
docs.oracle.com/javase/tutorial/sound/controls.html
先前的部分已经讨论了如何播放或捕获音频样本。隐含的目标是尽可能忠实地传递样本,而不进行修改(除了可能将样本与其他音频线路的样本混合)。然而,有时您可能希望能够修改信号。用户可能希望声音听起来更响亮、更安静、更丰满、更具混响、音高更高或更低等。本页讨论了提供这些类型信号处理的 Java Sound API 功能。
有两种应用信号处理的方式:
-
您可以通过查询
Control
对象并根据用户的需求设置控件来使用混音器或其组件线路支持的任何处理。混音器和线路通常支持的控件包括增益、声像和混响控件。 -
如果混音器或其线路提供的处理方式不符合您的需求,您的程序可以直接操作音频字节,根据需要进行操作。
本页更详细地讨论了第一种技术,因为第二种技术没有专门的 API。
控件介绍
混音器的某些或所有线路上可以有各种信号处理控件。例如,用于音频捕获的混音器可能具有带增益控制的输入端口,以及带增益和声像控制的目标数据线。用于音频播放的混音器可能在其源数据线上具有采样率控制。在每种情况下,所有控件都通过Line
接口的方法访问。
因为Mixer
接口扩展了Line
,混音器本身可以具有自己的一组控件。这些控件可能作为主控件,影响所有混音器的源或目标线路。例如,混音器可能具有主增益控制,其分贝值会添加到其目标线路上各个增益控制的值中。
混音器的其他控件可能会影响一个特殊的线路,既不是源也不是目标,混音器在内部用于其处理。例如,全局混响控制可能选择应用于输入信号混合的混响类型,这个“湿”(混响)信号会在传递到混音器的目标线路之前混合回“干”信号中。
如果混音器或其任何线路具有控件,则您可能希望通过程序用户界面中的图形对象公开控件,以便用户可以根据需要调整音频特性。这些控件本身并不是图形化的;它们只是允许您检索和更改其设置。由您决定在程序中使用何种图形表示(滑块、按钮等)。
所有控件都作为抽象类Control
的具体子类实现。许多典型的音频处理控件可以通过基于数据类型(如布尔值、枚举值或浮点数)的Control
的抽象子类来描述。例如,布尔控件代表二进制状态控件,如静音或混响的开/关控件。另一方面,浮点控件非常适合表示连续可变控件,如声道、平衡或音量。
Java 音频 API 指定了以下Control
的抽象子类:
-
BooleanControl
— 代表二进制状态(真或假)控件。例如,静音、独奏和开/关开关都是BooleanControls
的良好候选。 -
FloatControl
— 提供对一系列浮点值的控制的数据模型。例如,音量和声道是可以通过旋钮或滑块操作的FloatControls
。 -
EnumControl
— 提供从一组对象中进行选择的选项。例如,您可以将用户界面中的一组按钮与EnumControl
关联起来,以选择几个预设混响设置中的一个。 -
CompoundControl
— 提供对一组相关项目的访问,其中每个项目本身都是Control
子类的实例。CompoundControls
代表多控件模块,如图形均衡器。(图形均衡器通常由一组滑块表示,每个滑块影响一个FloatControl
。)
上述每个Control
的子类都有适用于其基础数据类型的方法。大多数类包括设置和获取控件当前值的方法,获取控件标签等。
当然,每个类都有特定于它和类所代表的数据模型的方法。例如,EnumControl
有一个方法让您获取其可能值的集合,而FloatControl
允许您获取其最小和最大值,以及控件的精度(增量或步长)。
每个Control
的子类都有一个对应的Control.Type
子类,其中包括标识特定控件的静态实例。
以下表格显示了每个Control
子类、其对应的Control.Type
子类以及指示特定控件类型的静态实例:
Control | Control.Type | Control.Type 实例 |
---|---|---|
BooleanControl | BooleanControl.Type | MUTE – 线路静音状态 APPLY_REVERB – 混响开/关 |
CompoundControl | CompoundControl.Type | (无) |
EnumControl | EnumControl.Type | REVERB – 访问混响设置(每个都是 ReverbType 的实例) |
| FloatControl
| FloatControl.Type
| AUX_RETURN
– 线路上的辅助返回增益 AUX_SEND
– 线路上的辅助发送增益
BALANCE
– 左右音量平衡
MASTER_GAIN
– 线路上的总增益
PAN
– 左右位置
REVERB_RETURN
– 线路上的后混响增益
REVERB_SEND
– 线路上的前混响增益
SAMPLE_RATE
– 播放采样率
VOLUME
– 线路上的音量 |
Java Sound API 的实现可以在其混音器和线路上提供任何或所有这些控件类型。它还可以提供 Java Sound API 中未定义的其他控件类型。这些控件类型可以通过这四个抽象子类的具体子类或不继承这四个抽象子类的其他 Control
子类来实现。应用程序可以查询每条线路以查找它支持的控件。
获取具有所需控件的线路
在许多情况下,应用程序将简单地显示该线路支持的任何控件。如果线路没有任何控件,那就算了。但是如果重要的是找到具有特定控件的线路呢?在这种情况下,您可以使用 Line.Info
来获取具有正确特征的线路,如之前在 获取所需类型的线路 中描述的。
例如,假设您希望有一个输入端口,让用户设置声音输入的音量。以下代码摘录显示了如何查询默认混音器以确定它是否具有所需的端口和控件:
Port lineIn;
FloatControl volCtrl;
try {
mixer = AudioSystem.getMixer(null);
lineIn = (Port)mixer.getLine(Port.Info.LINE_IN);
lineIn.open();
volCtrl = (FloatControl) lineIn.getControl(
FloatControl.Type.VOLUME);
// Assuming getControl call succeeds,
// we now have our LINE_IN VOLUME control.
} catch (Exception e) {
System.out.println("Failed trying to find LINE_IN"
+ " VOLUME control: exception = " + e);
}
if (volCtrl != null)
// ...
从线路获取控件
一个需要在其用户界面中公开控件的应用程序可能只需查询可用的线路和控件,然后为感兴趣的每条线路上的每个控件显示适当的用户界面元素。在这种情况下,程序的唯一任务是为用户提供对控件的“处理”,而不是知道这些控件对音频信号做了什么。只要程序知道如何将线路的控件映射到用户界面元素,Java Sound API 的 Mixer
、Line
和 Control
架构通常会处理其余部分。
例如,假设您的程序播放声音。您正在使用一个 SourceDataLine
,如之前在 获取所需类型的线路 中描述的那样获取。您可以通过调用以下 Line
方法来访问线路的控件:
Control[] getControls()
然后,对于返回的数组中的每个控件,您可以使用以下 Control
方法来获取控件的类型:
Control.Type getType()
知道特定的Control.Type
实例后,您的程序可以显示相应的用户界面元素。当然,为特定的Control.Type
选择“相应的用户界面元素”取决于您的程序采取的方法。一方面,您可能会使用相同类型的元素来表示同一类别的所有Control.Type
实例。这将需要您使用例如Object.getClass
方法查询Control.Type
实例的类。假设结果匹配了BooleanControl.Type
。在这种情况下,您的程序可能会显示一个通用的复选框或切换按钮,但如果其类匹配了FloatControl.Type
,那么您可能会显示一个图形滑块。
另一方面,您的程序可能会区分不同类型的控件,甚至是同一类别的控件,并为每个控件使用不同的用户界面元素。这将需要您测试Control's getType
方法返回的实例。然后,例如,如果类型匹配了BooleanControl.Type.APPLY_REVERB
,您的程序可能会显示一个复选框;而如果类型匹配了BooleanControl.Type.MUTE
,您可能会显示一个切换按钮。
使用控件更改音频信号
现在您知道如何访问控件并确定其类型,本节将描述如何使用Controls
来更改音频信号的各个方面。本节不涵盖所有可用的控件;相反,它提供了一些示例,以展示如何入门。这些示例包括:
-
控制线的静音状态
-
更改线路的音量
-
在各种混响预设中进行选择
假设您的程序已经访问了所有的混音器、它们的线路以及这些线路上的控件,并且它有一个数据结构来管理控件与其对应的用户界面元素之间的逻辑关联。那么,将用户对这些控件的操作转换为相应的Control
方法就变得相当简单。
以下各小节描述了必须调用的一些方法,以影响特定控件的更改。
控制线的静音状态
控制任何线的静音状态只需调用以下BooleanControl
方法:
void setValue(boolean value)
(可以假设程序通过引用其控件管理数据结构知道,静音是BooleanControl
的一个实例。)为了使通过线路传递的信号静音,程序调用上述方法,指定值为true
。要关闭静音,允许信号通过线路流动,程序调用该方法并将参数设置为false
。
更改线路的音量
假设您的程序将特定的图形滑块与特定线路的音量控制相关联。音量控制的值(即FloatControl.Type.VOLUME
)是使用以下FloatControl
方法设置的:
void setValue(float newValue)
检测到用户移动滑块后,程序会获取滑块的当前值,并将其作为参数newValue
传递给上述方法。这会改变通过拥有该控件的线路流动的信号的音量。
在各种混响预设中进行选择
假设我们的程序有一个带有类型为EnumControl.Type.REVERB
的控件的混音器。调用EnumControl
方法:
java.lang.Objects[] getValues()
在该控件上会生成一个ReverbType
对象数组。如果需要,可以使用以下ReverbType
方法访问每个对象的特定参数设置:
int getDecayTime()
int getEarlyReflectionDelay()
float getEarlyReflectionIntensity()
int getLateReflectionDelay()
float getLateReflectionIntensity()
例如,如果一个程序只想要一个听起来像洞穴的混响设置,它可以迭代ReverbType
对象,直到找到一个getDecayTime
返回大于 2000 的值。有关这些方法的详细解释,包括代表性返回值表,请参阅javax.sound.sampled.ReverbType
的 API 参考文档。
通常,程序会为getValues
方法返回的数组中的每个ReverbType
对象创建一个用户界面元素,例如单选按钮。当用户点击其中一个单选按钮时,程序会调用EnumControl
方法。
void setValue(java.lang.Object value)
其中value
设置为与新选择的按钮对应的ReverbType
。通过拥有此EnumControl
的线路发送的音频信号将根据构成控件当前ReverbType
(即setValue
方法的value
参数中指定的特定ReverbType
)的参数设置而产生混响效果。
因此,从我们应用程序的角度来看,让用户从一个混响预设(即 ReverbType)移动到另一个只是将getValues
返回的数组的每个元素连接到不同的单选按钮。
直接操作音频数据
Control
API 允许 Java Sound API 的实现或混音器的第三方提供商通过控件提供任意类型的信号处理。但是如果没有混音器提供所需类型的信号处理怎么办?这需要更多的工作,但您可能可以在程序中实现信号处理。因为 Java Sound API 允许您将音频数据作为字节数组访问,您可以以任何您选择的方式修改这些字节。
如果您正在处理传入的声音,您可以从TargetDataLine
中读取字节,然后对其进行操作。一个算法上微不足道但可以产生引人入胜结果的例子是通过将其帧按相反顺序排列来倒放声音。这个微不足道的例子可能对您的程序没有太大用处,但有许多复杂的数字信号处理(DSP)技术可能更合适。一些例子包括均衡、动态范围压缩、峰值限制、时间拉伸或压缩,以及延迟、合唱、混响、失真等特效。
要播放处理过的声音,您可以将处理后的字节数组放入SourceDataLine
或Clip
中。当然,字节数组不一定需要来自现有的声音。您可以从头开始合成声音,尽管这需要一些声学知识或者访问声音合成函数。无论是处理还是合成,您可能需要查阅音频 DSP 教科书以获取您感兴趣的算法,或者将第三方信号处理函数库导入到您的程序中。对于合成声音的播放,考虑一下javax.sound.midi
包中的Synthesizer
API 是否符合您的需求。稍后在合成声音下您将了解更多关于javax.sound.midi
的内容。
使用文件和格式转换器
原文:
docs.oracle.com/javase/tutorial/sound/converters.html
大多数处理声音的应用程序需要读取声音文件或音频流。这是常见的功能,无论程序随后对读取的数据做什么(如播放、混合或处理)。同样,许多程序需要写入声音文件(或流)。在某些情况下,已读取的数据(或将要写入的数据)需要转换为不同的格式。
正如在访问音频系统资源中简要提到的,Java Sound API 为应用程序开发人员提供了各种设施,用于文件输入/输出和格式转换。应用程序可以读取、写入和在各种声音文件格式和音频数据格式之间进行转换。
采样包概述介绍了与声音文件和音频数据格式相关的主要类。作为回顾:
-
从文件中读取或写入的音频数据流由
AudioInputStream
对象表示。(AudioInputStream
继承自java.io.InputStream
。) -
此音频数据的格式由
AudioFormat
对象表示。此格式指定了音频样本本身的排列方式,但不指定它们可能存储在的文件的结构。换句话说,
AudioFormat
描述了“原始”音频数据,例如系统在从麦克风输入捕获或从声音文件解析后可能传递给程序的数据。AudioFormat
包括编码、字节顺序、通道数、采样率和每个样本的位数等信息。 -
有几种众所周知的标准声音文件格式,如 WAV、AIFF 或 AU。不同类型的声音文件具有不同的存储音频数据以及存储有关音频数据的描述信息的结构。Java Sound API 中通过
AudioFileFormat
对象表示声音文件格式。AudioFileFormat
包括一个AudioFormat
对象来描述文件中存储的音频数据的格式,还包括有关文件类型和文件中数据长度的信息。 -
AudioSystem
类提供了方法,用于将来自AudioInputStream
的音频数据流存储到特定类型的音频文件中(换句话说,写入文件),从音频文件中提取音频字节流(AudioInputStream
)(换句话说,读取文件),以及将音频数据从一种数据格式转换为另一种数据格式。本页分为三个部分,解释了这三种活动。
注意:
Java Sound API 的实现不一定提供全面的设施来读取、写入和转换不同数据和文件格式的音频。它可能仅支持最常见的数据和文件格式。然而,服务提供者可以开发和分发扩展这一集合的转换服务,正如你稍后将在提供采样音频服务中看到的那样。AudioSystem
类提供了允许应用程序了解可用转换的方法,稍后在转换文件和数据格式下描述。
读取声音文件
AudioSystem
类提供了两种类型的文件读取服务:
-
存储在声音文件中的音频数据格式的信息
-
可从声音文件中读取的格式化音频数据流
第一个是getAudioFileFormat
方法的三个变体:
static AudioFileFormat getAudioFileFormat (java.io.File file)
static AudioFileFormat getAudioFileFormat(java.io.InputStream stream)
static AudioFileFormat getAudioFileFormat (java.net.URL url)
如上所述,返回的AudioFileFormat
对象告诉你文件类型、文件中数据的长度、编码、字节顺序、通道数、采样率和每个样本的位数。
第二种文件读取功能由这些AudioSystem
方法提供。
static AudioInputStream getAudioInputStream (java.io.File file)
static AudioInputStream getAudioInputStream (java.net.URL url)
static AudioInputStream getAudioInputStream (java.io.InputStream stream)
这些方法给你一个对象(一个AudioInputStream
),让你使用AudioInputStream
的读取方法读取文件的音频数据。我们马上会看到一个例子。
假设你正在编写一个声音编辑应用程序,允许用户从文件中加载声音数据,显示相应的波形图或频谱图,编辑声音,播放编辑后的数据,并将结果保存在新文件中。或者你的程序将读取文件中存储的数据,应用某种信号处理(例如减慢声音而不改变音调的算法),然后播放处理后的音频。在任何情况下,你需要访问音频文件中包含的数据。假设你的程序提供了一些方式让用户选择或指定输入声音文件,读取该文件的音频数据涉及三个步骤:
-
从文件中获取一个
AudioInputStream
对象。 -
创建一个字节数组,用于存储文件中连续的数据块。
-
重复从音频输入流中读取字节到数组中。在每次迭代中,对数组中的字节执行一些有用的操作(例如,你可以播放它们、过滤它们、分析它们、显示它们或将它们写入另一个文件)。
以下代码片段概述了这些步骤:
int totalFramesRead = 0;
File fileIn = new File(somePathName);
// somePathName is a pre-existing string whose value was
// based on a user selection.
try {
AudioInputStream audioInputStream =
AudioSystem.getAudioInputStream(fileIn);
int bytesPerFrame =
audioInputStream.getFormat().getFrameSize();
if (bytesPerFrame == AudioSystem.NOT_SPECIFIED) {
// some audio formats may have unspecified frame size
// in that case we may read any amount of bytes
bytesPerFrame = 1;
}
// Set an arbitrary buffer size of 1024 frames.
int numBytes = 1024 * bytesPerFrame;
byte[] audioBytes = new byte[numBytes];
try {
int numBytesRead = 0;
int numFramesRead = 0;
// Try to read numBytes bytes from the file.
while ((numBytesRead =
audioInputStream.read(audioBytes)) != -1) {
// Calculate the number of frames actually read.
numFramesRead = numBytesRead / bytesPerFrame;
totalFramesRead += numFramesRead;
// Here, do something useful with the audio data that's
// now in the audioBytes array...
}
} catch (Exception ex) {
// Handle the error...
}
} catch (Exception e) {
// Handle the error...
}
让我们看看上面代码示例中发生了什么。首先,外部的try
子句通过调用AudioSystem.getAudioInputStream(File)
方法实例化了一个AudioInputStream
对象。此方法透明地执行了所有必要的测试,以确定指定的文件实际上是 Java Sound API 支持的声音文件类型。如果正在检查的文件(例如此示例中的fileIn
)不是声音文件,或者是某种不受支持的声音文件类型,将抛出UnsupportedAudioFileException
异常。这种行为很方便,因为应用程序员不需要测试文件属性,也不需要遵守任何文件命名约定。相反,getAudioInputStream
方法负责处理验证输入文件所需的所有底层解析和验证。然后,外部的try
子句创建了一个名为audioBytes
的字节数组,长度是任意固定的。我们确保其字节长度等于整数帧数,这样我们就不会最终只读取部分帧或更糟糕的是只读取部分样本。这个字节数组将作为一个缓冲区,临时保存从流中读取的一块音频数据。如果我们知道我们将只读取非常短的声音文件,我们可以使此数组与文件中的数据长度相同,通过从AudioInputStream
的getFrameLength
方法返回的帧数长度来推导其字节长度。(实际上,我们可能会使用Clip
对象。)但为了避免在一般情况下耗尽内存,我们会一次读取一个缓冲区的文件。
内部的try
子句包含一个while
循环,在这里我们将音频数据从AudioInputStream
读入字节数组中。您应该在此循环中添加代码,以适当处理此数组中的音频数据,以满足程序的需求。如果您对数据应用某种信号处理,您可能需要进一步查询AudioInputStream
的AudioFormat
,以了解每个样本的位数等信息。
注意,方法AudioInputStream.read(byte[])
返回读取的字节数,而不是样本数或帧数。当没有更多数据可读取时,此方法返回-1。一旦检测到这种情况,我们就会跳出while
循环。
写入声音文件
前一节描述了读取声音文件的基础知识,使用了AudioSystem
和AudioInputStream
类的特定方法。本节描述了如何将音频数据写入新文件。
以下AudioSystem
方法创建指定文件类型的磁盘文件。该文件将包含指定AudioInputStream
中的音频数据:
static int write(AudioInputStream in,
AudioFileFormat.Type fileType, File out)
请注意,第二个参数必须是系统支持的文件类型之一(例如,AU、AIFF 或 WAV),否则write
方法将抛出IllegalArgumentException
。为了避免这种情况,您可以通过调用此AudioSystem
方法来测试特定AudioInputStream
是否可以写入特定类型的文件:
static boolean isFileTypeSupported
(AudioFileFormat.Type fileType, AudioInputStream stream)
只有在支持特定组合时才会返回true
。
更一般地,您可以通过调用这些AudioSystem
方法来了解系统可以写入哪些类型的文件:
static AudioFileFormat.Type[] getAudioFileTypes()
static AudioFileFormat.Type[] getAudioFileTypes(AudioInputStream stream)
其中第一个返回系统可以写入的所有文件类型,第二个仅返回系统可以从给定音频输入流写入的文件类型。
以下摘录演示了使用上述提到的write
方法从AudioInputStream
创建输出文件的一种技术。
File fileOut = new File(someNewPathName);
AudioFileFormat.Type fileType = fileFormat.getType();
if (AudioSystem.isFileTypeSupported(fileType,
audioInputStream)) {
AudioSystem.write(audioInputStream, fileType, fileOut);
}
上面的第一个语句创建了一个新的File
对象fileOut
,带有用户或程序指定的路径名。第二个语句从名为fileFormat
的预先存在的AudioFileFormat
对象中获取文件类型,该对象可能已从其他声音文件(例如,在上面的读取声音文件中读取的文件)中获取。 (您可以选择提供您想要的任何支持的文件类型,而不是从其他地方获取文件类型。例如,您可以删除第二个语句,并将上面代码中的其他两个fileType
出现替换为AudioFileFormat.Type.WAVE
。)
第三个语句测试指定类型的文件是否可以从所需的AudioInputStream
写入。与文件格式一样,此流可能是先前读取的声音文件派生而来的。(如果是这样,那么您可能以某种方式处理或更改了其数据,否则有更简单的方法可以简单地复制文件。)或者也许该流包含从麦克风输入中新捕获的字节。
最后,流、文件类型和输出文件被传递给AudioSystem
.write
方法,以实现写入文件的目标。
转换文件和数据格式
请回想一下什么是格式化音频数据?,Java Sound API 区分音频文件格式和音频数据格式。这两者或多或少是独立的。粗略地说,数据格式指的是计算机表示每个原始数据点(样本)的方式,而文件格式指的是存储在磁盘上的声音文件的组织方式。每种声音文件格式都有一个定义文件头中存储信息的特定结构。在某些情况下,文件格式还包括包含某种形式元数据的结构,除了实际的“原始”音频样本。本页的其余部分将探讨 Java Sound API 的方法,这些方法使得可以进行各种文件格式和数据格式的转换。
从一种文件格式转换为另一种
本节介绍了在 Java Sound API 中转换音频文件类型的基础知识。再次,我们提出一个假设的程序,其目的是从任意输入文件中读取音频数据并将其写入类型为 AIFF 的文件中。当然,输入文件必须是系统能够读取的类型,输出文件必须是系统能够写入的类型。(在此示例中,我们假设系统能够写入 AIFF 文件。)示例程序不执行任何数据格式转换。如果输入文件的数据格式无法表示为 AIFF 文件,则程序简单地通知用户存在问题。另一方面,如果输入音频文件已经是 AIFF 文件,则程序会通知用户无需转换。
以下函数实现了刚才描述的逻辑:
public void ConvertFileToAIFF(String inputPath,
String outputPath) {
AudioFileFormat inFileFormat;
File inFile;
File outFile;
try {
inFile = new File(inputPath);
outFile = new File(outputPath);
} catch (NullPointerException ex) {
System.out.println("Error: one of the
ConvertFileToAIFF" +" parameters is null!");
return;
}
try {
// query file type
inFileFormat = AudioSystem.getAudioFileFormat(inFile);
if (inFileFormat.getType() != AudioFileFormat.Type.AIFF)
{
// inFile is not AIFF, so let's try to convert it.
AudioInputStream inFileAIS =
AudioSystem.getAudioInputStream(inFile);
inFileAIS.reset(); // rewind
if (AudioSystem.isFileTypeSupported(
AudioFileFormat.Type.AIFF, inFileAIS)) {
// inFileAIS can be converted to AIFF.
// so write the AudioInputStream to the
// output file.
AudioSystem.write(inFileAIS,
AudioFileFormat.Type.AIFF, outFile);
System.out.println("Successfully made AIFF file, "
+ outFile.getPath() + ", from "
+ inFileFormat.getType() + " file, " +
inFile.getPath() + ".");
inFileAIS.close();
return; // All done now
} else
System.out.println("Warning: AIFF conversion of "
+ inFile.getPath()
+ " is not currently supported by AudioSystem.");
} else
System.out.println("Input file " + inFile.getPath() +
" is AIFF." + " Conversion is unnecessary.");
} catch (UnsupportedAudioFileException e) {
System.out.println("Error: " + inFile.getPath()
+ " is not a supported audio file type!");
return;
} catch (IOException e) {
System.out.println("Error: failure attempting to read "
+ inFile.getPath() + "!");
return;
}
}
如前所述,此示例函数ConvertFileToAIFF
的目的是查询输入文件,以确定它是否是 AIFF 音频文件,如果不是,则尝试将其转换为 AIFF 格式,生成一个新的副本,其路径名由第二个参数指定。(作为练习,您可以尝试使此函数更通用,以便不总是转换为 AIFF,而是根据新的函数参数指定的文件类型进行转换。)请注意,副本的音频数据格式——即新文件模仿原始输入文件的音频数据格式。
大部分此函数是不言自明的,并且与 Java Sound API 无关。然而,例程中使用了一些 Java Sound API 方法,这些方法对于音频文件类型转换至关重要。这些方法调用都在上面的第二个try
子句中找到,包括以下内容:
-
AudioSystem.getAudioFileFormat
:在此处用于确定输入文件是否已经是 AIFF 类型。如果是,则函数会快速返回;否则,转换尝试继续进行。 -
AudioSystem.isFileTypeSupported
:指示系统是否可以写入包含来自指定AudioInputStream
的音频数据的指定类型文件。在我们的示例中,如果指定的音频输入文件可以转换为 AIFF 音频文件格式,则此方法返回true
。如果不支持AudioFileFormat.Type.AIFF
,ConvertFileToAIFF
会发出警告,指出无法转换输入文件,然后返回。 -
AudioSystem.write
:在此处用于将inFileAIS
的音频数据从AudioInputStream
写入到输出文件outFile
中。
这些方法中的第二个isFileTypeSupported
方法有助于在写入之前确定特定输入音频文件是否可以转换为特定输出音频文件类型。在下一节中,我们将看到如何通过对ConvertFileToAIFF
示例例程进行一些修改,可以转换音频数据格式以及音频文件类型。
在不同数据格式之间转换音频
前一节展示了如何使用 Java Sound API 将文件从一种文件格式(即一种声音文件类型)转换为另一种。本节探讨了一些方法,这些方法使音频数据格式转换成为可能。
在前一节中,我们从一个任意类型的文件中读取数据,并将其保存在一个 AIFF 文件中。请注意,尽管我们改变了用于存储数据的文件类型,但我们并没有改变音频数据本身的格式。(大多数常见的音频文件类型,包括 AIFF,可以包含各种格式的音频数据。)因此,如果原始文件包含 CD 音质音频数据(16 位样本大小、44.1kHz 采样率和两个声道),那么我们的输出 AIFF 文件也会包含相同的数据。
现在假设我们想要指定输出文件的数据格式,以及文件类型。例如,也许我们正在保存许多长文件以供在互联网上使用,并且担心我们的文件所需的磁盘空间和下载时间。我们可能选择创建包含低分辨率数据的较小的 AIFF 文件-例如,具有 8 位样本大小、8kHz 采样率和单声道的数据。
不像之前那样详细地进行编码,让我们探讨一些用于数据格式转换的方法,并考虑我们需要对ConvertFileToAIFF
函数进行的修改以实现新目标。
音频数据转换的主要方法再次在AudioSystem
类中找到。这个方法是getAudioInputStream
的一个变体:
AudioInputStream getAudioInputStream(AudioFormat
format, AudioInputStream stream)
此函数返回一个AudioInputStream
,该流是使用指定的AudioFormat``format
转换stream
的结果。如果AudioSystem
不支持转换,此函数会抛出IllegalArgumentException
。
为了避免这种情况,我们可以首先通过调用这个AudioSystem
方法来检查系统是否可以执行所需的转换:
boolean isConversionSupported(AudioFormat targetFormat,
AudioFormat sourceFormat)
在这种情况下,我们将stream.getFormat()
作为第二个参数传递。
要创建一个特定的AudioFormat
对象,我们使用下面显示的两个AudioFormat
构造函数之一,要么:
AudioFormat(float sampleRate, int sampleSizeInBits,
int channels, boolean signed, boolean bigEndian)
使用线性 PCM 编码和给定参数构造一个AudioFormat
,或者:
AudioFormat(AudioFormat.Encoding encoding,
float sampleRate, int sampleSizeInBits, int channels,
int frameSize, float frameRate, boolean bigEndian)
还构造了一个AudioFormat
,但允许您指定编码、帧大小和帧速率,除了其他参数。
现在,有了上面的方法,让我们看看如何扩展我们的ConvertFileToAIFF
函数以执行所需的“低分辨率”音频数据格式转换。首先,我们将构造一个描述所需输出音频数据格式的AudioFormat
对象。以下语句就足够了,并且可以插入到函数的顶部附近:
AudioFormat outDataFormat = new AudioFormat((float) 8000.0,
(int) 8, (int) 1, true, false);
由于上面的AudioFormat
构造函数描述了一个具有 8 位样本的格式,因此构造函数的最后一个参数,指定样本是大端还是小端,是无关紧要的。(大端与小端只有在样本大小大于一个字节时才是一个问题。)
以下示例展示了我们如何使用这个新的AudioFormat
来转换我们从输入文件创建的AudioInputStream
,inFileAIS
:
AudioInputStream lowResAIS;
if (AudioSystem.isConversionSupported(outDataFormat,
inFileAIS.getFormat())) {
lowResAIS = AudioSystem.getAudioInputStream
(outDataFormat, inFileAIS);
}
不管我们在何处插入这段代码,只要它在构建inFileAIS
之后即可。如果没有isConversionSupported
测试,如果请求的特定转换不受支持,调用将失败并抛出IllegalArgumentException
。(在这种情况下,控制将转移到我们函数中适当的catch
子句。)
因此,在这个过程中,我们将产生一个新的AudioInputStream
,这是通过将原始输入文件(以其AudioInputStream
形式)转换为由outDataFormat
定义的所需低分辨率音频数据格式而产生的。
生成所需的低分辨率 AIFF 声音文件的最后一步是将AudioSystem.write
调用中的AudioInputStream
参数(即第一个参数)替换为我们转换后的流lowResAIS
,如下所示:
AudioSystem.write(lowResAIS, AudioFileFormat.Type.AIFF,
outFile);
对我们之前的函数进行这几处修改,可以产生一个能够转换任何指定输入文件的音频数据和文件格式的东西,前提是系统支持转换。
学习可用的转换方式
几个AudioSystem
方法会测试它们的参数,以确定系统是否支持特定的数据格式转换或文件写入操作。(通常,每个方法都与另一个方法配对,执行数据转换或写入文件。)其中一个查询方法AudioSystem.isFileTypeSupported
在我们的示例函数ConvertFileToAIFF
中被使用,以确定系统是否能够将音频数据写入 AIFF 文件。相关的AudioSystem
方法getAudioFileTypes(AudioInputStream)
返回给定流支持的所有文件类型的完整列表,作为AudioFileFormat.Type
实例的数组。该方法:BEGINCODE boolean isConversionSupported(AudioFormat.Encoding encoding,
AudioFormat 格式)
用于确定是否可以从具有指定音频格式的音频输入流中获取具有指定编码的音频输入流。类似地,该方法:
boolean isConversionSupported(AudioFormat newFormat,
AudioFormat oldFormat)
告诉我们是否可以通过将具有音频格式oldFormat
的AudioInputStream
转换为具有指定音频格式newFormat
的AudioInputStream
来获得AudioInputStream
。(这个方法在前一节代码片段中被调用,用于创建低分辨率音频输入流lowResAIS
。)
这些与格式相关的查询有助于在尝试使用 Java Sound API 执行格式转换时防止错误。