/** * */ package marytts.util.data.text; import java.io.BufferedReader; import java.io.IOException; import java.io.PrintWriter; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; import java.util.Arrays; import marytts.util.dom.DomUtils; import marytts.util.string.StringUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.traversal.NodeIterator; /** * @author marc * */ public class PraatPitchTier implements PraatTier { protected static final String FIRSTLINE = "File type = \"ooTextFile\""; protected static final String SECONDLINE = "Object class = \"PitchTier\""; protected double xmin; protected double xmax; protected int numTargets; protected PitchTarget[] targets; public PraatPitchTier(Reader input) throws IOException { BufferedReader lineReader = new BufferedReader(input); try { String filetype = lineReader.readLine(); if (!filetype.equals(FIRSTLINE)) { throw new IllegalArgumentException("First line expected to be '" + FIRSTLINE + "' but was '" + filetype + "'"); } String subtype = lineReader.readLine(); if (!subtype.equals(SECONDLINE)) { throw new IllegalArgumentException("Second line expected to be '" + SECONDLINE + "' but was '" + subtype + "'"); } lineReader.readLine(); // skip empty line String xminLine = lineReader.readLine(); String xmaxLine = lineReader.readLine(); String numTargetsLine = lineReader.readLine(); try { xmin = Double.parseDouble(xminLine); xmax = Double.parseDouble(xmaxLine); numTargets = Integer.parseInt(numTargetsLine); } catch (NumberFormatException nfe) { String[] fields = xminLine.split("\\s+"); xmin = Double.parseDouble(fields[fields.length - 1]); fields = xmaxLine.split("\\s+"); xmax = Double.parseDouble(fields[fields.length - 1]); fields = numTargetsLine.split("\\s+"); numTargets = Integer.parseInt(fields[fields.length - 1]); } targets = new PitchTarget[numTargets]; for (int i = 0; i < numTargets; i++) { String timeLine = lineReader.readLine(); String freqLine = lineReader.readLine(); try { double time = Double.parseDouble(timeLine); double freq = Double.parseDouble(freqLine); targets[i] = new PitchTarget(time, freq); } catch (NumberFormatException nfe) { // default format has an extra line indexing each pitch // point timeLine = freqLine; freqLine = lineReader.readLine(); String[] fields = timeLine.split("\\s+"); double time = Double.parseDouble(fields[fields.length - 1]); fields = freqLine.split("\\s+"); double freq = Double.parseDouble(fields[fields.length - 1]); targets[i] = new PitchTarget(time, freq); } } } finally { lineReader.close(); } } public PraatPitchTier(double xmin, double[] frames, double step) { this.xmin = xmin; this.xmax = xmin + (frames.length - 1) * step; importFrames(frames, step); } public PraatPitchTier(Document acoustparams) { this(computePitchTargets(acoustparams)); } private static PitchTarget[] computePitchTargets(Document acoustparams) { ArrayList<PitchTarget> targets = new ArrayList<PitchTarget>(); String PHONE = "ph"; String A_PHONE_DURATION = "d"; String A_F0 = "f0"; String BOUNDARY = "boundary"; String A_BOUNDARY_DURATION = "duration"; NodeIterator it = DomUtils.createNodeIterator(acoustparams, PHONE, BOUNDARY); Element e = null; double startTime = 0; double endTime = 0; double duration = 0; while ((e = (Element) it.nextNode()) != null) { startTime = /* previous */endTime; if (e.getTagName().equals(PHONE)) { duration = 0.001 * Double.parseDouble(e.getAttribute(A_PHONE_DURATION)); endTime = startTime + duration; } else { // BOUNDARY duration = 0.001 * Double.parseDouble(e.getAttribute(A_BOUNDARY_DURATION)); endTime = startTime + duration; continue; // no f0 targets for boundaries } assert e.getTagName().equals(PHONE); assert startTime < endTime : "for phone '" + e.getAttribute("p") + "', startTime " + startTime + " is not less than endTime " + endTime; String f0String = e.getAttribute(A_F0).trim(); if (f0String.isEmpty()) { continue; } int[] localF0Targets = StringUtils.parseIntPairs(f0String); for (int i = 0, len = localF0Targets.length / 2; i < len; i++) { int percent = localF0Targets[2 * i]; int hertz = localF0Targets[2 * i + 1]; double time = startTime + 0.01 * percent * (endTime - startTime); targets.add(new PitchTarget(time, hertz)); } } return targets.toArray(new PitchTarget[0]); } public PraatPitchTier(PitchTarget[] targets) { this.targets = targets; this.numTargets = targets.length; this.xmin = targets[0].time; this.xmax = targets[targets.length - 1].time; } /* * (non-Javadoc) * * @see marytts.util.data.text.PraatTier#getName() */ @Override public String getName() { return null; } /* * (non-Javadoc) * * @see marytts.util.data.text.PraatTier#getXmax() */ @Override public double getXmax() { return xmax; } public void setXmax(double value) { xmax = value; } /* * (non-Javadoc) * * @see marytts.util.data.text.PraatTier#getXmin() */ @Override public double getXmin() { return xmin; } public void setXmin(double value) { xmin = value; } public int getNumTargets() { return numTargets; } public PitchTarget[] getPitchTargets() { return targets; } public void writeTo(Writer out) { PrintWriter pw = new PrintWriter(out); try { pw.println(FIRSTLINE); pw.println(SECONDLINE); pw.println(); pw.println(xmin); pw.println(xmax); pw.println(numTargets); for (int i = 0; i < numTargets; i++) { pw.println(targets[i].time); pw.println(targets[i].frequency); } } finally { pw.close(); } } /** * Convert this sequence of pitch targets into an array of frame values. Values before the first target are NaN, values after * the last target are NaN; values between targets are linearly interpolated. * * @param step * the constant time distance between two frames, in seconds. * @return an array of doubles representing the equally spaced frequency values, from xmin to xmax. */ public double[] toFrames(double step) { int numFrames = (int) ((xmax - xmin) / step) + 1; assert xmin + (numFrames - 1) * step <= xmax; double[] frames = new double[numFrames]; double t = xmin; PitchTarget prev = null; int j = 0; for (int i = 0; i < frames.length; i++) { frames[i] = getFrequency(t); t += step; } return frames; } public double getFrequency(double time) { PitchTarget prev = null; PitchTarget current = null; for (int j = 0; j < targets.length; j++) { if (time <= targets[j].time) { current = targets[j]; if (j > 0) { prev = targets[j - 1]; } break; } } if (current == null) return Double.NaN; if (Math.abs(time - current.time) < 1.e-7) return current.frequency; if (prev == null) return Double.NaN; // need to interpolate: assert prev != null; double deltaT = current.time - prev.time; double deltaF = current.frequency - prev.frequency; return prev.frequency + (time - prev.time) / deltaT * deltaF; } /** * For every frame that is not NaN, create a pitch-time target. * * @param frames * frames * @param step * step */ protected void importFrames(double[] frames, double step) { ArrayList<PitchTarget> newTargets = new ArrayList<PitchTarget>(); double t = xmin; for (int i = 0; i < frames.length; i++) { if (!Double.isNaN(frames[i])) { newTargets.add(new PitchTarget(t, frames[i])); } t += step; } targets = newTargets.toArray(new PitchTarget[0]); numTargets = targets.length; } @Override public boolean equals(Object otherObj) { if (this == otherObj) { return true; } if (!(otherObj instanceof PraatPitchTier)) { return false; } PraatPitchTier other = (PraatPitchTier) otherObj; if (this.xmin == other.xmin && this.xmax == other.xmax && this.numTargets == other.numTargets) { return Arrays.equals(this.targets, other.targets); } return false; } @Override public int hashCode() { int hash = 31 + Integer.parseInt(Double.toString(xmin + xmax)) + numTargets + Arrays.hashCode(targets); return hash; } public static class PitchTarget { public final double time; public final double frequency; public PitchTarget(double time, double frequency) { this.time = time; this.frequency = frequency; } @Override public boolean equals(Object otherObj) { if (this == otherObj) { return true; } if (!(otherObj instanceof PitchTarget)) { return false; } PitchTarget other = (PitchTarget) otherObj; if (this.time == other.time && this.frequency == other.frequency) { return true; } return false; } @Override public int hashCode() { int hash = 31 + Integer.parseInt(Double.toString(time + frequency)); return hash; } } }