
org.harctoolbox.harchardware.ir.Wave Maven / Gradle / Ivy
/*
Copyright (C) 2009-2013 Bengt Martensson.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see http://www.gnu.org/licenses/.
*/
package org.harctoolbox.harchardware.ir;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.internal.DefaultConsole;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import org.harctoolbox.harchardware.Version;
import org.harctoolbox.ircore.InvalidArgumentException;
import org.harctoolbox.ircore.ModulatedIrSequence;
import org.harctoolbox.ircore.OddSequenceLengthException;
import org.harctoolbox.irp.IrpUtils;
/**
* This class generates (or analyzes) a wave audio file that can be played
* on standard audio equipment and fed to a pair of anti-parallel double IR sending diodes,
* which can thus control IR equipment.
*
*
* @see http://www.lirc.org/html/audio.html
*/
public class Wave {
private static int debug = 0; // presently not used
private static int epsilon8Bit = 2;
private static int epsilon16Bit = 257;
private static JCommander argumentParser;
private static CommandLineArgs commandLineArgs = new CommandLineArgs();
/**
* Returns a line to the audio mixer on the local machine, suitable for sound with
* the parameter values given. When not needed, the user should close the line with its close()-function.
*
* @param audioFormat
* @return open audio line
* @throws LineUnavailableException
*/
public static SourceDataLine getLine(AudioFormat audioFormat) throws LineUnavailableException {
SourceDataLine line = AudioSystem.getSourceDataLine(audioFormat);
line.open(audioFormat);
return line;
}
private static void usage(int exitcode) {
PrintStream printStream = exitcode == IrpUtils.EXIT_SUCCESS ? System.out : System.err;
argumentParser.setConsole(new DefaultConsole(printStream));
argumentParser.usage();
printStream.println("\n"
+ "parameters: [] commandno []\n"
+ " or \n"
+ " or ");
System.exit(exitcode);
}
/**
* Provides a command line interface to the export/import functions.
*
*
* Usage: Wave [options] [parameters]
* Options:
* -c, --config Path to IrpProtocols.ini
* Default: data/IrpProtocols.ini
* -h, --help, -? Display help message
* Default: false
* -m, --macrofile Macro filename
* -1, --nodivide Do not divide modulation frequency
* Default: false
* -t, --omittail Skip silence at end
* Default: false
* -o, --outfile Output filename
* Default: irpmaster.wav
* -p, --play Send the generated wave to the audio device of the
* local machine
* Default: false
* -r, --repeats Number of times to include the repeat sequence
* Default: 0
* -f, --samplefrequency Sample frequency in Hz
* Default: 44100
* -q, --square Modulate with square wave instead of sine
* Default: false
* -S, --stereo Generate two channels in anti-phase
* Default: false
* -v, --version Display version information
* Default: false
* -s, samplesize Sample size in bits
* Default: 8
*
* parameters: protocol deviceno [subdevice_no] commandno [toggle]
* or ProntoCode
* or importfile
*
* @param args
*/
public static void main(String[] args) {
argumentParser = new JCommander(commandLineArgs);
argumentParser.setProgramName("Wave");
try {
argumentParser.parse(args);
} catch (ParameterException ex) {
System.err.println(ex.getMessage());
usage(IrpUtils.EXIT_USAGE_ERROR);
}
if (commandLineArgs.helpRequensted)
usage(IrpUtils.EXIT_SUCCESS);
if (commandLineArgs.versionRequested) {
System.out.println(Version.versionString);
System.out.println("JVM: " + System.getProperty("java.vendor") + " " + System.getProperty("java.version") + " " + System.getProperty("os.name") + "-" + System.getProperty("os.arch"));
System.out.println();
System.out.println(Version.licenseString);
System.exit(IrpUtils.EXIT_SUCCESS);
}
if (commandLineArgs.macrofile == null && commandLineArgs.parameters.isEmpty()) {
System.err.println("Parameters missing");
usage(IrpUtils.EXIT_USAGE_ERROR);
}
try {
if (commandLineArgs.parameters.size() == 1) {
// Exactly one argument left -> input wave file
String inputfile = commandLineArgs.parameters.get(0);
Wave wave = new Wave(new File(inputfile));
ModulatedIrSequence seq = wave.analyze(!commandLineArgs.dontDivide);
// FIXME
//IrSignal irSignal = new
//DecodeIR.invoke(seq);
wave.dump(new File(inputfile + ".tsv"));
if (commandLineArgs.play)
wave.play();
} else {
//if (commandLineArgs.macrofile != null) {
//IrSequence irSequence = IrSequence.parseMacro(commandLineArgs.irprotocolsIniFilename, commandLineArgs.macrofile);
//Wave wave = new Wave()
//}
// IrSignal irSignal = new IrSignal(commandLineArgs.irprotocolsIniFilename, 0, commandLineArgs.parameters.toArray(new String[commandLineArgs.parameters.size()]));
// File file = new File(commandLineArgs.outputfile);
// Wave wave = new Wave(irSignal.toModulatedIrSequence(true, commandLineArgs.noRepeats, true), commandLineArgs.sampleFrequency, commandLineArgs.sampleSize,
// commandLineArgs.stereo ? 2 : 1, false /* bigEndian */,
// commandLineArgs.omitTail, commandLineArgs.square, !commandLineArgs.dontDivide);
// wave.export(file);
// if (commandLineArgs.play)
// wave.play();
}
} catch (IOException | UnsupportedAudioFileException | LineUnavailableException ex) {
System.err.println(ex.getMessage());
System.exit(IrpUtils.EXIT_FATAL_PROGRAM_FAILURE);
}
}
private int noFrames = -1;
private AudioFormat audioFormat;
private byte[] buf;
private Wave() {
}
/**
* Reads a wave file into a Wave object.
*
* @param file Wave file as input.
* @throws UnsupportedAudioFileException
* @throws IOException
*/
public Wave(File file) throws UnsupportedAudioFileException, IOException {
AudioInputStream af = AudioSystem.getAudioInputStream(file);
audioFormat = af.getFormat();
noFrames = (int) af.getFrameLength();
buf = new byte[noFrames*audioFormat.getFrameSize()];
int n = af.read(buf, 0, buf.length);
if (n != buf.length)
System.err.println("Too few bytes read: " + n + " < " + buf.length);
}
/**
* Generates a wave audio file from its arguments.
*
* @param freq Carrier frequency in Hz.
* @param data double array of durations in micro seconds.
* @param sampleFrequency Sample frequency of the generated wave file.
* @param sampleSize Sample size (8 or 16) in bits of the samples.
* @param channels If == 2, generates two channels in perfect anti-phase.
* @param bigEndian if true, use bigendian byte order for 16 bit samples.
* @param omitTail If true, the last trailing gap will be omitted.
* @param square if true, use a square wave for modulation, otherwise a sine.
* @param divide If true, divides the carrier frequency by 2, to be used with full-wave rectifiers, e.g. a pair of IR LEDs in anti-parallel.
* @throws org.harctoolbox.ircore.InvalidArgumentException
*/
@SuppressWarnings("ValueOfIncrementOrDecrementUsed")
public Wave(double freq, double[] data,
int sampleFrequency, int sampleSize, int channels, boolean bigEndian,
boolean omitTail, boolean square, boolean divide) throws InvalidArgumentException {
if (data == null || data.length == 0)
throw new InvalidArgumentException("Cannot create wave file from zero array.");
double sf = sampleFrequency/1000000.0;
int[] durationsInSamplePeriods = new int[omitTail ? data.length-1 : data.length];
int length = 0;
for (int i = 0; i < durationsInSamplePeriods.length; i++) {
durationsInSamplePeriods[i] = (int) Math.round(Math.abs(sf*data[i]));
length += durationsInSamplePeriods[i];
}
double c = sampleFrequency/freq;
buf = new byte[length*sampleSize/8*channels];
int index = 0;
for (int i = 0; i < data.length-1; i += 2) {
// Handle pulse, even index
for (int j = 0; j < durationsInSamplePeriods[i]; j++) {
double t = j/(divide ? 2*c : c);
double fraq = t - (int)t;
double s = square
? (fraq < 0.5 ? -1.0 : 1.0)
: Math.sin(2*Math.PI*(fraq));
if (sampleSize == 8) {
int val = (int) Math.round(Byte.MAX_VALUE*s);
buf[index++] = (byte) val;
if (channels == 2)
buf[index++] = (byte)-val;
} else {
int val = (int) Math.round(Short.MAX_VALUE*s);
byte low = (byte) (val & 0xFF);
byte high = (byte) (val >> 8);
buf[index++] = bigEndian ? high : low;
buf[index++] = bigEndian ? low : high;
if (channels == 2) {
val = -val;
low = (byte) (val & 0xFF);
high = (byte) (val >> 8);
buf[index++] = bigEndian ? high : low;
buf[index++] = bigEndian ? low : high;
}
}
}
// Gap, odd index
if (!omitTail || i < data.length - 2) {
for (int j = 0; j < durationsInSamplePeriods[i + 1]; j++) {
for (int ch = 0; ch < channels; ch++) {
buf[index++] = 0;
if (sampleSize == 16)
buf[index++] = 0;
}
}
}
}
audioFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, sampleFrequency, sampleSize, channels, sampleSize/8*channels, sampleFrequency, bigEndian);
}
/**
* Generates a wave audio file from its arguments.
*
* @param irSequence ModulatedIrSequence to be used.
* @param sampleFrequency Sample frequency of the generated wave file.
* @param sampleSize Sample size (8 or 16) in bits of the samples.
* @param channels If == 2, generates two channels in perfect anti-phase.
* @param bigEndian if true, use bigendian byte order for 16 bit samples.
* @param omitTail If true, the last trailing gap will be omitted.
* @param square if true, use a square wave for modulation, otherwise a sine.
* @param divide If true, divides the carrier frequency by 2, to be used with full-wave rectifiers, e.g. a pair of IR LEDs in anti-parallel.
* @throws org.harctoolbox.ircore.InvalidArgumentException
*/
public Wave(ModulatedIrSequence irSequence,
int sampleFrequency, int sampleSize, int channels, boolean bigEndian,
boolean omitTail, boolean square, boolean divide) throws InvalidArgumentException {
this(irSequence.getFrequency(), irSequence.toDoubles(),
sampleFrequency, sampleSize, channels, bigEndian,
omitTail, square, divide);
}
/**
* Generates a wave audio file from its arguments.
*
* @param irSequence ModulatedIrSequence to be used.
* @param audioFormat AudioFormat bundling sampleFrequency, sample size, channels, and bigEndian together.
* @param omitTail If true, the last trailing gap will be omitted.
* @param square if true, use a square wave for modulation, otherwise a sine.
* @param divide If true, divides the carrier frequency by 2, to be used with full-wave rectifiers, e.g. a pair of IR LEDs in anti-parallel.
* @throws org.harctoolbox.ircore.InvalidArgumentException
*/
public Wave(ModulatedIrSequence irSequence,
AudioFormat audioFormat,
boolean omitTail,
boolean square, boolean divide) throws InvalidArgumentException {
this(irSequence,
(int) audioFormat.getSampleRate(), audioFormat.getSampleSizeInBits(),
audioFormat.getChannels(),
audioFormat.isBigEndian(),
omitTail, square, divide);
}
// set up integer data (left and right channel) from the byte array.
private int[][] computeData() {
int channels = audioFormat.getChannels();
int sampleSize = audioFormat.getSampleSizeInBits();
AudioFormat.Encoding encoding = audioFormat.getEncoding();
boolean bigEndian = audioFormat.isBigEndian();
int[][] data = new int[noFrames][channels];
if (encoding == AudioFormat.Encoding.PCM_UNSIGNED && sampleSize != 8) {
System.err.println("Case not yet implemented");
return null;
}
for (int frame = 0; frame < noFrames; frame++) {
if (sampleSize == 8) {
for (int ch = 0; ch < channels; ch++) {
int val = buf[channels*frame + ch];
if (encoding == AudioFormat.Encoding.PCM_UNSIGNED)
val += (val < 0) ? 128 : -128;
data[frame][ch] = val;
}
} else {
// sampleSize == 16
for (int ch = 0; ch < channels; ch++) {
int baseIndex = 2*(channels*frame + ch);
int high = buf[bigEndian ? baseIndex : baseIndex+1]; // may be negative
int low = buf[bigEndian ? baseIndex+1 : baseIndex]; // consider as unsigned
if (low < 0)
low += 256;
int value = 256*high + low;
data[frame][ch] = value;
}
}
}
return data;
}
/**
* Analyzes the data and computes a ModulatedIrSequence. Generates some messages on stderr.
*
* @param divide consider the carrier as having its frequency halved or not?
* @return ModulatedIrSequence computed from the data.
*/
public ModulatedIrSequence analyze(boolean divide) {
double sampleFrequency = audioFormat.getSampleRate();
int channels = audioFormat.getChannels();
System.err.println("Format is: " + audioFormat.toString() + ".");
System.err.println(String.format("%d frames = %7.6f seconds.", noFrames, noFrames/sampleFrequency));
int[][] data = computeData();
if (channels == 2) {
int noDiffPhase = 0;
int noDiffAntiphase = 0;
int noNonNulls = 0;
for (int i = 0; i < noFrames; i++) {
if (data[i][0] != 0 || data[i][1] != 0) { // do not count nulls
noNonNulls++;
if (data[i][0] != data[i][1])
noDiffPhase++;
if (data[i][0] != -data[i][1])
noDiffAntiphase++;
}
}
System.err.println("This is a 2-channel file. Left and right channel are "
+ (noDiffPhase == 0 ? "perfectly in phase."
: noDiffAntiphase == 0 ? "perfectly in antiphase."
: "neither completely in nor out of phase. Pairs in-phase:"
+ (noNonNulls - noDiffPhase) + ", pairs anti-phase: " + (noNonNulls - noDiffAntiphase)
+ " (out of " + noNonNulls + ")."));
System.err.println("Subsequent analysis will be base on the left channel exclusively.");
}
// Search the largest block of oscillations
ArrayList durations = new ArrayList<>(noFrames);
int bestLength = -1; // length of longest block this far
int bestStart = -1;
boolean isInInterestingBlock = true;
int last = -1111111;
int epsilon = audioFormat.getSampleSizeInBits() == 8 ? epsilon8Bit : epsilon16Bit;
int firstNonNullIndex = 0; // Ignore leading silence, it is silly.
while (data[firstNonNullIndex][0] == 0)
firstNonNullIndex++;
if (firstNonNullIndex > 0)
System.err.println("The first " + firstNonNullIndex + " sample(s) are 0, ignored.");
int beg = firstNonNullIndex; // start of current block
for (int i = firstNonNullIndex; i < noFrames; i++) {
int value = data[i][0];
// two consecutive zeros -> interesting block ends
if (((Math.abs(value) <= epsilon && Math.abs(last) <= epsilon) || (i == noFrames - 1)) && isInInterestingBlock) {
isInInterestingBlock = false;
// evaluate just ended block
int currentLength = i - 1 - beg;
if (currentLength > bestLength) {
// longest this far
bestLength = currentLength;
bestStart = beg;
}
durations.add((int)Math.round(currentLength/sampleFrequency*1000000.0));
beg = i;
} else if (Math.abs(value) > epsilon && !isInInterestingBlock) {
// Interesting block starts
isInInterestingBlock = true;
int currentLength = i - 1 - beg;
durations.add((int) Math.round(currentLength/sampleFrequency*1000000.0));
beg = i;
}
last = value;
}
if (!isInInterestingBlock && noFrames - beg > 1)
durations.add((int)Math.round((noFrames - beg)/sampleFrequency*1000000.0));
if (durations.size() % 2 == 1)
durations.add(0);
// Found the longest interesting block, now evaluate frequency
int signchanges = 0;
last = 0;
for (int i = 0; i < bestLength; i++) {
int indx = i + bestStart;
int value = data[indx][0];
if (value != 0) {
if (value*last < 0)
signchanges++;
last = value;
}
}
double carrierFrequency = (divide ? 2 : 1)*sampleFrequency * signchanges/(2*bestLength);
System.err.println("Carrier frequency estimated to " + Math.round(carrierFrequency) + " Hz.");
int arr[] = new int[durations.size()];
int ind = 0;
for (Integer val : durations) {
arr[ind] = val;
ind++;
if (debug > 0)
System.err.print(val + " ");
}
if (debug > 0)
System.err.println();
try {
//return new IrSignal(arr, arr.length/2, 0, (int) Math.round(carrierFrequency));
return new ModulatedIrSequence(arr, carrierFrequency);
} catch (OddSequenceLengthException ex) {
// cannot happen, we have insured that the data has even size.
return null;
}
}
/**
* Print the channels to a tab separated text file, for example for debugging purposes.
* This file can be imported in a spreadsheet.
*
* @param dumpfile Output file.
* @throws FileNotFoundException
*/
public void dump(File dumpfile) throws FileNotFoundException {
int data[][] = computeData();
double sampleRate = audioFormat.getSampleRate();
int channels = audioFormat.getChannels();
try (PrintStream stream = new PrintStream(dumpfile, "US-ASCII")) {
for (int i = 0; i < noFrames; i++) {
stream.print(String.format("%d\t%8.6f\t", i, i / sampleRate));
for (int ch = 0; ch < channels; ch++)
stream.print(data[i][ch] + (ch < channels - 1 ? "\t" : "\n"));
}
} catch (UnsupportedEncodingException ex) {
throw new InternalError();
}
}
/**
* Write the signal to the file given as argument.
* @param file Output File.
*/
public void export(File file) {
ByteArrayInputStream bs = new ByteArrayInputStream(buf);
bs.reset();
AudioInputStream ais = new AudioInputStream(bs, audioFormat, (long) buf.length/audioFormat.getFrameSize());
try {
int result = AudioSystem.write(ais, AudioFileFormat.Type.WAVE, file);
if (result <= buf.length)
System.err.println("Wrong number of bytes written: " + result + " < " + buf.length);
} catch (IOException e) {
System.err.println(e.getMessage());
}
}
/**
* Sends the generated wave to the line in argument, if possible.
* @param line Line to used. Should be open, and remains open. User must make sure AudioFormat is compatible.
* @throws LineUnavailableException
* @throws IOException
*/
public void play(SourceDataLine line) throws LineUnavailableException, IOException {
line.start();
int bytesWritten = line.write(buf, 0, buf.length);
if (bytesWritten != buf.length)
throw new IOException("Not all bytes written");
line.drain();
}
/**
* Sends the generated wave to the local machine's audio system, if possible.
* @throws LineUnavailableException
* @throws IOException
*/
public void play() throws LineUnavailableException, IOException {
try (SourceDataLine line = AudioSystem.getSourceDataLine(audioFormat)) {
line.open(audioFormat);
play(line);
}
}
private final static class CommandLineArgs {
@Parameter(names = {"-1", "--nodivide"}, description = "Do not divide modulation frequency")
boolean dontDivide = false;
@Parameter(names = {"-c", "--config"}, description = "Path to IrpProtocols.ini")
String irprotocolsIniFilename = "data/IrpProtocols.ini";
@Parameter(names = {"-h", "--help", "-?"}, description = "Display help message")
boolean helpRequensted = false;
@Parameter(names = {"-f", "--samplefrequency"}, description = "Sample frequency in Hz")
int sampleFrequency = 44100;
@Parameter(names = {"-m", "--macrofile"}, description = "Macro filename")
String macrofile = null;
@Parameter(names = {"-o", "--outfile"}, description = "Output filename")
String outputfile = "irpmaster.wav";
@Parameter(names = {"-p", "--play"}, description = "Send the generated wave to the audio device of the local machine")
boolean play = false;
@Parameter(names = {"-q", "--square"}, description = "Modulate with square wave instead of sine")
boolean square = false;
@Parameter(names = {"-r", "--repeats"}, description = "Number of times to include the repeat sequence")
int noRepeats = 0;
@Parameter(names = {"-s", "samplesize"}, description = "Sample size in bits")
int sampleSize = 8;
@Parameter(names = {"-S", "--stereo"}, description = "Generate two channels in anti-phase")
boolean stereo = false;
@Parameter(names = {"-t", "--omittail"}, description = "Skip silence at end")
boolean omitTail = false;
@Parameter(names = {"-v", "--version"}, description = "Display version information")
boolean versionRequested;
@Parameter(description = "[parameters]")
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
private ArrayList parameters = new ArrayList<>(64);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy