All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.harctoolbox.harchardware.ir.Wave Maven / Gradle / Ivy

There is a newer version: 2.4.1
Show newest version
/*
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 - 2024 Weber Informatics LLC | Privacy Policy