Изображения и звук

Как уже упоминалось в предыдущих главах, изображение в Java — это объект класса Image. В главе 18 было показано, как в апплетах применяются методы getImage ( ) для создания этих объектов из графических файлов.

Приложения могут применять аналогичные методы getimage() класса Toolkit из пакета java.awt с одним аргументом типа String или url. Обращение к этим методам из компонента выполняется через метод getToolkit ( ) класса Component и выглядит так:

Image img = getToolkit().getImage("C:imagesIvanov.gif");

В общем случае обращение можно сделать через статический метод getDefaultToolkit () класса Toolkit:

Image img = Toolkit.getDefaultToolkit().getImage("C:imagesIvanov.gif");

Но кроме этих методов класс Toolkit содержит пять методов createImage(), возвращающих ссылку на объект типа Image:

? createImage (String fileName) — создает изображение из содержимого графического файла с именем filename;

? createImage (url address) — создает изображение из содержимого графического файла, расположенного по адресу URL address;

? createImage (byte [ ] imageData) — создает изображение из массива байтов imageData, данные в котором должны иметь формат GIF или JPEG;

? createImage(byte[] imageData, int offset, int length) — создает изображение из части массива imageData, начинающейся с индекса offset длиной length байтов;

? createImage (ImageProducer producer) — создает изображение, полученное от поставщика producer.

Последний метод есть и в классе Component. Он использует модель "поставщик-потребитель", которая требует более подробного объяснения.

Модель "поставщик-потребитель"

Очень часто изображение перед выводом на экран подвергается обработке: меняются цвета отдельных пикселов или целых участков изображения, выделяются и преобразуются какие-то фрагменты изображения.

В библиотеке AWT применяются две модели обработки изображения. Одна модель реализует давно известную в программировании общую модель "поставщик-потребитель" (Producer-Consumer). Согласно этой модели один объект, "поставщик", генерирует сам или преобразует полученную из другого места продукцию, в данном случае набор пикселов, и передает другим объектам. Эти объекты, "потребители", принимают продукцию и тоже преобразуют ее при необходимости. Только после этого создается объект класса Image и изображение выводится на экран. У одного поставщика может быть несколько потребителей, которые должны быть зарегистрированы поставщиком. Поставщик и потребитель активно взаимодействуют, обращаясь к методам друг друга.

В графической библиотеке AWT эта модель описана в двух интерфейсах: ImageProducer и ImageConsumer пакета j ava. awt. image.

Интерфейс ImageProducer описывает пять методов:

? addConsumer(ImageConsumer ic) — регистрирует потребителя ic;

? removeConsumer(ImageConsumer ic) — отменяет регистрацию;

? isConsumer(ImageConsumer ic) — логический метод; проверяет, зарегистрирован ли потребитель ic;

? startProduction (ImageConsumer ic) — регистрирует потребителя ic и начинает поставку изображения всем зарегистрированным потребителям;

? requestTopDownLeftRightResend(ImageConsumer ic) — используется потребителем для того, чтобы затребовать изображение еще раз в порядке "сверху вниз, слева направо" для методов обработки, применяющих именно такой порядок.

С каждым экземпляром класса Image связан объект, реализующий интерфейс ImageProducer. Его можно получить методом getSource () класса Image.

Самая простая реализация интерфейса ImageProducer — класс MemoryImageSource — создает пикселы в оперативной памяти по массиву байтов или целых чисел. Вначале создается массив pix, содержащий цвет каждой точки. Затем одним из шести конструкторов создается объект класса MemoryImageSource. Он может быть обработан потребителем или прямо преобразован в тип Image методом createImage ( ).

В листинге 20.1 приведена простая программа, выводящая на экран квадрат размером 100x100 пикселов. Левый верхний угол квадрата — синий, левый нижний — красный, правый верхний — зеленый, а к центру квадрата цвета перемешиваются.

Листинг 20.1. Изображение, построенное по точкам

import java.awt.*; import java.awt.event.*; import java.awt.image.*; import javax.swing.*;

public class InMemory extends JFrame{ private int w = 100, h = 100; private int[] pix = new int[w * h]; private Image img;

InMemory(String s){ super(s); int i = 0;

for (int y = 0; y < h; y++){ int red = 255 * y / (h — 1); for (int x = 0; x < w; x++){ int green = 255 * x / (w — 1);

pix[i++] = (255 << 24) | (red << 16) | (green << 8) | 128;

}

}

setSize(250, 200); setVisible(true);

setDefaultCloseOperation(EXIT ON CLOSE);

}

public void paint(Graphics gr){ i f (img == null)

img = createImage(new MemoryImageSource(w, h, pix, 0, w)); gr.drawImage(img, 50, 50, this);

}

public static void main(String[] args){ new InMemory(" Изображение в памяти");

}

}

В листинге 20.1 в конструктор класса-поставщика MemoryImageSource(w, h, pix, 0, w) заносится ширина w и высота h изображения, массив pix, смещение в этом массиве 0 и длина строки w. Потребителем служит изображение img, которое создается методом createImage ( ) и выводится на экран методом drawImage(img, 50, 50, this). Левый верхний угол изображения img располагается в точке (50, 50) контейнера, а последний аргумент this показывает, что роль ImageObserver играет сам класс InMemory. Это заставляет включить в метод paint () проверку if (img == null), иначе изображение будет постоянно перерисовываться. Другой способ избежать этого- переопределить метод imageUpdate ( ),

о чем уже говорилось в главе 18, просто написав в нем оператор return true.

Рисунок 20.1 демонстрирует вывод этой программы. К сожалению, черно-белая печать не передает смешение цветов.

Интерфейс ImageConsumer описывает семь методов, самыми важными из которых являются два метода setPixels (). Сигнатура первого метода выглядит так:

setPixels(int x, int y, int width, int height, ColorModel model, byte[] pix, int offset, int scansize);

Второй метод отличается только тем, что массив pix содержит элементы типа int, а не

byte.

К этим методам обращается поставщик для передачи пикселов потребителю. Передается прямоугольник шириной width и высотой height c заданным верхним левым углом (x, y), заполняемый пикселами из массива pix, начиная с индекса offset. Каждая строка занимает scansize элементов массива pix. Цвета пикселов определяются в цветовой модели model (обычно это модель RGB).

Object

— ColorModel ComponentColorModel

Рис. 20.1. Изображение, созданное по точкам

[— IndexColorModel I— PackedColorModel

I— DirectColorModel

— FilteredlmageSource

— ImageFilter —i— CropImageFilter

v- RGBImageFilter I— ReplicateScaleFilter

I—AreaAveragingScaleFilter

— MemoryImageSource

— PixelGrabber

Рис. 20.2. Классы, реализующие модель "поставщик-потребитель"

На рис. 20.2 показана иерархия классов, реализующих модель "поставщик-потребитель".

Классы-фильтры

Интерфейс ImageConsumer нет нужды реализовывать, обычно используется его готовая реализация — класс ImageFilter. Несмотря на название, этот класс не производит никакой фильтрации, он передает изображение без изменений. Для преобразования изображений данный класс следует расширить, переопределив метод setPixels (). Результат преобразования следует передать потребителю, роль которого играет защищенное (protected) поле consumer этого класса.

В пакете java.awt.image есть четыре готовых расширения класса ImageFilter:

? CropImageFilter(int x, int y, int w, int h) — выделяет фрагмент изображения, указанный в приведенном конструкторе;

? RGBImageFilter — позволяет изменять отдельные пикселы; это абстрактный класс, он требует расширения и переопределения своего метода filterRGB ();

? ReplicateScaleFilter(int w, int h) — изменяет размеры изображения на указанные в приведенном конструкторе, дублируя строки и/или столбцы при увеличении размеров или убирая некоторые из них при уменьшении;

? AreaAveragingScaleFilter ( int w, int h) -расширение предыдущего класса; использу

ет более сложный алгоритм изменения размеров изображения, усредняющий значения соседних пикселов.

Применяются эти классы совместно со вторым классом-поставщиком, реализующим интерфейс ImageProducer — классом FilteredImageSource. Этот класс преобразует уже готовую продукцию, полученную от другого поставщика producer, используя для преобразования объект filter класса-фильтра ImageFilter или его подкласса. Оба объекта задаются в конструкторе

FilteredImageSource(ImageProducer producer, ImageFilter filter);

Все это кажется очень запутанным, но схема применения фильтров всегда одна и та же. Она показана в листингах 20.2—20.4.

Как выделить фрагмент изображения

В листинге 20.2 выделяется фрагмент изображения и выводится на экран в увеличенном виде. Кроме того, ниже выводятся изображения, увеличенные с помощью классов

ReplicateScaleFilter и AreaAveragingScaleFilter.

Листинг 20.2. Примеры масштабирования изображения j

import java.awt.*; import java.awt.event.*; import java.awt.image.*; import javax.swing.*;

public class CropTest extends JFrame{

private Image img, cropimg, replimg, averimg;

CropTest(String s){ super(s);

// 1. Создаем изображение — объект класса Image. img = getToolkit().getImage("javalogo52x88.gif");

// 2. Создаем объекты-фильтры:

// а) выделяем левый верхний угол размером 30x30,

CropImageFilter crp =

new CropImageFilter(0, 0, 30, 30);

// б) увеличиваем изображение в два раза простым методом,

ReplicateScaleFilter rsf =

new ReplicateScaleFilter(104, 176);

// в) увеличиваем изображение в два раза с усреднением.

AreaAveragingScaleFilter asf =

new AreaAveragingScaleFilter(104, 176);

// 3. Создаем измененные изображения. cropimg = createImage(new FilteredImageSource(img.getSource(), crp)); replimg = createImage(new FilteredImageSource(img.getSource(), rsf)); averimg = createImage(new FilteredImageSource(img.getSource(), asf)); setSize(400, 350); setVisible(true);

setDefaultCloseOperation(EXIT ON CLOSE);

}

public void paint(Graphics g){ g.drawImage(img, 10, 40, this); g.drawImage(cropimg, 150, 40, 100, 100, this); g.drawImage(replimg, 10, 150, this); g.drawImage(averimg, 150, 150, this);

}

public static void main(String[] args){ new CropTest(" Масштабирование");

}

}

На рис. 20.3, слева сверху, показано исходное изображение, справа — увеличенный фрагмент, внизу — изображение, увеличенное двумя способами.

Рис. 20.3. Масштабированное изображение

Как изменить цвет изображения

В листинге 20.3 меняются цвета каждого пиксела изображения. Это достигается просто сдвигом rgb >> 1 содержимого пиксела на один бит вправо в методе filterRGB(). При этом усиливается красная составляющая цвета. Метод filterRGB() переопределен в расширении ColorFilter класса RGBImageFilter.

Листинг 20.3. Изменение цвета всех пикселов

import java.awt.*; import java.awt.event.*; import java.awt.image.*; import javax.swing.*;

public class RGBTest extends JFrame{ private Image img, newimg;

RGBTest(String s){ super(s);

img = getToolkit().getImage(Mjavalogo52x88.gifM);

RGBImageFilter rgb = new ColorFilter();

newimg = createImage(new FilteredImageSource(img.getSource(), rgb)); setSize(400, 350); setVisible(true);

setDefaultCloseOperation(EXIT ON CLOSE);

}

public void paint(Graphics g){ g.drawImage(img, 10, 40, this);

g.drawImage(newimg, 150, 40, this);

}

public static void main(String[] args){ new RGBTest(" Изменение цвета");

}

}

class ColorFilter extends RGBImageFilter{

ColorFilter(){

canFilterIndexColorModel = true;

}

public int filterRGB(int x, int y, int rgb){ return rgb >> 1;

}

}

Как переставить пикселы изображения

В листинге 20.4 определяется преобразование пикселов изображения. Создается новый фильтр — расширение ShiftFilter класса ImageFilter, сдвигающее изображение циклически вправо на указанное в конструкторе число пикселов. Все, что для этого нужно, — это переопределить метод setPixels ().

Листинг 20.4. Циклический сдвиг изображения

import java.awt.*; import java.awt.event.*; import java.awt.image.*; import javax.swing.*;

public class ShiftImage extends JFrame{ private Image img, newimg;

ShiftImage(String s){ super(s);

// 1. Получаем изображение из файла. img = getToolkit().getImage("javalogo52x88.gif");

// 2. Создаем экземпляр фильтра.

ImageFilter imf = new ShiftFilter(26); // Сдвиг на 26 пикселов.

// 3. Получаем новые пикселы с помощью фильтра.

ImageProducer ip = new FilteredImageSource(img.getSource(), imf);

// 4. Создаем новое изображение. newimg = createImage(ip); setSize(300, 200); setVisible(true);

setDefaultCloseOperation(EXIT ON CLOSE);

}

public void paint(Graphics gr){

gr.drawImage(img, 20, 40, this);

gr.drawImage(newimg, 100, 40, this);

public static void main(String[] args){

new ShiftImage(" Циклический сдвиг изображения");

}

}

// Класс-фильтр

class ShiftFilter extends ImageFilter{

private int sh; // Сдвиг на sh пикселов вправо.

public ShiftFilter(int shift){ sh = shift; } public void setPixels(int x, int y, int w, int h,

ColorModel m, byte[] pix, int off, int size){ for (int k = x; k < x+w; k++){ if (k+sh <= w)

consumer.setPixels(k, y, 1, h, m, pix, off+sh+k, size); else

consumer.setPixels(k, y, 1, h, m, pix, off+sh+k-w, size);

}

}

}

Как видно из листинга 20.4, переопределение метода setPixels () заключается в том, чтобы изменить аргументы этого метода, переставив тем самым пикселы изображения, и передать их потребителю consumer — полю класса ImageFilter методом setPixels () потребителя.

На рис. 20.4 показан результат выполнения этой программы.

Рис. 20.4. Перестановка пикселов изображения

Упражнения

1. Создайте "лупу", увеличивающую центр изображения.

2. Измените изображение, заменив цвет каждого пиксела дополнительным цветом.

3. Измените в листинге 20.4 метод setPixels() так, чтобы он давал зеркальное изображение, а не сдвиг.

Модель обработки прямым доступом

Вторая модель обработки изображения введена в Java 2D. Она названа моделью прямого доступа (immediate mode model).

Подобно тому как вместо класса Graphics система Java 2D использует его расширение Graphics2D, описанное в главе 9, вместо класса Image в Java 2D употребляется его расширение -класс BufferedImage. В конструкторе этого класса

BufferedImage(int width, int height, int imageType);

задаются размеры изображения и способ хранения точек — одна из констант:

TYPE_INT_RGB TYPE_INT_ARGB TY PE_INT_ARGB_PRE TYPE_INT_BRG TYPE 3BYTE BRG

TYPE_4BYTE_ABRG TYPE_4BYTE_ABRG_PRE TYPE_BYTE_GRAY TYPE_BYTE_BINARY TYPE BYTE INDEXED

TY PE_USHORT_565_RGB TYPE_USHORT_555_RGB TYPE USHORT GRAY

Как видите, каждый пиксел может занимать 4 байта — int, 4byte, или 2 байта — ushort, или 1 байт — byte. Может использоваться цветовая модель rgb, или добавлена альфа-составляющая — ARGB, или задан другой порядок расположения цветовых составляющих — BRG, или заданы градации серого цвета — GRAY. Каждая составляющая цвета может занимать один байт, 5 или 6 битов.

Экземпляры класса BufferedImage редко создаются конструкторами. Для их создания чаще обращаются к методам createImage () класса Component с простым приведением типа:

BufferedImage bi = (BufferedImage)createImage(width, height);

При этом экземпляр bi получает характеристики компонента: цвет фона и цвет рисования, способ хранения точек.

Расположение точек в изображении регулируется классом Raster или его подклассом WritableRaster. Эти классы задают систему координат изображения, предоставляют доступ к отдельным пикселам методами getPixel(), позволяют выделять фрагменты изображения методами getPixels (). Класс WritableRaster дополнительно разрешает изменять отдельные пикселы методами setPixel () или целые фрагменты изображения методами setPixels() и setRect().

Начало системы координат изображения — левый верхний угол — имеет координаты (minx, minY), не обязательно равные нулю.

При создании экземпляра класса BufferedImage автоматически формируется связанный с ним экземпляр класса WritableRaster.

Точки изображения хранятся в скрытом буфере, содержащем одномерный или двумерный массив точек. Вся работа с буфером осуществляется методами одного из классов

DataBufferByte, DataBufferInt, DataBufferShort, DataBufferUShort в зависимости от длины данных. Общие свойства этих классов собраны в их абстрактном суперклассе DataBuffer. В нем определены типы данных, хранящихся в буфере: type_byte, type_ushort, TYPE_INT, TYPE_UNDEFINED.

Методы класса DataBuffer предоставляют прямой доступ к данным буфера, но удобнее и безопаснее обращаться к ним методами классов Raster и WritableRaster.

При создании экземпляра класса Raster или класса WritableRaster создается экземпляр соответствующего подкласса класса DataBuffer.

Чтобы отвлечься от способа хранения точек изображения, Raster может обращаться не к буферу DataBuffer, а к подклассам абстрактного класса sampleModel, рассматривающим не отдельные байты буфера, а составляющие (samples) цвета. В модели RGB — это красная, зеленая и синяя составляющие.

В пакете java.awt.image есть пять подклассов класса SampleModel:

? ComponentSampleModel — каждая составляющая цвета хранится в отдельном элементе массива DataBuffer;

? BandedSampleModel — данные хранятся по составляющим, составляющие одного цвета находятся обычно в одном массиве, а DataBuffer содержит двумерный массив: по массиву для каждой составляющей; данный класс расширяет класс

ComponentSampleModel;

? PixelInterleavedSampleModel — все составляющие цвета одного пиксела хранятся в соседних элементах единственного массива DataBuffer; данный класс расширяет класс ComponentSampleModel;

? MultiPixelPackedSampleModel — цвет каждого пиксела содержит только одну составляющую, которая может быть упакована в один элемент массива DataBuffer;

? singlePixelPackedSampleModel — все составляющие цвета каждого пиксела хранятся в одном элементе массива DataBuffer.

На рис. 20.5 представлена иерархия классов Java 2D, реализующая модель прямого доступа.

Итак, Java 2D создает сложную и разветвленную трехслойную систему DataBuffer — SampleModel — Raster управления данными изображения BufferedImage. Вы можете манипулировать точками изображения, используя их координаты в методах классов Raster или спуститься на уровень ниже и обращаться к составляющим цвета пиксела методами классов SampleModel. Если же вам надо работать с отдельными байтами, воспользуйтесь классами DataBuffer.

Object

—Image-BufferedImage

P ByteLookupTable LshortLookupTable

— LookupTable

- DataBuffer --Kernel

-DataBufferByte

-DataBufferlnt

- DataBufferShort

- DataBufferUShort

— Raster-WritableRaster

— SampleModelrMultiPixelPackedSampleModel ^SinglePixelPackedSampleModel ^ComponentSampleModel —i— BandedSampleModel

'— PixelInterleavedSampleModel

ColorModel

ComponentColorModel IndexColorModel PackedColorModel —

DirectColorModel

Рис. 20.5. Классы, реализующие модель прямого доступа

Применять эту систему приходится редко, только при создании своего способа преобразования изображения. Стандартные же преобразования выполняются очень просто.

Преобразование изображения в Java 2D

Преобразование изображения source, хранящегося в объекте класса BufferedImage, в новое изображение destination выполняется методом

filter(BufferedImage source, BufferedImage destination);

описанным в интерфейсе BufferedImageOp. Указанный метод возвращает ссылку на новый, измененный объект destination класса BufferedImage, что позволяет задать цепочку последовательных преобразований.

Можно преобразовать только координатную систему изображения методом

filter(Raster source, WritableRaster destination);

возвращающим ссылку на измененный объект класса WritableRaster. Данный метод описан в интерфейсе RasterOp.

Способ преобразования определяется классом, реализующим эти интерфейсы, а параметры преобразования задаются в конструкторе класса.

В пакете java.awt.image есть шесть классов, реализующих интерфейсы BufferedImageOp и RasterOp:

? AffineTransformOp — выполняет аффинное преобразование изображения: сдвиг, поворот, отражение, сжатие или растяжение по осям;

? RescaleOp — изменяет интенсивность изображения;

? LookupOp — изменяет отдельные составляющие цвета изображения;

? BandCombineOp-меняет составляющие цвета в Raster;

? ColorConvertOp — изменяет цветовую модель изображения;

? ConvolveOp — выполняет свертку, позволяющую изменить контраст и/или яркость изображения, создать эффект "размытости" и другие эффекты.

Рассмотрим, как можно применить эти классы для преобразования изображения.

Аффинное преобразование изображения

Класс AffineTransform и его использование подробно разобраны в главе 9, здесь мы только применим его для преобразования изображения.

В конструкторе класса AffineTransformOp указывается предварительно созданное аффинное преобразование at и способ интерполяции interp и/или правила визуализации hints:

AffineTransformOp(AffineTransform at, int interp);

AffineTransformOp(AffineTransform at, RenderingHints hints);

Способ интерполяции — это одна из двух констант: type_nearest_neighbor (по умолчанию во втором конструкторе) или type_bilinear.

После создания объекта класса AffineTransformOp применяется метод filter ( ). При этом изображение преобразуется внутри новой области типа BufferedImage, как показано на рис. 20.6, справа. Сама область выделена черным цветом.

Другой способ аффинного преобразования изображения — применить метод

drawImage(BufferedImage img, BufferedImageOp op, int x, int y);

класса Graphics2D. При этом преобразуется вся область img, как продемонстрировано на рис. 20.6, посередине.

Рис. 20.6. Аффинное преобразование изображения

В листинге 20.5 показано, как задаются преобразования, представленные на рис. 20.6.

Обратите внимание на особенности работы с BufferedImage. Надо создать графический контекст изображения и вывести в него изображение. Эти действия кажутся лишними, но удобны для двойной буферизации, которая сейчас стала стандартом перерисовки изображений, а в библиотеке Swing выполняется автоматически.

Листинг 20.5. Аффинное преобразование изображения

import java.awt.*; import java.awt.geom.*; import java.awt.image.*; import java.awt.event.*; import javax.swing.*;

public class AffOp extends JFrame{ private BufferedImage bi;

public AffOp(String s){ super(s);

// Загружаем изображение img.

Image img = getToolkit().getImage(,,javalogo52x88.gif"); // В этом блоке организовано ожидание загрузки.

try{

MediaTracker mt = new MediaTracker(this); mt.addImage(img, 0);

mt.waitForID(0); // Ждем окончания загрузки.

}catch(Exception e){}

// Размеры создаваемой области bi совпадают // с размерами изображения img.

bi = new BufferedImage(img.getWidth(this), img.getHeight(this),

BufferedImage.TYPE_INT_RGB);

// Создаем графический контекст big изображения bi.

Graphics2D big = bi.createGraphics();

// Выводим изображение img в графический контекст big. big.drawImage(img, 0, 0, this);

}

public void paint(Graphics g){

Graphics2D g2 = (Graphics2D)g; int w = getSize().width; int h = getSize().height; int bw = bi.getWidth(this); int bh = bi.getHeight(this);

// Создаем аффинное преобразование at.

AffineTransform at = new AffineTransform(); at.rotate(Math.PI/4); // Задаем поворот на 45 градусов // по часовой стрелке вокруг левого верхнего угла.

// Затем сдвигаем изображение вправо на величину bw. at.preConcatenate(new AffineTransform(1, 0, 0, 1, bw, 0));

// Определяем область хранения bimg преобразованного // изображения. Ее размер вдвое больше исходного.

BufferedImage bimg =

new BufferedImage(2*bw, 2*bw, BufferedImage.TYPE INT RGB);

// Создаем объект biop, содержащий преобразование at.

BufferedImageOp biop = new AffineTransformOp(at,

AffineTransformOp.TYPE_NEAREST_NEIGHBOR);

// Преобразуем изображение, результат заносим в bimg. biop.filter(bi, bimg);

// Выводим исходное изображение. g2.drawImage(bi, null, 10, 30);

// Выводим измененную преобразованием biop область bi. g2.drawImage(bi, biop, w/4+3, 30);

// Выводим преобразованное внутри области bimg изображение. g2.drawImage(bimg, null, w/2+3, 30);

}

public static void main(String[] args){

JFrame f = new AffOp(" Аффинное преобразование"); f.setSize(400, 200); f.setVisible(true);

f.setDefaultCloseOperation(EXIT_ON_CLOSE);

}

}

На рис. 20.6 показано исходное изображение, преобразованная область и преобразованное внутри области изображение.

Изменение интенсивности изображения

Изменение интенсивности изображения выражается математически в умножении каждой составляющей цвета на число factor и прибавлении к результату умножения числа offset. Результат приводится к диапазону значений составляющей. После этого интенсивность каждой составляющей цвета линейно изменяется в одном и том же масштабе.

Числа factor и offset постоянны для каждого пиксела и задаются в конструкторе класса вместе с правилами визуализации hints:

RescaleOp(float factor, float offset, RenderingHints hints);

После этого остается применить метод filter ().

На рис. 20.7 интенсивность каждого цвета уменьшена вдвое, в результате белый фон стал серым, а цвета — темнее. Затем интенсивность увеличена на 70 единиц. В листинге 20.6 приведена программа, выполняющая это преобразование.

Рис. 20.7. Изменение интенсивности изображения

Листинг 20.6. Изменение интенсивности изображения

import java.awt.*; import java.awt.image.*; import java.awt.event.*; import javax.swing.*;

public class Rescale extends JFrame{ private BufferedImage bi;

public Rescale(String s){ super(s);

Image img = getToolkit().getImage("javalogo52x88.gif"); try{

MediaTracker mt = new MediaTracker(this); mt.addImage(img, 0); mt.waitForID(0);

}catch(Exception e){}

bi = new BufferedImage(img.getWidth(this), img.getHeight(this), BufferedImage.TYPE_INT_RGB);

Graphics2D big = bi.createGraphics(); big.drawImage(img, 0, 0, this);

public void paint(Graphics g){

Graphics2D g2 = (Graphics2D)g; int w = getSize().width; int bw = bi.getWidth(this); int bh = bi.getHeight(this);

BufferedImage bimg =

new BufferedImage(bw, bh, BufferedImage.TYPE INT RGB);

//---------- Начало определения преобразования -------

RescaleOp rop = new RescaleOp(0.5f, 70.0f, null); rop.filter(bi, bimg);

//---------- Конец определения преобразования --------

g2.drawImage(bi, null, 10, 30); g2.drawImage(bimg, null, w/2+3, 30);

}

public static void main(String[] args){

JFrame f = new Rescale(" Изменение интенсивности"); f.setSize(300, 200); f.setVisible(true);

f.setDefaultCloseOperation(EXIT_ON_CLOSE);

}

}

Изменение составляющих цвета

Чтобы изменить отдельные составляющие цвета, надо прежде всего посмотреть тип хранения элементов в BufferedImage, по умолчанию это type_int_rgb. Здесь три составляющие — красная, зеленая и синяя. Каждая составляющая цвета занимает один байт, все они хранятся в одном числе типа int. Затем следует сформировать таблицу новых значений составляющих. В листинге 20.7 это двумерный массив samples.

Потом заполняем данный массив нужными значениями составляющих каждого цвета. В листинге 20.7 задается ярко-красный цвет рисования и белый цвет фона. По полученной таблице создаем экземпляр класса ByteLookupTable, который свяжет эту таблицу с буфером данных. Этот экземпляр используем для создания объекта класса LookupOp. Наконец, применяем метод filter() этого класса.

В листинге 20.7 приведен только фрагмент программы. Для получения полной программы его надо вставить в листинг 20.6 вместо выделенного в нем фрагмента. Логотип Java будет нарисован ярко-красным цветом.

Листинг 20.7. Изменение составляющих цвета

//----------------Вставить в листинг 20.6---------------

byte samples[] [] = new byte[3] [256]; for (int j = 0; j < 255; j++){

samples[0][j] = (byte)(255); // Красная составляющая.

samples[1][j] = (byte)(0); // Зеленая составляющая.

samples[2][j] = (byte)(0); // Синяя составляющая.

samples[0][255] = (byte)(255); // Цвет фона — белый.

samples[1][255] = (byte)(255); samples[2][255] = (byte)(255);

ByteLookupTable blut=new ByteLookupTable(0, samples);

LookupOp lop = new LookupOp(blut, null); lop.filter(bi, bimg);

//------------------ Конец вставки ------------------------

Создание различных эффектов

В этом разделе мы рассмотрим методы создания различных цветовых эффектов.

Операция свертки (convolution) задает значение цвета точки в зависимости от цветов окружающих точек следующим образом. Пусть точка с координатами (x, y) имеет цвет, выражаемый числом A(x, y). Составляем массив из девяти вещественных чисел w(0), w(1) ... w(8). Тогда новое значение цвета точки с координатами (x, y) будет равно:

Задавая различные значения весовым коэффициентам w(i) , будем получать разные эффекты, усиливая или уменьшая влияние соседних точек.

Если сумма всех девяти чисел w(i) равна i.0f, то интенсивность цвета останется прежней. Если при этом все веса равны между собой, т. е. равны 0.nnnif, то получим эффект размытости, тумана, дымки. Если вес w(4) значительно больше остальных при общей сумме их i.0f, то возрастет контрастность, возникнет эффект графики, штрихового рисунка.

Можно свернуть не только соседние точки, но и следующие ряды точек, взяв массив весовых коэффициентов из 15 элементов (3x5, 5x3), 25 элементов (5x5) и больше.

В Java 2D свертка делается так. Сначала определяем массив весов, например:

float [] w = {0, -1, 0, -1, 5, -1, 0, -1, 0};

Затем создаем экземпляр класса Kernel — ядра свертки:

Kernel kern = new Kernel(3, 3, w);

Потом объект класса ConvolveOp с этим ядром:

ConvolveOp conv = new ConvolveOp(kern);

Все готово, применяем метод filter ():

conv.filter(bi, bimg);

В листинге 20.8 записаны действия, необходимые для создания эффекта "размытости".

Листинг 20.8. Создание различных эффектов

//-------------- Вставить в листинг 20.6 ----------------

float[] w1 = { 0.urnmf, 0.urnmf, 0.шшт,

0.mnU1f, 0.nnn11f, 0.ni1U11f, 0.mnU1f, 0.11111111f, 0.11111111f };

Kernel kern = new Kernel(3, 3, w1);

ConvolveOp cop = new ConvolveOp(kern, ConvolveOp.EDGE NO OP, null); cop1.filter(bi, bimg);

//------------- Конец вставки ------------------------------------

На рис. 20.8 представлены, слева направо, исходное изображение и изображения, преобразованные весовыми матрицами w1, w2 и w3, где матрица w1 показана в листинге 20.8, а матрицы w2 и w3 выглядят так:

float[] w2 = { 0, -1, 0,-1, 4, -1, 0, -1, 0 }; float[] w3 = { -1, -1, -1,-1, 9, -1, -1, -1, -1 };
Рис. 20.8. Создание эффектов

Упражнения

4. Сделайте аффинное преобразование изображения, растягивающее его на все окно.

5. Измените изображение, максимально увеличив интенсивность каждой составляющей цвета каждого пиксела.

6. Замените в листинге 20.8 массивы из 9 элементов на массивы из 15 или 25 элементов. Как при этом изменится изображение?

Анимация

Есть несколько способов создать анимацию. Самый простой из них — записать заранее все необходимые кадры в графические файлы, загрузить их в оперативную память в виде объектов класса Image или BufferedImage и выводить по очереди на экран.

Это сделано в листинге 20.9. Заготовлено десять кадров в файлах run1.gif, run2.gif, ..., run10.gif. Они загружаются в массив img [ ] и выводятся на экран циклически 100 раз, с задержкой в 0,1 сек.

Листинг 20.9. Простая анимация

import java.awt.*; import java.awt.event.*; import javax.swing.*;

class SimpleAnim extends JFrame{

private Image[] img = new Image[10];

private int count;

SimpleAnim(String s){ super(s);

MediaTracker tr = new MediaTracker(this); for (int k = 0; k < 10; k++){

img[k] = getToolkit().getImage(,,run,,+ (k+1)+,,.gifn); tr.addImage(img[k], 0);

}

try{

tr.waitForAll(); // Ждем загрузки всех изображений.

}catch(InterruptedException e){} setSize(400, 300); setVisible(true);

setDefaultCloseOperation(EXIT ON CLOSE);

}

public void paint(Graphics g){

g.clearRect(0, 0, getSize().width, getSize().height); g.drawImage(img[count % 10], 0, 0, this);

}

public void go(){

while(count < 100){

repaint(); // Выводим следующий кадр.

try{ // Задержка в 0,1 сек.

Thread.sleep(100);

}catch(InterruptedException e){} count++;

}

}

public static void main(String[] args){

SimpleAnim f = new SimpleAnim(" Простая анимация");

f.go ();

}

}

Обратите внимание на следующее важное обстоятельство. Мы не можем обратиться прямо к методу paint () для перерисовки окна компонента, потому что выполнение этого метода связано с операционной системой — метод paint () выполняется автоматически при каждом изменении содержимого окна, его перемещении и изменении размеров. Для запроса на перерисовку окна в классе Component есть метод repaint ( ).

Метод repaint () ждет, когда представится возможность перерисовать окно, и потом обращается к методу paint (). Для "тяжелого" компонента он вызывает метод update (Graphics g). При этом несколько обращений к repaint() могут быть выполнены исполняющей системой Java за один раз.

Метод update ( ) в классе Component просто обращается к методу paint (g), но этот метод переопределяется в подклассах класса Component. Для "легких" компонентов дело обстоит сложнее. Метод repaint () последовательно обращается к методам repaint () объемлющих "легких" контейнеров, пока не встретится "тяжелый" контейнер, чаще всего это экземпляр класса Container. В нем вызывается переопределенный метод update (), очищающий и перерисовывающий контейнер. Затем идет обращение в обратном порядке к методам update () всех "легких" компонентов в контейнере.

Отсюда следует, что для устранения мерцания "легких" компонентов необходимо переопределять метод update () первого объемлющего "тяжелого" контейнера, обращаясь в нем к методу super.update(g) или super.paint(g).

Если кадры покрывают только часть окна, причем каждый раз новую, то очистка окна методом clearRect () необходима, иначе старые кадры останутся в окне, появится "хвост". Чтобы устранить мерцание, используют прием, получивший название двойная буферизация (double buffering).

Улучшение изображения двойной буферизацией

Суть двойной буферизации в том, что в оперативной памяти создается буфер — объект

класса Image или BufferedImage - и вызывается его графический контекст, в котором

формируется изображение. Там же происходит очистка буфера, которая тоже не отражается на экране. Только после выполнения всех действий готовое изображение выводится на экран.

Все это происходит в методе update (), а метод paint ( ) только обращается к update ( ). Листинги 20.10 и 20.11 разъясняют данный прием.

Листинг 20.10. Двойная буферизация с помощью класса image

public void update(Graphics g){

int w = getSize().width, h = getSize().height;

// Создаем изображение-буфер в оперативной памяти. Image offImg = createImage(w, h);

// Получаем его графический контекст.

Graphics offGr = offImg.getGraphics();

// Меняем текущий цвет буфера на цвет фона offGr.setColor(getBackground());

// и заполняем им окно компонента, очищая буфер. offGr.fillRect(0, 0, w, h);

// Восстанавливаем текущий цвет буфера. offGr.setColor(getForeground());

// Для листинга 20.9 выводим в контекст изображение. offGr.drawImage(img[count % 10], 0, 0, this);

// Рисуем в графическом контексте буфера // (необязательное действие). paint(offGr);

// Выводим изображение-буфер на экран // (можно перенести в метод paint()).

g.drawImage(offImg, 0, 0, this);

}

// Метод paint() необязателен. public void paint(Graphics g){ update(g); }

Листинг 20.11. Двойная буферизация с помощью класса BufferedImage

public void update(Graphics g){

Graphics2D g2 = (Graphics2D)g;

int w = getSize().width, h = getSize().height;

// Создаем изображение-буфер в оперативной памяти.

BufferedImage bi = (BufferedImage)createImage(w, h);

// Создаем графический контекст буфера.

Graphics2D big = bi.createGraphics();

// Устанавливаем цвет фона. big.setColor(getBackground());

// Очищаем буфер цветом фона. big.clearRect(0, 0, w, h);

// Восстанавливаем текущий цвет. big.setColor(getForeground());

// Выводим что-нибудь в графический контекст big // ...

// Выводим буфер на экран. g2.drawImage(bi, 0, 0, this);

}

Метод двойной буферизации стал фактическим стандартом вывода изменяющихся изображений, а в библиотеке Swing он применяется автоматически.

Данный метод удобен и при перерисовке отдельных частей изображения. В этом случае в изображении-буфере рисуется неизменяемая часть изображения, а в методе paint () — то, что меняется при каждой перерисовке.

В листинге 20.12 показан второй способ анимации — кадры изображения рисуются непосредственно в программе, в методе update (), по заданному закону изменения изображения. В результате красный мячик прыгает на фоне изображения.

Листинг 20.12. Анимация рисованием

import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.awt.image.*; import javax.swing.*;

class DrawAnim1 extends JFrame{ private Image img;

private int count;

DrawAnim1(String s){ super(s);

MediaTracker tr = new MediaTracker(this); img = getToolkit().getImage("back2.jpg"); tr.addImage(img, 0);

try{

tr.waitForID(0);

}catch(InterruptedException e){} setSize(400, 400); setVisible(true);

setDefaultCloseOperation(EXIT ON CLOSE);

}

public void update(Graphics g){

Graphics2D g2 = (Graphics2D)g;

int w = getSize().width, h = getSize().height;

BufferedImage bi = (BufferedImage)createImage(w, h);

Graphics2D big = bi.createGraphics();

// Заполняем фон изображением img. big.drawImage(img, 0, 0, this);

// Устанавливаем цвет рисования. big.setColor(Color.red);

// Рисуем в графическом контексте буфера круг,

// перемещающийся по синусоиде. big.fill(new Arc2D.Double(4*count, 50+30*Math.sin(count),

50, 50, 0, 360, Arc2D.OPEN));

// Меняем цвет рисования. big.setColor(getForeground());

// Рисуем горизонтальную прямую big.draw(new Line2D.Double(0, 125, w, 125));

// Выводим изображение-буфер на экран. g2.drawImage(bi, 0, 0, this);

}

public void go(){ while(count < 100){ repaint(); try{

Thread.sleep(10);

}catch(InterruptedException e){} count++;

}

}

public static void main(String[] args){

DrawAnim1 f = new DrawAnim1(" Анимация");

f.go ();

}

}

Эффект мерцания, переливы цвета, затемнение и прочие эффекты, получающиеся заменой отдельных пикселов изображения, удобно создавать с помощью класса

MemoryImageSource. Методы newPixels () этого класса вызывают немедленную перерисовку изображения даже без обращения к методу repaint (), если перед этим выполнен метод setAnimated (true). Чаще всего применяются два метода:

? newPixels (int x, int y, int width, int height) — получателю посылается указанный аргументами прямоугольный фрагмент изображения;

? newPixels () — получателю посылается все изображение.

В листинге 20.13 показано применение этого способа. Квадрат, выведенный на экран, переливается разными цветами.

Листинг 20.13. Анимация С ПОМОЩЬЮ MemoryImageSource

import java.awt.*; import java.awt.event.*; import java.awt.image.*; import javax.swing.*;

class InMemory extends JFrame{

private int w = 100, h = 100, count; private int[] pix = new int[w * h]; private Image img;

MemoryImageSource mis;

InMemory(String s){ super(s); int i = 0;

for(int y = 0; y < h; y++){ int red = 255 * y / (h — 1); for(int x = 0; x < w; x++){

int green = 255 * x / (w — 1);

pix[i++] = (255 << 24)|(red << 16)|(green << 8)| 128;

}

}

mis = new MemoryImageSource(w, h, pix, 0, w);

// Задаем возможность анимации. mis.setAnimated(true); img = createImage(mis); setSize(350, 300); setVisible(true);

setDefaultCloseOperation(EXIT ON CLOSE);

}

public void paint(Graphics gr){ gr.drawImage(img, 10, 30, this);

}

public void go(){

while(count < 100){ int i = 0;

// Изменяем массив пикселов по некоторому закону. for(int y = 0; y < h; y++) for(int x = 0; x < w; x++)

pix[i++] = (255 << 24)|(255 + 8 * count << 16)|

(8*count << 8)| 255 + 8 * count;

// Уведомляем потребителя об изменении. mis.newPixels(); try{

Thread.sleep(100);

}catch(InterruptedException e){} count++;

}

}

public static void main(String[] args){

InMemory f= new InMemory(" Изображение в памяти");

f.go ();

}

}

Вот и все средства для анимации, остальное — умелое их применение. Комбинируя рассмотренные способы, можно добиться удивительных эффектов. В документации, приложенной к Java SE, в каталогах $JAVA_HOME/demo/applets/ и $JAVA_HOME/ demo/jfc/Java2D/src/, приведено много примеров апплетов и приложений с анимацией.

Упражнения

7. Создайте в каком-либо графическом редакторе или цифровым фотоаппаратом файлы run1.gif, run2.gif и т. д. и используйте их в программе листинга 20.9.

8. Средствами Java 2D сделайте небольшой рисованный фильм.

Звук

Как было указано в главе 18, в апплетах реализуется интерфейс AudioClip. Экземпляр объекта, реализующего этот интерфейс, можно получить методом getAudioClip(), который, кроме того, загружает звуковой файл, а затем пользоваться методами play (), loop () и stop () этого интерфейса для проигрывания музыки.

Для применения этого же приема в приложениях в класс Applet введен статический метод newAudioClip(URL address), загружающий звуковой файл, находящийся по адресу address, и возвращающий объект, реализующий интерфейс AudioClip. Его можно использовать для проигрывания звука не только в апплетах, но и в приложении, если, конечно, звуковая система компьютера уже настроена.

В листинге 20.14 приведено простейшее консольное приложение, бесконечно проигрывающее звуковой файл doom.mid, находящийся в текущем каталоге. Для завершения приложения требуется применить средства операционной системы, например комбинацию клавиш <Ctrl>+<C>.

Листинг 20.14. Простейшее аудиоприложение

import java.applet.*; import java.net.*;

class SimpleAudio{

SimpleAudio(){ try{

AudioClip ac = Applet.newAudioClip(new URL("file:doom.mid")); ac.loop();

}catch(Exception e){}

}

public static void main(String[] args){ new SimpleAudio();

}

}

Таким способом можно проигрывать звуковые файлы типов AU, WAVE, AIFF, MIDI, записанные без сжатия.

В состав виртуальной машины Java, входящей в Java SE начиная с версии 1.3, включено устройство, проигрывающее звук, записанный в одном из форматов: AU, WAVE, AIFF, MIDI, преобразующее, микширующее и записывающее звук в тех же форматах.

Для работы с этим устройством созданы классы, собранные в пакеты j avax.sound.sampled,

javax.sound.midi, javax.sound.sampled.spi и javax.sound.midi.spi. Перечисленный набор классов для работы со звуком получил название Java Sound API.

Проигрывание звука в Java

Проигрыватель звука, встроенный в JVM, рассчитан на два способа записи звука: моно-и стереооцифровку (digital audio) с частотой дискретизации (sample rate) от 8000 до 48 000 Гц и аппроксимацией (quantization) 8 и 16 битов и MIDI-последовательности (sequences) типа 0 и 1.

Оцифрованный звук должен храниться в файлах типа AU, WAV и AIFF. Его можно проигрывать двумя способами.

Первый способ описан в интерфейсе Clip. Он рассчитан на воспроизведение небольших файлов или неоднократное проигрывание файла и заключается в том, что весь файл целиком загружается в оперативную память, а затем проигрывается.

Второй способ описан в интерфейсе SourceDataLine. По этому способу файл загружается в оперативную память по частям в буфер, размер которого можно задать произвольно.

Перед загрузкой файла надо задать формат записи звука в объекте класса AudioFormat. Конструктор этого класса:

AudioFormat(float sampleRate, int sampleSize, int channels, boolean signed, boolean bigEndian);

требует знания частоты дискретизации sampleRate (по умолчанию 44 100 Гц), аппроксимации sampleSize, заданной в битах (по умолчанию 16 битов), числа каналов channels

(1 — моно, по умолчанию 2 — стерео); знания того, записаны числа со знаком (signed == true) или без знака, и порядка расположения байтов в числе bigEndian. Такие сведения обычно неизвестны, поэтому их получают косвенным образом из файла. Это осуществляется в два шага.

На первом шаге получаем формат файла статическим методом getAudioFileFormat () класса AudioSystem, на втором — формат записи звука методом getFormat () класса AudioFileFormat. Это описано в листинге 20.15. После того как формат записи определен и занесен в объект класса AudioFormat, в объекте класса DataLine.Info собирается информация о входной линии (line) и способе проигрывания: Clip или SourceDataLine. Далее следует проверить, сможет ли проигрыватель обслуживать линию с таким форматом. Затем надо связать линию с проигрывателем статическим методом getLine () класса

AudioSystem. Потом создаем поток данных из файла объект класса AudioInputStream. Из

этого потока тоже можно извлечь объект класса AudioFormat методом getFormat ( ). Данный вариант выбран в листинге 20.16. Открываем созданный поток методом open ().

У-фф! Все готово, теперь можно начать проигрывание методом start(), завершить методом stop(), "перемотать" в начало методом setFramePosition(0) или setMillisecondPosition(0).

Можно задать проигрывание n раз подряд методом loop(n) или бесконечное число раз методом loop(Clip.LOOP_CONTINUOUSLY). Перед этим необходимо установить начальную n и конечную m позиции повторения методом setLoopPoints (n, m).

По окончании проигрывания следует закрыть линию методом close ().

Вся эта последовательность действий показана в листинге 20.15.

Листинг 20.15. Проигрывание аудиоклипа

import javax.sound.sampled.*; import java.io.*;

class PlayAudio{

PlayAudio(String s){ play(s);

}

public void play(String file){

Clip line = null; try{

// Создаем объект, представляющий файл.

File f = new File(file);

// Получаем информацию о способе записи файла. AudioFileFormat aff = AudioSystem.getAudioFileFormat(f);

// Получаем информацию о способе записи звука. AudioFormat af = aff.getFormat();

// Собираем всю информацию вместе,

// добавляя сведения о классе Class.

DataLine.Info info = new DataLine.Info(Clip.class, af);

// Проверяем, можно ли проигрывать такой формат. if (!AudioSystem.isLineSupported(info)){

System.err.println("Line is not supported");

System.exit(0);

}

// Получаем линию связи с файлом. line = (Clip)AudioSystem.getLine(info);

// Создаем поток байтов из файла.

AudioInputStream ais = AudioSystem.getAudioInputStream(f);

// Открываем линию. line.open(ais);

}catch(Exception e){

System.err.println(e) ;

}

// Начинаем проигрывание. line.start();

// Здесь надо сделать задержку до окончания проигрывания // или остановить его следующим методом: line.stop();

// По окончании проигрывания закрываем линию. line.close();

}

public static void main(String[] args){ if (args.length != 1)

System.out.println("Usage: j ava PlayAudio filename"); new PlayAudio(args[0]);

}

}

Как видите, методы Java Sound API выполняют элементарные действия, которые надо повторять из программы в программу. Как говорят, это методы "низкого уровня" (low level).

Второй способ (использующий методы интерфейса SourceDataLine) требует предварительного создания буфера произвольного размера.

Листинг 20.16. Проигрывание аудиофайла

import javax.sound.sampled.*; import java.io.*;

class PlayAudioLine{

PlayAudioLine(String s){ play(s);

}

public void play(String file){ SourceDataLine line = null; AudioInputStream ais = null;

// Буфер данных.

byte[] b = new byte[2048];

try{

File f = new File(file);

// Создаем входной поток байтов из файла f. ais = AudioSystem.getAudioInputStream(f);

// Извлекаем из потока информацию о способе записи звука.

AudioFormat af = ais.getFormat();

// Заносим эту информацию в объект info.

DataLine.Info info = new DataLine.Info(SourceDataLine.class, af);

// Проверяем, приемлем ли такой способ записи звука. if (!AudioSystem.isLineSupported(info)){

System.err.println("Line is not supported");

System.exit(0);

}

// Получаем входную линию. line = (SourceDataLine)AudioSystem.getLine(info);

// Открываем линию. line.open(af);

// Начинаем проигрывание.

line.start(); // Ждем появления данных в буфере.

int num = 0;

// Раз за разом заполняем буфер. while(( num = ais.read(b)) != -1) line.write(b, 0, num);

// "Сливаем" буфер, проигрывая остаток файла. line.drain();

// Закрываем поток. ais.close();

}catch(Exception e){

System.err.println(e);

}

// Останавливаем проигрывание. line.stop();

// Закрываем линию. line.close();

}

public static void main(String[] args){

String s = "mamba.aif"; if (args.length > 0) s = args[0]; new PlayAudioLine(s);

}

}

Управлять проигрыванием файла можно с помощью событий. Событие класса LineEvent происходит при открытии, open, и закрытии, close, потока, при начале, start, и окончании, stop, проигрывания. Характер события отмечается указанными константами. Соответствующий интерфейс LineListener описывает только один метод update ( ).

В MIDI-файлах хранится последовательность (sequence) команд для секвенсора (sequencer) — устройства для записи, проигрывания и редактирования MIDI-по-

следовательности, которым может быть физическое устройство или программа. Последовательность состоит из нескольких дорожек (tracks), на которых записаны MIDI-события (events). Каждая дорожка загружается в своем канале (channel). Обычно дорожка содержит звучание одного музыкального инструмента или запись голоса одного исполнителя или нескольких исполнителей, микшированную синтезатором (synthesizer).

Для проигрывания MIDI-последовательности в простейшем случае надо создать экземпляр секвенсора, открыть его и направить в него последовательность, извлеченную из файла, как показано в листинге 20.17. После этого следует начать проигрывание методом start (). Закончить проигрывание можно методом stop(), "перемотать" последовательность на начало записи или на указанное время проигрывания — методами setMicrosecondPosition(long mcs) или setTickPosition(long tick).

Листинг 20.17. Проигрывание MIDI-последовательности

import javax.sound.midi.*; import java.io.*;

class PlayMIDI{

PlayMIDI(String s){ play(s);

}

public void play(String file){ try{

File f = new File(file);

// Получаем секвенсор по умолчанию.

Sequencer sequencer = MidiSystem.getSequencer();

// Проверяем, получен ли секвенсор.

if (sequencer == null) {

System.err.println("Sequencer is not supported"); System.exit(0);

}

// Открываем секвенсор. sequencer.open();

// Получаем MIDI-последовательность из файла.

Sequence seq = MidiSystem.getSequence(f);

// Направляем последовательность в секвенсор. sequencer.setSequence(seq);

// Начинаем проигрывание. sequencer.start();

// Здесь надо сделать задержку на время проигрывания, // а затем остановить: sequencer.stop();

}catch(Exception e){

System.err.println(e);

}

public static void main(String[] args){

String s = "doom.mid"; if (args.length > 0) s = args[0]; new PlayMIDI(s);

}

}

Синтез и запись звука в Java

Синтез звука заключается в создании MIDI-последовательности — объекта класса Sequence — каким-либо способом: с микрофона, линейного входа, синтезатора, из файла или просто создать в программе, как это делается в листинге 20.18.

Сначала создается пустая последовательность одним из двух конструкторов:

Sequence(float divisionType, int resolution);

Sequence(float divisionType, int resolution, int numTracks);

Первый аргумент, divisionType, определяет способ отсчета моментов (ticks) MIDI-событий — это одна из констант:

? ppq (Pulses Per Quarter note) — отсчеты замеряются в долях от длительности звука в четверть;

? smpte_24, smpte_25, smpte_30, smpte_30drop (Society of Motion Picture and Television Engineers) — отсчеты в долях одного кадра, при указанном числе кадров в секунду.

Второй аргумент, resolution, задает количество отсчетов в указанную единицу, например:

Sequence seq = new Sequence(Sequence.PPQ, 10);

задает 10 отсчетов в звуке длительностью в четверть.

Третий аргумент, numTracks, определяет количество дорожек в MIDI-последовательности.

Потом, если применялся первый из перечисленных ранее конструкторов, в последовательности создается одна или несколько дорожек:

Track tr = seq.createTrack();

Если применялся второй конструктор, то надо получить уже созданные конструктором дорожки:

Track[] trs = seq.getTracks();

Затем дорожки заполняются MIDI-событиями с помощью MIDI-сообщений. Есть несколько типов сообщений для разных типов событий. Наиболее часто встречаются сообщения типа ShortMessage, которые создаются конструктором по умолчанию и потом заполняются методом setMessage ():

ShortMessage msg = new ShortMessage(); msg.setMessage(ShortMessage.NOTE ON, 60, 93);

Первый аргумент указывает тип сообщения: note_on — начать звучание, note_off — прекратить звучание и т. д. Второй аргумент для типа note_on показывает высоту звука, в стандарте MIDI это числа от 0 до 127, например число 60 обозначает ноту "до" первой октавы. Третий аргумент означает "скорость" нажатия клавиши MIDI-инструмента и по-разному понимается различными устройствами.

Далее создается MIDI-событие:

MidiEvent me = new MidiEvent(msg, ticks);

Первый аргумент конструктора, msg — это сообщение, второй аргумент, ticks — время наступления события (в нашем примере проигрывания ноты "до") в единицах последовательности seq (в нашем примере в десятых долях четверти). Время отсчитывается от начала проигрывания последовательности.

Наконец, событие заносится на дорожку:

tr.add(me);

Указанные действия продолжаются, пока все дорожки не будут заполнены всеми событиями. В листинге 20.18 это делается в цикле, но обычно MIDI-события создаются в методах обработки нажатия клавиш на обычной или специальной MIDI-клавиатуре. Еще один способ — вывести на экран изображение клавиатуры и создавать MIDI-события в методах обработки нажатий кнопки мыши на этой клавиатуре.

После создания последовательности ее можно проиграть, как в листинге 20.17, или записать в файл или выходной поток. Для этого вместо метода start () надо применить метод startRecording (), который одновременно и проигрывает последовательность, и подготавливает ее к записи. Саму же запись осуществляют статические методы:

write(Sequence in, int type, File out); write(Sequence in, int type, OutputStream out);

Второй аргумент этих методов, type, задает тип MIDI-файла, который лучше всего определить для заданной последовательности seq статическим методом getMidiFileTypes(seq). Данный метод возвращает массив возможных типов. Надо воспользоваться нулевым элементом полученного массива. Все это показано в листинге 20.18.

Листинг 20.18. Создание MIDI-последовательности нот звукоряда

import javax.sound.midi.*; import java.io.*;

class SynMIDI{

SynMIDI(){

play(synth());

}

public Sequence synth(){

Sequence seq = null;

try{

// Последовательность будет отсчитывать по 10 // MIDI-событий на звук длительностью в четверть. seq = new Sequence(Sequence.PPQ, 10);

// Создаем в последовательности одну дорожку.

Track tr = seq.createTrack(); for (int k = 0; k < 100; k++){

ShortMessage msg = new ShortMessage();

// Пробегаем MIDI-ноты от номера 10 до 109. msg.setMessage(ShortMessage.NOTE ON, 10+k, 93);

// Будем проигрывать ноты через каждые 5 отсчетов. tr.add(new MidiEvent(msg, 5*k)); msg = null;

}

}catch(Exception e){

System.err.println("From synth(): "+e);

System.exit(0);

}

return seq;

}

public void play(Sequence seq){ try{

Sequencer sequencer = MidiSystem.getSequencer(); if (sequencer == null){

System.err.println("Sequencer is not supported");

System.exit(0);

}

sequencer.open(); sequencer.setSequence(seq); sequencer.startRecording();

int[] type = MidiSystem.getMidiFileTypes(seq);

MidiSystem.write(seq, type[0], new File("gammas.mid"));

}catch(Exception e){

System.err.println("From play(): " + e);

}

}

public static void main(String[] args){

new SynMIDI();

}

}

К сожалению, объем книги не позволяет коснуться темы о работе с синтезатором (synthesizer), микширования звука, работы с несколькими инструментами и прочих возможностей Java Sound API. В документации Java SE, в каталоге $JAVA_ HOME/docs/technotes/guides/sound/programmer_guide/, есть подробное руководство программиста.

Упражнение

9. Средствами Java 2D и Java Sound API сделайте небольшой рисованный фильм со звуком.

Вопросы для самопроверки

1. Что понимается под изображением в Java?

2. Как создать объект класса Image?

3. Что такое модель "поставщик-потребитель"?

4. Как создать изображение по набору пикселов?

5. Какие способы фильтрации изображения предлагает Java?

6. Как реализуется модель обработки прямым доступом?

7. Какими средствами Java 2D можно изменить изображение?

8. Какие средства анимации предлагает Java?

9. Какие звуковые файлы можно проигрывать средствами Java?

10. Можно ли создать звуковой файл средствами Java?