diff --git a/pom.xml b/pom.xml index 86d1ae16..1e597d64 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>org.pamguard</groupId> <artifactId>Pamguard</artifactId> - <version>2.02.16</version> + <version>2.02.16a</version> <name>Pamguard</name> <description>Pamguard using Maven to control dependencies</description> <url>www.pamguard.org</url> diff --git a/src/PamController/PamguardVersionInfo.java b/src/PamController/PamguardVersionInfo.java index 1f17d80c..62896aaf 100644 --- a/src/PamController/PamguardVersionInfo.java +++ b/src/PamController/PamguardVersionInfo.java @@ -31,12 +31,12 @@ public class PamguardVersionInfo { * Version number, major version.minorversion.sub-release. * Note: can't go higher than sub-release 'f' */ - static public final String version = "2.02.16"; + static public final String version = "2.02.16a"; /** * Release date */ - static public final String date = "20 February 2025"; + static public final String date = "April 2025"; // /** // * Release type - Beta or Core diff --git a/src/PamModel/PamModel.java b/src/PamModel/PamModel.java index 928a552c..4f4dd769 100644 --- a/src/PamModel/PamModel.java +++ b/src/PamModel/PamModel.java @@ -467,7 +467,7 @@ final public class PamModel implements PamSettings { mi.setToolTipText("Record observer monitoring effort"); mi.setModulesMenuGroup(utilitiesGroup); // mi.setHidden(SMRUEnable.isEnable() == false); - mi.setToolTipText("Enables an observer to enter their name and infomation about which displays are being monitored"); + mi.setToolTipText("Enables an observer to enter their name and information about which displays are being monitored"); mi.setMaxNumber(1); mi = PamModuleInfo.registerControlledUnit(BackupManager.class.getName(), BackupManager.defaultName); @@ -708,7 +708,7 @@ final public class PamModel implements PamSettings { mi = PamModuleInfo.registerControlledUnit("envelopeTracer.EnvelopeControl", "Envelope Tracing"); mi.addDependency(new PamDependency(RawDataUnit.class, "Acquisition.AcquisitionControl")); - mi.setToolTipText(""); + mi.setToolTipText("Traces the envelope of audio data and outputs it as a new waveform"); mi.setModulesMenuGroup(processingGroup); mi.setModulesMenuGroup(processingGroup); mi.setHelpPoint("sound_processing/EnvelopeTrace/Docs/EnvelopeOverview.html"); diff --git a/src/PamUtils/LittleEndianDataInputStream.java b/src/PamUtils/LittleEndianDataInputStream.java new file mode 100644 index 00000000..297ac7fa --- /dev/null +++ b/src/PamUtils/LittleEndianDataInputStream.java @@ -0,0 +1,167 @@ +package PamUtils; + +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +/** + * Copied from https://www.peterfranza.com/2008/09/26/little-endian-input-stream/ + * @author dg50 + * + */ +public class LittleEndianDataInputStream extends InputStream implements DataInput { + + public LittleEndianDataInputStream(InputStream in) { + this.in = in; + this.d = new DataInputStream(in); + w = new byte[8]; + } + + public int available() throws IOException { + return d.available(); + } + + + public final short readShort() throws IOException + { + d.readFully(w, 0, 2); + return (short)( + (w[1]&0xff) << 8 | + (w[0]&0xff)); + } + + /** + * Note, returns int even though it reads a short. + */ + public final int readUnsignedShort() throws IOException + { + d.readFully(w, 0, 2); + return ( + (w[1]&0xff) << 8 | + (w[0]&0xff)); + } + + /** + * like DataInputStream.readChar except little endian. + */ + public final char readChar() throws IOException + { + d.readFully(w, 0, 2); + return (char) ( + (w[1]&0xff) << 8 | + (w[0]&0xff)); + } + + /** + * like DataInputStream.readInt except little endian. + */ + public final int readInt() throws IOException + { + d.readFully(w, 0, 4); + return + (w[3]) << 24 | + (w[2]&0xff) << 16 | + (w[1]&0xff) << 8 | + (w[0]&0xff); + } + + /** + * like DataInputStream.readUnsignedInt except little endian. + */ + public final long readUnsignedInt() throws IOException + { + int v = readInt(); + return Integer.toUnsignedLong(v); +// ByteArray.to +// return +// (w[3]) << 24 | +// (w[2]&0xff) << 16 | +// (w[1]&0xff) << 8 | +// (w[0]&0xff); + + } + /** + * like DataInputStream.readLong except little endian. + */ + public final long readLong() throws IOException + { + d.readFully(w, 0, 8); + return + (long)(w[7]) << 56 | + (long)(w[6]&0xff) << 48 | + (long)(w[5]&0xff) << 40 | + (long)(w[4]&0xff) << 32 | + (long)(w[3]&0xff) << 24 | + (long)(w[2]&0xff) << 16 | + (long)(w[1]&0xff) << 8 | + (long)(w[0]&0xff); + } + + public final float readFloat() throws IOException { + // still need to byteswap + return Float.intBitsToFloat(readInt()); + } + + public final double readDouble() throws IOException { + // still need to byteswap + return Double.longBitsToDouble(readLong()); + } + + public final int read(byte b[], int off, int len) throws IOException { + return in.read(b, off, len); + } + + public final void readFully(byte b[]) throws IOException { + d.readFully(b, 0, b.length); + } + + public final void readFully(byte b[], int off, int len) throws IOException { + d.readFully(b, off, len); + } + + public final int skipBytes(int n) throws IOException { + return d.skipBytes(n); + } + + public final long skip(long n) throws IOException { + return d.skipBytes((int) n); + } + + public final boolean readBoolean() throws IOException { + return d.readBoolean(); + } + + public final byte readByte() throws IOException { + return d.readByte(); + } + + public int read() throws IOException { + return in.read(); + } + + public final int readUnsignedByte() throws IOException { + return d.readUnsignedByte(); + } + + @Deprecated + public final String readLine() throws IOException { + return d.readLine(); + } + + public final String readUTF() throws IOException { + return d.readUTF(); + } + + public final void close() throws IOException { + d.close(); + } + + private DataInputStream d; // to get at high level readFully methods of + // DataInputStream + private InputStream in; // to get at the low-level read methods of + // InputStream + private byte w[]; // work array for buffering input + +} diff --git a/src/PamView/PamGui.java b/src/PamView/PamGui.java index bf746071..48b0aabb 100644 --- a/src/PamView/PamGui.java +++ b/src/PamView/PamGui.java @@ -641,15 +641,18 @@ public class PamGui extends PamView implements WindowListener, PamSettings { menuItem = new JMenuItem("Module Ordering ..."); menuItem.addActionListener(new menuModuleOrder()); + menuItem.setToolTipText("Change the order of modules in the PAMGuard configuration"); orderModulesEnabler.addMenuItem(menuItem); fileMenu.add(menuItem); menuItem = new JMenuItem("Show Object List ..."); menuItem.addActionListener(new menuShowObjectList()); + menuItem.setToolTipText("Show a list of data and detections currnetly held in memory for each module"); fileMenu.add(menuItem); menuItem = new JMenuItem("Show Data Model ..."); menuItem.addActionListener(new menuShowObjectDiagram()); + menuItem.setToolTipText("Show a graphical representation of modules and their interconnections"); fileMenu.add(menuItem); if (!isViewer) { diff --git a/src/PamView/dialog/PamDialog.java b/src/PamView/dialog/PamDialog.java index 95fcba0f..44e4122f 100644 --- a/src/PamView/dialog/PamDialog.java +++ b/src/PamView/dialog/PamDialog.java @@ -620,6 +620,13 @@ abstract public class PamDialog extends JDialog { return showWarning(warningTitle, warningText); } + /** + * Display a warning text. + * @param owner + * @param warningTitle + * @param warningText + * @return + */ public static boolean showWarning(Window owner, String warningTitle, String warningText) { JOptionPane.showMessageDialog(owner, warningText, warningTitle, JOptionPane.ERROR_MESSAGE); return false; diff --git a/src/PamView/dialog/SourcePanel.java b/src/PamView/dialog/SourcePanel.java index 7b6087db..90d1770b 100644 --- a/src/PamView/dialog/SourcePanel.java +++ b/src/PamView/dialog/SourcePanel.java @@ -1,7 +1,6 @@ package PamView.dialog; import java.awt.BorderLayout; -import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Window; @@ -16,20 +15,14 @@ import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.JScrollPane; import javax.swing.border.TitledBorder; - - - - import PamController.PamController; import PamDetection.LocalisationInfo; import PamUtils.PamUtils; import PamguardMVC.PamConstants; import PamguardMVC.PamDataBlock; import PamguardMVC.PamDataUnit; -import PamguardMVC.PamProcess; /** * Standard panel for dialogs that shows a list of diff --git a/src/clickDetector/ClickDetector.java b/src/clickDetector/ClickDetector.java index 3ce20376..bb14fef2 100644 --- a/src/clickDetector/ClickDetector.java +++ b/src/clickDetector/ClickDetector.java @@ -143,7 +143,7 @@ public class ClickDetector extends PamProcess { private PamDataBlock<TriggerLevelDataUnit> triggerDataBlock; - private PamRawDataBlock doubleFilteredData; +// private PamRawDataBlock doubleFilteredData; // protected PamDataBlock<ClickDetection> trackedClicks; private PamDataBlock trackedClicks; @@ -730,12 +730,12 @@ public class ClickDetector extends PamProcess { // } if ((newRawData.getChannelBitmap() & clickControl.clickParameters.getChannelBitmap()) == 0) - return; + return; // not a channel we're interested in // // if (obs == filteredDataBlock || obs == doubleFilteredData) // return; - clickControl.newRawData(obs, newData); + clickControl.newRawData(obs, newData); // does nothing // see if it's time to start a new file // only do this here if it's not multithread @@ -1496,7 +1496,9 @@ public class ClickDetector extends PamProcess { int keepMillis = (int) (relSamplesToMilliseconds(requiredKeepSamples) * 2); // int keepSeconds = Math.max(1, (int) // relSamplesToMilliseconds(requiredKeepSamples)/1000); - keepMillis = Math.max(1000, keepMillis); + // add an extra second on 2025-04-01 to try to avoid null clicks. + + keepMillis = Math.max(1000, keepMillis) + 1000; // filteredDataBlock.setNaturalLifetime(keepSeconds); finalDataSource = filteredDataBlock; finalDataSource.setNaturalLifetimeMillis(keepMillis); @@ -1642,7 +1644,7 @@ public class ClickDetector extends PamProcess { /* * Waveform data ends up pointing either to the raw data, or the output of the * fist filter if there is one. new wavefformData is created every time (or - * recycled from the data block) since we may beed to go back a while to find + * recycled from the data block) since we may need to go back a while to find * data from a previous block */ double[][] waveformData = new double[nChannels][]; diff --git a/src/ravendata/RavenControl.java b/src/ravendata/RavenControl.java index 79a2be15..4b817169 100644 --- a/src/ravendata/RavenControl.java +++ b/src/ravendata/RavenControl.java @@ -131,4 +131,11 @@ public class RavenControl extends PamControlledUnit implements PamSettings { return true; } + /** + * @return the ravenParameters + */ + public RavenParameters getRavenParameters() { + return ravenParameters; + } + } diff --git a/src/ravendata/RavenParameters.java b/src/ravendata/RavenParameters.java index 93792833..9e0d09e0 100644 --- a/src/ravendata/RavenParameters.java +++ b/src/ravendata/RavenParameters.java @@ -9,6 +9,8 @@ public class RavenParameters implements Serializable, Cloneable { public String importFile; + public double timeOffsetSeconds = 0; + private ArrayList<RavenColumnInfo> extraColumns; public ArrayList<RavenColumnInfo> getExtraColumns() { diff --git a/src/ravendata/RavenProcess.java b/src/ravendata/RavenProcess.java index 196116a4..a611fb81 100644 --- a/src/ravendata/RavenProcess.java +++ b/src/ravendata/RavenProcess.java @@ -92,7 +92,7 @@ public class RavenProcess extends PamProcess { * this as an option in future releases. * Offset of 2843100 needed for mn23_055a tag data. */ - long offsetMillis = 0;//2843100; + long offsetMillis = (long) (ravenControl.getRavenParameters().timeOffsetSeconds * 1000.); RavenDataRow prevRow = null; for (RavenDataRow ravenRow : ravenData) { diff --git a/src/ravendata/swing/RavenImportDialog.java b/src/ravendata/swing/RavenImportDialog.java index af82e452..6f001899 100644 --- a/src/ravendata/swing/RavenImportDialog.java +++ b/src/ravendata/swing/RavenImportDialog.java @@ -10,6 +10,7 @@ import java.io.File; import javax.swing.JButton; import javax.swing.JFileChooser; +import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.border.TitledBorder; @@ -30,6 +31,7 @@ public class RavenImportDialog extends PamDialog { private JButton chooseButton; + private JTextField timeOffset; private RavenImportDialog(Window parentFrame) { @@ -40,10 +42,30 @@ public class RavenImportDialog extends PamDialog { ravenFile = new JTextField(80); ravenFile.setEditable(false); chooseButton = new JButton("Select ..."); + c.gridwidth = 2; + c.gridx = c.gridy = 0; mainPanel.add(ravenFile, c); + + JPanel p2 = new JPanel(new GridBagLayout()); + GridBagConstraints c2 = new PamGridBagContraints(); + c2.gridx = 1; + c2.gridwidth = 1; + p2.add(chooseButton, c2); + + c2.gridx = 0; + c2.gridy++; + c2.gridwidth = 1; + p2.add(new JLabel("Time offset (s) ", JLabel.RIGHT), c2); + c2.gridx++; + p2.add(timeOffset = new JTextField(7), c2); + String tip = "Added to data as it's read from file"; + timeOffset.setToolTipText(tip); + + c.gridwidth = 2; c.gridy++; - mainPanel.add(new PamAlignmentPanel(chooseButton, BorderLayout.EAST), c); + mainPanel.add(new PamAlignmentPanel(p2, BorderLayout.EAST), c); + chooseButton.addActionListener(new ActionListener() { @Override @@ -80,6 +102,7 @@ public class RavenImportDialog extends PamDialog { private void setParams(RavenParameters ravenParameters) { this.ravenParameters = ravenParameters; ravenFile.setText(ravenParameters.importFile); + timeOffset.setText(String.format("%5.3f", ravenParameters.timeOffsetSeconds)); } @Override @@ -94,6 +117,12 @@ public class RavenImportDialog extends PamDialog { return showWarning(str); } ravenParameters.importFile = fn; + try { + ravenParameters.timeOffsetSeconds = Double.valueOf(timeOffset.getText()); + } + catch (NumberFormatException e) { + return showWarning("Invalid time offset value. Must be a number"); + } return true; } diff --git a/src/wavFiles/WavHeader.java b/src/wavFiles/WavHeader.java index 2d0a69c8..d0bfbb85 100644 --- a/src/wavFiles/WavHeader.java +++ b/src/wavFiles/WavHeader.java @@ -6,6 +6,8 @@ import java.util.ArrayList; import javax.sound.sampled.AudioFormat; import clickDetector.WindowsFile; +import wavFiles.xwav.HarpHeader; +import wavFiles.xwav.XWavException; public class WavHeader { @@ -110,6 +112,18 @@ public class WavHeader { windowsWavFile.seek(fmtEnd); // break; } + else if (testString.equals("harp")) { + chunkSize = windowsWavFile.readWinInt(); + headChunk = new byte[chunkSize]; + windowsWavFile.read(headChunk); + try { + HarpHeader.readHarpHeader(headChunk); + } catch (XWavException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } +// wavHeadChunks.add(new WavHeadChunk(testString, headChunk)); + } else { /* * As an example, SCRIPPS HARP .x.wav files have a chunk diff --git a/src/wavFiles/xwav/HarpHeader.java b/src/wavFiles/xwav/HarpHeader.java new file mode 100644 index 00000000..433561ed --- /dev/null +++ b/src/wavFiles/xwav/HarpHeader.java @@ -0,0 +1,112 @@ +package wavFiles.xwav; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.util.Calendar; + +import PamUtils.LittleEndianDataInputStream; +import PamUtils.PamCalendar; + +public class HarpHeader { + + private HarpHeader() { + // TODO Auto-generated constructor stub + } + + /** + * Unpack harp data junk from a xwav file. + * @param chunkData + * @return + */ + public static HarpHeader readHarpHeader(byte[] chunkData) throws XWavException { + /* + * Based on matlab code found at https://github.com/MarineBioAcousticsRC/Wav2XWav/blob/main/wrxwavhdX.m + */ + LittleEndianDataInputStream dis = new LittleEndianDataInputStream(new ByteArrayInputStream(chunkData)); +// new LittleEnd + try { + int harpSize = chunkData.length; + int xhdVersion = dis.readUnsignedByte(); + String firmwareVersion = readString(dis, 10); + String insId = readString(dis, 4); + String site = readString(dis, 4); + String experiment = readString(dis, 8);// could be 8 in example + int diskSequenceNumber = dis.readUnsignedByte(); + String diskSerialNumber = readString(dis, 8); + int numRF = dis.readUnsignedShort(); + int longitude = dis.readInt(); // defo written as integers. guessing float*1e5. + int latitude = dis.readInt(); +// float longitude = dis.readFloat(); +// float latitude = dis.readFloat(); + int depth = dis.readShort(); + // skip 8. + dis.skip(8); + /* + * then read numRF chunks, each of which is 32 bytes. In this example, we + * have harpSize = 29752, so expecting about (29752-50)/32 + */ + long lastT = 0; + for (int iRF = 0; iRF < numRF; iRF++) { + // time is from datevec, so it's year, month ... second in the first six + int[] dateVec = new int[7]; + for (int i = 0; i < 6; i++) { + dateVec[i] = dis.readUnsignedByte(); + } + dateVec[6] = dis.readUnsignedShort(); // number of millis. + long byteLoc = dis.readUnsignedInt(); + long byteLength = dis.readUnsignedInt(); + long writeLength = dis.readUnsignedInt(); + long sampleRate = dis.readUnsignedInt(); + int gain = dis.readUnsignedByte(); + dis.skip(7); + long tMillis = dateVec2Millis(dateVec); +// if (lastT != 0) { +// System.out.printf("%s length %d = %3.3fs, step = %dms\n", PamCalendar.formatDBDateTime(tMillis, true), byteLength, +// (double) byteLength / (double) sampleRate / 2., tMillis-lastT); +// } +// else { +// System.out.printf("%s length %d = %3.3fs\n", PamCalendar.formatDBDateTime(tMillis, true), byteLength, +// (double) byteLength / (double) sampleRate / 2.); +// } + lastT = tMillis; + } + + } catch (IOException e) { + throw new XWavException(e.getMessage()); + } + + + return null; + } + + /** + * Convert datevec read from file to Java millis. + * @param dateVec + */ + private static long dateVec2Millis(int[] dateVec) { + // format is yy, mm, dd, hh, mm, ss, ms as int values. + Calendar c = Calendar.getInstance(); + c.setTimeZone(PamCalendar.defaultTimeZone); + c.clear(); + int yy = dateVec[0]; + if (yy < 90) { + yy += 2000; + } + c.set(yy, dateVec[1]-1, dateVec[2], dateVec[3], dateVec[4], dateVec[5]); + long millis = c.getTimeInMillis() + dateVec[6]; + return millis; + } + + private static String readString(LittleEndianDataInputStream dis, int bytes) throws XWavException { + byte[] data; + try { + data = dis.readNBytes(bytes); + String str = new String(data); + return str; + } catch (IOException e) { + throw new XWavException(e.getMessage()); + } + } + +} diff --git a/src/wavFiles/xwav/XWavException.java b/src/wavFiles/xwav/XWavException.java new file mode 100644 index 00000000..7eb6b470 --- /dev/null +++ b/src/wavFiles/xwav/XWavException.java @@ -0,0 +1,34 @@ +package wavFiles.xwav; + +public class XWavException extends Exception { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public XWavException() { + // TODO Auto-generated constructor stub + } + + public XWavException(String message) { + super(message); + // TODO Auto-generated constructor stub + } + + public XWavException(Throwable cause) { + super(cause); + // TODO Auto-generated constructor stub + } + + public XWavException(String message, Throwable cause) { + super(message, cause); + // TODO Auto-generated constructor stub + } + + public XWavException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + // TODO Auto-generated constructor stub + } + +}