/**
* Portions Copyright 2003 Sun Microsystems, Inc.
* Portions Copyright 1999-2001 Language Technologies Institute,
* Carnegie Mellon University.
* All Rights Reserved. Use is subject to license terms.
*
* See the file "license.terms" for information on usage and
* redistribution of this file, and for a DISCLAIMER OF ALL
* WARRANTIES.
*/
package com.sun.speech.freetts.diphone;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sun.speech.freetts.relp.Sample;
import com.sun.speech.freetts.relp.SampleInfo;
import com.sun.speech.freetts.util.BulkTimer;
import com.sun.speech.freetts.util.Utilities;
/**
* Represents and manages the unit data for all diphones. The diphone
* data set is stored in a set of data files. These data are loaded by this
* class into internal data structures before diphone synthesis can
* occur.
* <p>
*The diphone data set is one of the largest sets of data that
* needs to be loaded by the synthesizer and therefore can add to the
* overall startup time for any system using this database. For
* certain applications, the startup time is a critical spec that
* needs to be optimized, while for other applications, startup time
* is inconsequential. This class provides settings (via system
* properties) that control how the database is to be loaded so that
* applications can tune for quick startup or optimal run time.
* <p>
* This class serves also as a testbed for comparing performance of
* the traditional java binary I/O and the new io ( <code>java.nio </code>)
* package.
* <p>
* <p> A diphone database can be loaded from a text data file, or a
* binary datafile. The binary version loads significantly faster
* than the text version. Additionally, a binary index can be
* generated and used to reduce overall memory footprint.
* <p>
* <p>
* A DiphoneUnitDatabase contains an array of frames, and an aray of
* residuals. The frames are the samples of the wave, and the
* residuals are for linear predictive coding use. This is called
* "cst_sts" (a struct) in flite.
* <p>
* Note that if 'com.sun.speech.freetts.useNewIO' is set to true and
* the input type is binary, than the JDK1.4+ new IO api is used to
* load the database.
* <p>
* The system property
* <pre>
* com.sun.speech.freetts.diphone.UnitDatabase.cacheType
* </pre>
*
* can be set to one of:
*
* <ul>
* <li> preload: database is loaded at startup
* <li> demand: database is loaded on demand
* <li> hard: database is loaded on demand but cached
* <li> soft: database is loaded on demand but cached with soft references
* </ul>
*
* This <code> cacheType </code> setting controls how the database is
* loaded. The default is to 'preload' the database. This setting
* gives best runtime performance but with longer initial startup
* cost.
*/
public class DiphoneUnitDatabase {
/** Logger instance. */
private static final Logger LOGGER =
Logger.getLogger(DiphoneUnitDatabase.class.getName());
private String name;
private int sampleRate;
private int numChannels;
private int residualFold = 1;
private float lpcMin;
private float lpcRange;
private int lineCount = 0;
private Diphone defaultDiphone;
private Map diphoneMap = null;
private Map diphoneIndex;
private SampleInfo sampleInfo;
private boolean useNewIO =
Utilities.getProperty("com.sun.speech.freetts.useNewIO",
"true").equals("true");
// cache can be 'preload' 'none', 'soft' or 'hard'
private String cacheType =
Utilities.getProperty(
"com.sun.speech.freetts.diphone.UnitDatabase.cacheType",
"preload");
private boolean useIndexing = !cacheType.equals("preload");
private boolean useCache = !cacheType.equals("demand");
private boolean useSoftCache = cacheType.equals("soft");
private final static int MAGIC = 0xFEEDFACE;
private final static int INDEX_MAGIC = 0xFACADE;
private final static int VERSION = 1;
private final static int MAX_DB_SIZE = 4 * 1024 * 1024;
private String indexName = null;
private MappedByteBuffer mbb = null;
private int defaultIndex = -1;
/**
* Creates the DiphoneUnitDatabase from the given input stream.
*
* @param url the location of the database
* @param isBinary if <code>true</code> the database is in
* binary format; otherwise it is in text format
*
* @throws IOException if there is trouble opening the DB
*/
public DiphoneUnitDatabase(URL url, boolean isBinary) throws IOException {
// MS, 22.04.2005: Commented out the "if" clause:
// indexing is applied only when useNewIO is turned on and
// data is read from a FileInputStream. This is not true when useing
// the default settings but setting
// com.sun.speech.freetts.diphone.UnitDatabase.cacheType=demand
//if (!useIndexing || useCache) {
diphoneMap = new LinkedHashMap();
//}
InputStream is = Utilities.getInputStream(url);
indexName = getIndexName(url.toString());
if (isBinary) {
loadBinary(is);
} else {
loadText(is);
}
is.close();
sampleInfo = new SampleInfo(sampleRate, numChannels,
residualFold, lpcMin, lpcRange, 0.0f);
}
/**
* Return the information about the sample data
* for this database.
*
* @return the sample info
*/
SampleInfo getSampleInfo() {
return sampleInfo;
}
/**
* Returns the index name from the databaseName.
*
* @param databaseName the database name
*
* @return the index name or null if the database is not
* a binary database.
*
* [[[ TODO the index should probably be incorporated into the
* binary database ]]]
*/
private String getIndexName(String databaseName) {
String indexName = null;
if (databaseName.lastIndexOf(".") != -1) {
indexName = databaseName.substring(0,
databaseName.lastIndexOf(".")) + ".idx";
}
return indexName;
}
/**
* Loads the database from the given input stream.
*
* @param is the input stream
*/
private void loadText(InputStream is) {
BufferedReader reader;
String line;
if (is == null) {
throw new Error("Can't load diphone db file.");
}
reader = new BufferedReader(new InputStreamReader(is));
try {
line = reader.readLine();
lineCount++;
while (line != null) {
if (!line.startsWith("***")) {
parseAndAdd(line, reader);
}
line = reader.readLine();
}
reader.close();
} catch (IOException e) {
throw new Error(e.getMessage() + " at line " + lineCount);
} finally {
}
}
/**
* Parses and process the given line. Used to process the text
* form of the database.
*
* @param line the line to process
* @param reader the source for the lines
*/
private void parseAndAdd(String line, BufferedReader reader) {
try {
StringTokenizer tokenizer = new StringTokenizer(line," ");
String tag = tokenizer.nextToken();
if (tag.equals("NAME")) {
name = tokenizer.nextToken();
} else if (tag.equals("SAMPLE_RATE")) {
sampleRate = Integer.parseInt(tokenizer.nextToken());
} else if (tag.equals("NUM_CHANNELS")) {
numChannels = Integer.parseInt(tokenizer.nextToken());
} else if (tag.equals("LPC_MIN")) {
lpcMin = Float.parseFloat(tokenizer.nextToken());
} else if (tag.equals("COEFF_MIN")) {
lpcMin = Float.parseFloat(tokenizer.nextToken());
} else if (tag.equals("COEFF_RANGE")) {
lpcRange = Float.parseFloat(tokenizer.nextToken());
} else if (tag.equals("LPC_RANGE")) {
lpcRange = Float.parseFloat(tokenizer.nextToken());
} else if (tag.equals("ALIAS")) {
String name = tokenizer.nextToken();
String origName = tokenizer.nextToken();
AliasDiphone diphone = new AliasDiphone(name, origName);
add(diphone);
} else if (tag.equals("DIPHONE")) {
String name = tokenizer.nextToken();
int start = Integer.parseInt(tokenizer.nextToken());
int mid = Integer.parseInt(tokenizer.nextToken());
int end = Integer.parseInt(tokenizer.nextToken());
int numSamples = (end - start);
int midPoint = mid - start;
if (numChannels <= 0) {
throw new Error("For diphone '"+name+"': Bad number of channels " + numChannels);
}
if (numSamples <= 0) {
throw new Error("For diphone '"+name+"': Bad number of samples " + numSamples);
}
Sample[] samples = new Sample[numSamples];
for (int i = 0; i < samples.length; i++) {
samples[i] = new Sample(reader, numChannels);
}
Diphone diphone = new Diphone(name, samples, midPoint);
add(diphone);
} else {
throw new Error("Unsupported tag " + tag);
}
} catch (NoSuchElementException nse) {
throw new Error("Error parsing db " + nse.getMessage());
} catch (NumberFormatException nfe) {
throw new Error("Error parsing numbers in db " + nfe.getMessage());
}
}
/**
* Adds the given diphone to the DB. Diphones are kept in a map so
* they can be accessed by name.
*
* @param diphone the diphone to add.
*/
private void add(Diphone diphone) {
if (diphone instanceof AliasDiphone) {
AliasDiphone adiph = (AliasDiphone) diphone;
Diphone original = (Diphone)
diphoneMap.get(adiph.getOriginalName());
if (original != null) {
adiph.setOriginalDiphone(original);
} else {
// No original was found for this alias
// -- complain, and ignore
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("For diphone alias "
+adiph.getName()+", could not find original "
+adiph.getOriginalName());
}
return;
}
}
diphoneMap.put(diphone.getName(), diphone);
if (defaultDiphone == null) {
defaultDiphone = diphone;
}
}
/**
* Looks up the diphone with the given name.
*
* @param unitName the name of the diphone to look for
*
* @return the diphone or the defaultDiphone if not found.
*/
public Diphone getUnit(String unitName) {
Diphone diphone = null;
if (useIndexing) {
diphone = getFromCache(unitName);
if (diphone == null) {
int index = getIndex(unitName);
if (index != -1) {
mbb.position(index);
try {
diphone = Diphone.loadBinary(mbb);
if (diphone != null) {
// If diphone is an alias, must also get the original
if (diphone instanceof AliasDiphone) {
AliasDiphone adiph = (AliasDiphone) diphone;
Diphone original = getUnit(adiph.getOriginalName());
if (original != null) {
adiph.setOriginalDiphone(original);
putIntoCache(unitName, adiph);
} else {
// No original was found for this alias
// -- complain, and ignore
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("For diphone alias "
+adiph.getName()+", could not find original "
+adiph.getOriginalName());
}
diphone = null;
}
} else { // a normal diphone
putIntoCache(unitName, diphone);
}
}
} catch (IOException ioe) {
System.err.println("Can't load diphone " +
unitName);
diphone = null;
}
}
}
} else {
diphone = (Diphone) diphoneMap.get(unitName);
}
if (diphone == null) {
System.err.println("Can't find diphone " + unitName);
diphone = defaultDiphone;
}
return diphone;
}
/**
* Gets the named diphone from the cache. If we are using soft
* caching, the reference may be a soft/weak reference so check to
* see if the reference is still valid, if so return it; otherwise
* invalidate it. Note that we have not had good success with weak
* caches so far. The goal is to reduce the minimum required
* memory footprint as far as possible while not compromising
* performance. In small memory systems, the weak cache would
* likely be reclaimed, giving us lower performance but with the
* ability to still be able to run. In reality, the soft caches
* did not help much. They just did not work correctly.
* [[[ TODO: test weak/soft cache behavior with new versions of
* the runtime to see if their behavior has improved ]]]
*
* @param name the name of the diphone
*
* @return the diphone or <code> null </code> if not in the cache
*/
private Diphone getFromCache(String name) {
if (diphoneMap == null) {
return null;
}
Diphone diphone = null;
if (useSoftCache) {
Reference ref = (Reference) diphoneMap.get(name);
if (ref != null) {
diphone = (Diphone) ref.get();
if (diphone == null) {
diphoneMap.remove(name);
} else {
}
}
} else {
diphone = (Diphone) diphoneMap.get(name);
}
return diphone;
}
/**
* Puts the diphone in the cache.
*
* @param diphoneName the name of the diphone
* @param diphone the diphone to put in the cache
*/
private void putIntoCache(String diphoneName, Diphone diphone) {
if (diphoneMap == null) {
return ;
}
if (useSoftCache) {
diphoneMap.put(diphoneName, new WeakReference(diphone));
} else {
diphoneMap.put(diphoneName, diphone);
}
}
/**
* Dumps the soft ref cache.
*/
private void dumpCacheSize() {
int empty = 0;
int full = 0;
System.out.println("Entries: " + diphoneMap.size());
for (Iterator i = diphoneMap.values().iterator(); i.hasNext(); ) {
Reference ref = (Reference) i.next();
if (ref.get() == null) {
empty++;
} else {
full++;
}
}
System.out.println(" empty: " + empty);
System.out.println(" full: " + full);
}
/**
* Returns the name of this DiphoneUnitDatabase.
*/
public String getName() {
return name;
}
/**
* Dumps the diphone database.
*/
public void dump() {
System.out.println("Name " + name);
System.out.println("SampleRate " + sampleRate);
System.out.println("NumChannels " + numChannels);
System.out.println("lpcMin " + lpcMin);
System.out.println("lpcRange " + lpcRange);
for (Iterator i = diphoneMap.values().iterator(); i.hasNext(); ) {
Diphone diphone = (Diphone) i.next();
diphone.dump();
}
}
/**
* Dumps a binary form of the database.
*
* @param path the path to dump the file to
*/
public void dumpBinary(String path) {
try {
FileOutputStream fos = new FileOutputStream(path);
DataOutputStream os = new DataOutputStream(fos);
int written;
os.writeInt(MAGIC);
os.writeInt(VERSION);
os.writeInt(sampleRate);
os.writeInt(numChannels);
os.writeFloat(lpcMin);
os.writeFloat(lpcRange);
os.writeInt(diphoneMap.size());
for (Iterator i = diphoneMap.values().iterator(); i.hasNext();) {
Diphone diphone = (Diphone) i.next();
diphone.dumpBinary(os);
}
os.flush();
fos.close();
} catch (FileNotFoundException fe) {
throw new Error("Can't dump binary database " +
fe.getMessage());
} catch (IOException ioe) {
throw new Error("Can't write binary database " +
ioe.getMessage());
}
}
/**
* Dumps a binary index. The database index is used if our
* cacheType is not set to 'preload' and we are loading a binary
* database. The index is a simple mapping of diphone names (the
* key) to the file position in the database. In situations where
* the entire database is not preloaded, this index can be loaded
* and used to provide quicker startup (since only the index need
* be loaded at startup) and quick access to the diphone data.
*
* @param path the path to dump the file to
*/
void dumpBinaryIndex(String path) {
try {
FileOutputStream fos = new FileOutputStream(path);
DataOutputStream dos = new DataOutputStream(fos);
dos.writeInt(INDEX_MAGIC);
dos.writeInt(diphoneIndex.keySet().size());
for (Iterator i = diphoneIndex.keySet().iterator(); i.hasNext();) {
String key = (String) i.next();
int pos = ((Integer) diphoneIndex.get(key)).intValue();
dos.writeUTF(key);
dos.writeInt(pos);
}
dos.close();
} catch (FileNotFoundException fe) {
throw new Error("Can't dump binary index " +
fe.getMessage());
} catch (IOException ioe) {
throw new Error("Can't write binary index " +
ioe.getMessage());
}
}
/**
* Loads a binary index.
*
* @param url the location of the binary index file
*/
private void loadBinaryIndex(URL url) {
diphoneIndex = new HashMap();
try {
InputStream is = Utilities.getInputStream(url);
DataInputStream dis = new DataInputStream(is);
if (dis.readInt() != INDEX_MAGIC) {
throw new Error("Bad index file format");
}
int size = dis.readInt();
for (int i = 0; i < size; i++) {
String diphoneName = dis.readUTF();
int pos = dis.readInt();
diphoneIndex.put(diphoneName, new Integer(pos));
}
dis.close();
} catch (FileNotFoundException fe) {
throw new Error("Can't load binary index " +
fe.getMessage());
} catch (IOException ioe) {
throw new Error("Can't read binary index " +
ioe.getMessage());
}
}
/**
* Gets the index for the given diphone.
*
* @param diphone the name of the diphone
*
* @return the index into the database for the diphone
*/
private int getIndex(String diphone) {
Integer index = (Integer) diphoneIndex.get(diphone);
if (index != null) {
int idx = index.intValue();
if (defaultIndex == -1) {
defaultIndex = idx;
}
return idx;
} else {
System.out.println("Can't find index entry for " + diphone);
return defaultIndex;
}
}
/**
* Loads a binary file from the input stream.
* <p>
* Note that we currently have four! methods of loading up the
* database. We were interested in the performance characteristics
* of the various methods of loading the database so we coded it
* all up.
*
* @param is the input stream to read the database
* from
*
* @throws IOException if there is trouble opening the DB
*
*/
private void loadBinary(InputStream is) throws IOException {
// we get better performance if we can map the file in
// 1.0 seconds vs. 1.75 seconds, but we can't
// always guarantee that we can do that.
if (useNewIO && is instanceof FileInputStream) {
FileInputStream fis = (FileInputStream) is;
if (useIndexing) {
loadBinaryIndex(new URL(indexName));
mapDatabase(fis);
} else {
loadMappedBinary(fis);
}
} else {
useIndexing = false; // just to make this clear
DataInputStream dis = new DataInputStream(
new BufferedInputStream(is));
loadBinary(dis);
}
}
/**
* Loads the binary data from the given input stream.
*
* @param dis the data input stream.
*/
private void loadBinary(DataInputStream dis) throws IOException {
int size;
if (dis.readInt() != MAGIC) {
throw new Error("Bad magic in db");
}
if (dis.readInt() != VERSION) {
throw new Error("Bad VERSION in db");
}
sampleRate = dis.readInt();
numChannels = dis.readInt();
lpcMin = dis.readFloat();
lpcRange = dis.readFloat();
size = dis.readInt();
for (int i = 0; i < size; i++) {
Diphone diphone = Diphone.loadBinary(dis);
add(diphone);
}
}
/**
* Loads the database from the given FileInputStream.
*
* @param is the InputStream to load the database from
*
* @throws IOException if there is trouble opening the DB
*/
private void loadMappedBinary(FileInputStream is) throws IOException {
FileChannel fc = is.getChannel();
MappedByteBuffer bb =
fc.map(FileChannel.MapMode.READ_ONLY, 0, (int) fc.size());
bb.load();
loadDatabase(bb);
is.close();
}
/**
* Maps the database from the given FileInputStream.
*
* @param is the InputStream to load the database from
*
* @throws IOException if there is trouble opening the DB
*/
private void mapDatabase(FileInputStream is) throws IOException {
FileChannel fc = is.getChannel();
mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, (int) fc.size());
mbb.load();
loadDatabaseHeader(mbb);
}
/**
* Loads the database header from the given byte buffer.
*
* @param bb the byte buffer to load the db from
*
* @throws IOException if there is trouble opening the DB
*/
private void loadDatabaseHeader(ByteBuffer bb) throws IOException {
if (bb.getInt() != MAGIC) {
throw new Error("Bad magic in db");
}
if (bb.getInt() != VERSION) {
throw new Error("Bad VERSION in db");
}
sampleRate = bb.getInt();
numChannels = bb.getInt();
lpcMin = bb.getFloat();
lpcRange = bb.getFloat();
}
/**
* Loads the database from the given byte buffer.
*
* @param bb the byte buffer to load the db from
*
* @throws IOException if there is trouble opening the DB
*/
private void loadDatabase(ByteBuffer bb) throws IOException {
int size;
loadDatabaseHeader(bb);
size = bb.getInt();
diphoneIndex = new HashMap();
for (int i = 0; i < size; i++) {
int pos = bb.position();
Diphone diphone = Diphone.loadBinary(bb);
add(diphone);
diphoneIndex.put(diphone.getName(), new Integer(pos));
}
}
/**
* Compares this database to another. This is used for testing.
* With this method we can load up two databases (one perhaps from
* a text source and one from a binary source) and compare to
* verify that the dbs are identical
*
* @param other the other database
*
* @return <code>true</code> if the DBs are identical;
* otherwise <code>false</code>
*/
public boolean compare(DiphoneUnitDatabase other) {
if (sampleRate != other.sampleRate) {
return false;
}
if (numChannels != other.numChannels) {
return false;
}
if (lpcMin != other.lpcMin) {
return false;
}
if (lpcRange != other.lpcRange) {
return false;
}
for (Iterator i = diphoneMap.values().iterator(); i.hasNext(); ) {
Diphone diphone = (Diphone) i.next();
Diphone otherDiphone = (Diphone) other.getUnit(diphone.getName());
if (!diphone.compare(otherDiphone)) {
System.out.println("Diphones differ:");
System.out.println("THis:");
diphone.dump();
System.out.println("Other:");
otherDiphone.dump();
return false;
}
}
return true;
}
/**
* Manipulates a DiphoneUnitDatabase. This program is typically
* used to generate the binary form (with index) of the
* DiphoneUnitDatabase from the text form. Additionally, this program
* can be used to compare two databases to see if they are
* identical (used for testing).
*
* <p>
* <b> Usage </b>
* <p>
* <code> java com.sun.speech.freetts.diphone.DiphoneUnitDatabase
* [options]</code>
* <p>
* <b> Options </b>
* <p>
* <ul>
* <li> <code> -src path </code> provides a directory
* path to the source text for the database
* <li> <code> -dest path </code> provides a directory
* for where to place the resulting binaries
* <li> <code> -generate_binary [filename] </code>
* reads in the text
* version of the database and generates the binary
* version of the database.
* <li> <code> -compare </code> Loads the text and
* binary versions of the database and compares them to
* see if they are equivalent.
* <li> <code> -showTimes </code> shows timings for any
* loading, comparing or dumping operation
* </ul>
*
*/
public static void main(String[] args) {
boolean showTimes = false;
String srcPath = ".";
String destPath = ".";
try {
if (args.length > 0) {
BulkTimer timer = BulkTimer.LOAD;
timer.start();
for (int i = 0 ; i < args.length; i++) {
if (args[i].equals("-src")) {
srcPath = args[++i];
} else if (args[i].equals("-dest")) {
destPath = args[++i];
} else if (args[i].equals("-generate_binary")) {
String name = "diphone_units.txt";
if (i + 1 < args.length) {
String nameArg = args[++i];
if (!nameArg.startsWith("-")) {
name = nameArg;
}
}
int suffixPos = name.lastIndexOf(".txt");
String binaryName = "diphone_units.bin";
if (suffixPos != -1) {
binaryName = name.substring(0, suffixPos) + ".bin";
}
String indexName = "diphone_units.idx";
if (suffixPos != -1) {
indexName = name.substring(0, suffixPos) + ".idx";
}
System.out.println("Loading " + name);
timer.start("load_text");
DiphoneUnitDatabase udb = new DiphoneUnitDatabase(
new URL("file:"
+ srcPath + "/" + name), false);
timer.stop("load_text");
System.out.println("Dumping " + binaryName);
timer.start("dump_binary");
udb.dumpBinary(destPath + "/" + binaryName);
timer.stop("dump_binary");
timer.start("load_binary");
DiphoneUnitDatabase budb =
new DiphoneUnitDatabase(
new URL("file:"
+ destPath + "/" + binaryName),
true);
timer.stop("load_binary");
System.out.println("Dumping " + indexName);
timer.start("dump index");
budb.dumpBinaryIndex(destPath + "/" + indexName);
timer.stop("dump index");
} else if (args[i].equals("-compare")) {
timer.start("load_text");
DiphoneUnitDatabase udb = new DiphoneUnitDatabase(
new URL("file:./diphone_units.txt"), false);
timer.stop("load_text");
timer.start("load_binary");
DiphoneUnitDatabase budb =
new DiphoneUnitDatabase(
new URL("file:./diphone_units.bin"), true);
timer.stop("load_binary");
timer.start("compare");
if (udb.compare(budb)) {
System.out.println("other compare ok");
} else {
System.out.println("other compare different");
}
timer.stop("compare");
} else if (args[i].equals("-showtimes")) {
showTimes = true;
} else {
System.out.println("Unknown option " + args[i]);
}
}
timer.stop();
if (showTimes) {
timer.show("DiphoneUnitDatabase");
}
} else {
System.out.println("Options: ");
System.out.println(" -src path");
System.out.println(" -dest path");
System.out.println(" -compare");
System.out.println(" -generate_binary");
System.out.println(" -showTimes");
}
} catch (IOException ioe) {
System.err.println(ioe);
}
}
}