diff --git a/expansion/expander.js b/expansion/expander.js deleted file mode 100644 index aa87cb0..0000000 --- a/expansion/expander.js +++ /dev/null @@ -1,435 +0,0 @@ -/**************************************************************************** - * expander.js - * openacousticdevices.info - * June 2020 - *****************************************************************************/ - -'use strict'; - -const fs = require('fs'); -const path = require('path'); - -const wavHeader = require('./wavHeader.js'); - -/* Expansion constants */ - -const MILLISECONDS_IN_SECONDS = 1000; - -const NUMBER_OF_BYTES_IN_SAMPLE = 2; - -const EXPANSION_BUFFER_ENCODING_SIZE = 32; - -const EXPANSION_BUFFER_SIZE_IN_BYTES = 512; - -const SECONDS_IN_DAY = 24 * 60 * 60; - -const DATE_REGEX = /Recorded at (\d\d):(\d\d):(\d\d) (\d\d)\/(\d\d)\/(\d\d\d\d)/; - -/* Buffers for reading data */ - -const headerBuffer = Buffer.alloc(wavHeader.LENGTH_OF_WAV_HEADER); - -const blankHeaderBuffer = Buffer.alloc(wavHeader.LENGTH_OF_WAV_HEADER); - -const fileBuffer = Buffer.alloc(EXPANSION_BUFFER_SIZE_IN_BYTES); - -const blankFileBuffer = Buffer.alloc(EXPANSION_BUFFER_SIZE_IN_BYTES); - -/* Decode the expansion buffer */ - -function decodeExpansionBuffer (buffer) { - - var i, value, numberOfExpandedBuffers; - - numberOfExpandedBuffers = 0; - - for (i = 0; i < EXPANSION_BUFFER_ENCODING_SIZE; i += 1) { - - value = buffer.readInt16LE(NUMBER_OF_BYTES_IN_SAMPLE * i); - - if (value === 1) { - - numberOfExpandedBuffers += (1 << i); - - } else if (value !== -1) { - - return 0; - - } - - } - - for (i = EXPANSION_BUFFER_ENCODING_SIZE; i < EXPANSION_BUFFER_SIZE_IN_BYTES / NUMBER_OF_BYTES_IN_SAMPLE; i += 1) { - - value = buffer.readInt16LE(NUMBER_OF_BYTES_IN_SAMPLE * i); - - if (value !== 0) { - - return 0; - - } - - } - - return numberOfExpandedBuffers; - -} - -/* Date functions */ - -function twoDigits (value) { - - var string = '00' + value; - return string.substr(string.length - 2); - -} - -function formatFilename (timestamp) { - - var date = new Date(timestamp); - - return date.getUTCFullYear() + twoDigits(date.getUTCMonth() + 1) + twoDigits(date.getUTCDate()) + '_' + twoDigits(date.getUTCHours()) + twoDigits(date.getUTCMinutes()) + twoDigits(date.getUTCSeconds()) + '.WAV'; - -} - -function formatCommentDate (timestamp) { - - var date = new Date(timestamp); - - return 'Recorded at ' + twoDigits(date.getUTCHours()) + ':' + twoDigits(date.getUTCMinutes()) + ':' + twoDigits(date.getUTCSeconds()) + ' ' + twoDigits(date.getUTCDate()) + '/' + twoDigits(date.getUTCMonth() + 1) + '/' + date.getUTCFullYear(); - -} - -/* Expand a T.WAV file */ - -function expand (inputPath, maximumFileDuration, callback) { - - var fi, fo, fileSize, header, headerCheck, progress, inputFileDataSize, aligned, regex, filename, timestamp, outputPath, fileIsOpen, numberOfBytes, numberOfInputBytes, numberOfBlankBytes, inputFileBytesRead, numberOfBytesWritten, numberOfDeferredBlankBytes, numberOfInputBytesAlreadyWritten, bytesToWriteInEachOutputFile; - - /* Check parameter */ - - maximumFileDuration = maximumFileDuration || SECONDS_IN_DAY; - - if (maximumFileDuration !== Math.round(maximumFileDuration)) { - - return { - success: false, - error: 'Maximum file duration must be an integer.' - }; - - } - - if (maximumFileDuration <= 0) { - - return { - success: false, - error: 'Maximum file duration must be greater than zero.' - }; - - } - - /* Generate the output filename */ - - if (!inputPath.includes('T.wav') && !inputPath.includes('T.WAV')) { - - return { - success: false, - error: 'File name is incorrect.' - }; - - } - - /* Open input file */ - - try { - - fi = fs.openSync(inputPath, 'r'); - - } catch (e) { - - return { - success: false, - error: 'Could not open input file.' - }; - - } - - /* Find the input file size */ - - try { - - fileSize = fs.statSync(inputPath).size; - - } catch (e) { - - return { - success: false, - error: 'Could not read input file size.' - }; - - } - - if (fileSize === 0) { - - return { - success: false, - error: 'Input file has zero size.' - }; - - } - - if (fileSize <= wavHeader.LENGTH_OF_WAV_HEADER) { - - return { - success: false, - error: 'Input file is too small.' - }; - - } - - /* Read the header */ - - try { - - fs.readSync(fi, headerBuffer, 0, wavHeader.LENGTH_OF_WAV_HEADER, null); - - } catch (e) { - - return { - success: false, - error: 'Could not read the input WAV header.' - }; - - } - - /* Check the header */ - - header = wavHeader.readHeader(headerBuffer); - - headerCheck = wavHeader.checkHeader(header, fileSize); - - if (headerCheck.success === false) { - - return { - success: false, - error: headerCheck.error - }; - - } - - /* Check the header comment format */ - - if (header.icmt.comment.search(DATE_REGEX) !== 0) { - - return { - success: false, - error: 'Comment format is incorrect.' - }; - - } - - /* Read the timestamp from the header */ - - regex = DATE_REGEX.exec(header.icmt.comment); - - timestamp = Date.UTC(regex[6], regex[5] - 1, regex[4], regex[1], regex[2], regex[3]); - - filename = formatFilename(timestamp); - - outputPath = path.join(path.parse(inputPath).dir, filename); - - /* Read determine settings from the input file */ - - inputFileDataSize = header.data.size; - - bytesToWriteInEachOutputFile = NUMBER_OF_BYTES_IN_SAMPLE * header.wavFormat.samplesPerSecond * maximumFileDuration; - - /* Set up to process the file */ - - progress = 10; - - aligned = false; - - fileIsOpen = false; - - inputFileBytesRead = 0; - - numberOfBytesWritten = 0; - - numberOfDeferredBlankBytes = 0; - - /* Main loop */ - - try { - - while (inputFileBytesRead < inputFileDataSize) { - - if (inputFileBytesRead / inputFileDataSize > progress / 100) { - - if (callback) callback(progress); - - progress += 10; - - } - - /* Determine number of bytes to read */ - - numberOfBytes = aligned ? Math.min(inputFileDataSize - inputFileBytesRead, EXPANSION_BUFFER_SIZE_IN_BYTES) : EXPANSION_BUFFER_SIZE_IN_BYTES - wavHeader.LENGTH_OF_WAV_HEADER; - - aligned = true; - - /* Read the data from the input file */ - - fs.readSync(fi, fileBuffer, 0, numberOfBytes, null); - - inputFileBytesRead += numberOfBytes; - - /* Check if this is a normal buffer or an encoded buffer */ - - if (numberOfBytes === EXPANSION_BUFFER_SIZE_IN_BYTES) { - - numberOfBlankBytes = EXPANSION_BUFFER_SIZE_IN_BYTES * decodeExpansionBuffer(fileBuffer); - numberOfInputBytes = numberOfBlankBytes > 0 ? 0 : EXPANSION_BUFFER_SIZE_IN_BYTES; - - } else { - - numberOfBlankBytes = 0; - numberOfInputBytes = numberOfBytes; - - } - - /* Process these input bytes */ - - numberOfInputBytesAlreadyWritten = 0; - - while (numberOfBlankBytes > 0 || numberOfInputBytes > 0) { - - if (numberOfBlankBytes > 0) { - - if (fileIsOpen) { - - numberOfBytes = Math.min(numberOfBlankBytes, bytesToWriteInEachOutputFile - numberOfBytesWritten); - - numberOfBytesWritten += numberOfBytes; - - numberOfBlankBytes -= numberOfBytes; - - while (numberOfBytes > 0) { - - fs.writeSync(fo, blankFileBuffer, 0, Math.min(numberOfBytes, EXPANSION_BUFFER_SIZE_IN_BYTES), null); - - numberOfBytes -= Math.min(numberOfBytes, EXPANSION_BUFFER_SIZE_IN_BYTES); - - } - - } else { - - numberOfBytes = Math.min(numberOfBlankBytes, bytesToWriteInEachOutputFile - numberOfDeferredBlankBytes); - - numberOfDeferredBlankBytes += numberOfBytes; - - numberOfBlankBytes -= numberOfBytes; - - } - - } - - if (numberOfInputBytes > 0) { - - if (fileIsOpen) { - - numberOfBytes = Math.min(numberOfInputBytes, bytesToWriteInEachOutputFile - numberOfBytesWritten, EXPANSION_BUFFER_SIZE_IN_BYTES - numberOfInputBytesAlreadyWritten); - - fs.writeSync(fo, fileBuffer, numberOfInputBytesAlreadyWritten, numberOfBytes); - - numberOfInputBytesAlreadyWritten += numberOfBytes; - - numberOfBytesWritten += numberOfBytes; - - numberOfInputBytes -= numberOfBytes; - - } else { - - fo = fs.openSync(outputPath, 'w'); - - fs.writeSync(fo, blankHeaderBuffer, 0, wavHeader.LENGTH_OF_WAV_HEADER, null); - - fileIsOpen = true; - - numberOfBlankBytes += numberOfDeferredBlankBytes; - - numberOfDeferredBlankBytes = 0; - - continue; - - } - - } - - if (inputFileBytesRead === inputFileDataSize || numberOfBytesWritten === bytesToWriteInEachOutputFile || numberOfDeferredBlankBytes === bytesToWriteInEachOutputFile) { - - /* Write header and close file if it has been opened */ - - if (fileIsOpen === true) { - - wavHeader.updateDataSize(header, numberOfBytesWritten); - - wavHeader.overwriteComment(header, formatCommentDate(timestamp)); - - wavHeader.writeHeader(headerBuffer, header); - - fs.writeSync(fo, headerBuffer, 0, wavHeader.LENGTH_OF_WAV_HEADER, 0); - - fs.closeSync(fo); - - } - - /* Reset for next file */ - - numberOfBytesWritten = 0; - - numberOfDeferredBlankBytes = 0; - - timestamp += maximumFileDuration * MILLISECONDS_IN_SECONDS; - - filename = formatFilename(timestamp); - - outputPath = path.join(path.parse(inputPath).dir, filename); - - fileIsOpen = false; - - } - - } - - } - - fs.closeSync(fi); - - } catch (e) { - - return { - success: false, - error: 'Error occurred while processing file. ' + e - }; - - } - - if (callback) { - - callback(100); - - } - - /* Return success */ - - return { - success: true, - error: null - }; - -} - -/* Export expand */ - -exports.expand = expand; diff --git a/expansion/wavHeader.js b/expansion/wavHeader.js deleted file mode 100644 index c3f08a4..0000000 --- a/expansion/wavHeader.js +++ /dev/null @@ -1,292 +0,0 @@ -/**************************************************************************** - * wavHeader.js - * openacousticdevices.info - * June 2020 - *****************************************************************************/ - -'use strict'; - -/* WAV header constants */ - -const UINT16_LENGTH = 2; -const UINT32_LENGTH = 4; -const RIFF_ID_LENGTH = 4; - -/* WAV format constants */ - -const PCM_FORMAT = 1; -const NUMBER_OF_CHANNELS = 1; -const NUMBER_OF_BITS_IN_SAMPLE = 16; -const NUMBER_OF_BYTES_IN_SAMPLE = 2; - -/* WAV header base component read functions */ - -function readString (state, length) { - - if (state.buffer.length - state.index < length) throw new Error('WAVE header exceeded buffer length.'); - - var result = state.buffer.toString('utf8', state.index, state.index + length).replace(/\0/g, ''); - state.index += length; - return result; - -} - -function readUInt32LE (state) { - - if (state.buffer.length - state.index < UINT32_LENGTH) throw new Error('WAVE header exceeded buffer length.'); - - var result = state.buffer.readUInt32LE(state.index); - state.index += UINT32_LENGTH; - return result; - -} - -function readUInt16LE (state) { - - if (state.buffer.length - state.index < UINT16_LENGTH) throw new Error('WAVE header exceeded buffer length.'); - - var result = state.buffer.readUInt16LE(state.index); - state.index += UINT16_LENGTH; - return result; - -} - -/* WAV header high-level component read functions */ - -function readID (state, id) { - - const result = readString(state, id.length); - - if (result !== id) throw new Error('Could not find ' + id + ' ID.'); - - return result; - -} - -function readChunk (state, id) { - - const result = {}; - - result.id = readString(state, RIFF_ID_LENGTH); - - if (result.id !== id) throw new Error('Could not find ' + id.replace(' ', '') + ' chunk ID.'); - - result.size = readUInt32LE(state); - - return result; - -} - -/* WAV header component write functions */ - -function writeString (state, string, length, zeroTerminated) { - - const maximumWriteLength = zeroTerminated ? Math.min(string.length, length - 1) : Math.min(string.length, length); - state.buffer.fill(0, state.index, state.index + length); - state.buffer.write(string, state.index, maximumWriteLength, 'utf8'); - state.index += length; - -} - -function writeUInt32LE (state, value) { - - state.buffer.writeUInt32LE(value, state.index); - state.index += UINT32_LENGTH; - -} - -function writeUInt16LE (state, value) { - - state.buffer.writeUInt16LE(value, state.index); - state.index += UINT16_LENGTH; - -} - -function writeChunk (state, chunk) { - - writeString(state, chunk.id, RIFF_ID_LENGTH, false); - writeUInt32LE(state, chunk.size); - -} - -/* WAV header read and write functions */ - -function readHeader (buffer, fileSize) { - - const header = {}; - - const state = {buffer: buffer, index: 0}; - - try { - - /* Read RIFF chunk */ - - header.riff = readChunk(state, 'RIFF'); - - if (header.riff.size + RIFF_ID_LENGTH + UINT32_LENGTH !== fileSize) { - - return { - success: false, - error: 'RIFF chunk size does not match file size.' - }; - - } - - /* Read WAVE ID */ - - header.format = readID(state, 'WAVE'); - - /* Read FMT chunk */ - - header.fmt = readChunk(state, 'fmt '); - - header.wavFormat = {}; - header.wavFormat.format = readUInt16LE(state); - header.wavFormat.numberOfChannels = readUInt16LE(state); - header.wavFormat.samplesPerSecond = readUInt32LE(state); - header.wavFormat.bytesPerSecond = readUInt32LE(state); - header.wavFormat.bytesPerCapture = readUInt16LE(state); - header.wavFormat.bitsPerSample = readUInt16LE(state); - - if (header.wavFormat.format !== PCM_FORMAT || header.wavFormat.numberOfChannels !== NUMBER_OF_CHANNELS || header.wavFormat.bytesPerSecond !== NUMBER_OF_BYTES_IN_SAMPLE * header.wavFormat.samplesPerSecond || header.wavFormat.bytesPerCapture !== NUMBER_OF_BYTES_IN_SAMPLE || header.wavFormat.bitsPerSample !== NUMBER_OF_BITS_IN_SAMPLE) { - - return { - success: false, - error: 'Unexpected WAVE format.' - }; - - } - - /* Read LIST chunk */ - - header.list = readChunk(state, 'LIST'); - - /* Read INFO ID */ - - header.info = readID(state, 'INFO'); - - /* Read ICMT chunk */ - - header.icmt = readChunk(state, 'ICMT'); - - header.icmt.comment = readString(state, header.icmt.size); - - /* Read IART chunk */ - - header.iart = readChunk(state, 'IART'); - - header.iart.artist = readString(state, header.iart.size); - - /* Check LIST chunk size */ - - if (header.list.size !== 3 * RIFF_ID_LENGTH + 2 * UINT32_LENGTH + header.iart.size + header.icmt.size) { - - return { - success: false, - error: 'LIST chunk size does not match total size of INFO, ICMT and IART chunks.' - }; - - } - - /* Read DATA chunk */ - - header.data = readChunk(state, 'data'); - - /* Set the header size and check DATA chunk size */ - - header.size = state.index; - - if (header.data.size + header.size !== fileSize) { - - return { - success: false, - error: 'DATA chunk size does not match file size.' - }; - - } - - /* Success */ - - return { - header: header, - success: true, - error: null - }; - - } catch (e) { - - /* Header has exceed file buffer length */ - - return { - success: false, - error: e.message - }; - - } - -} - -function writeHeader (buffer, header) { - - const state = {buffer: buffer, index: 0}; - - writeChunk(state, header.riff); - - writeString(state, header.format, RIFF_ID_LENGTH, false); - - writeChunk(state, header.fmt); - - writeUInt16LE(state, header.wavFormat.format); - writeUInt16LE(state, header.wavFormat.numberOfChannels); - writeUInt32LE(state, header.wavFormat.samplesPerSecond); - writeUInt32LE(state, header.wavFormat.bytesPerSecond); - writeUInt16LE(state, header.wavFormat.bytesPerCapture); - writeUInt16LE(state, header.wavFormat.bitsPerSample); - - writeChunk(state, header.list); - - writeString(state, header.info, RIFF_ID_LENGTH, false); - - writeChunk(state, header.icmt); - writeString(state, header.icmt.comment, header.icmt.size, true); - - writeChunk(state, header.iart); - writeString(state, header.iart.artist, header.iart.size, true); - - writeChunk(state, header.data); - - return buffer; - -} - -/* Functions to update header */ - -function updateDataSize (header, size) { - - header.riff.size = header.size + size - UINT32_LENGTH - RIFF_ID_LENGTH; - - header.data.size = size; - -} - -function updateComment (header, comment) { - - header.icmt.comment = comment; - -} - -function overwriteComment (header, comment) { - - var length = Math.min(comment.length, header.icmt.size - 1); - - header.icmt.comment = comment.substr(0, length) + header.icmt.comment.substr(length); - -} - -/* Exports */ - -exports.writeHeader = writeHeader; -exports.readHeader = readHeader; -exports.updateDataSize = updateDataSize; -exports.updateComment = updateComment; -exports.overwriteComment = overwriteComment; diff --git a/main.js b/main.js index b6a1a11..01d6fc9 100644 --- a/main.js +++ b/main.js @@ -27,9 +27,9 @@ require('electron-debug')({ devToolsMode: 'undocked' }); -var mainWindow, aboutWindow, expansionWindow, splitWindow; +var mainWindow, aboutWindow, expansionWindow, splitWindow, downsampleWindow; -var expandProgressBar, splitProgressBar; +var expandProgressBar, splitProgressBar, downsampleProgressBar; function shrinkWindowHeight (windowHeight) { @@ -60,7 +60,7 @@ function openSplitWindow () { splitWindow = new BrowserWindow({ width: 565, height: shrinkWindowHeight(403), - title: 'Split AudioMoth Recordings', + title: 'Split AudioMoth WAV Files', useContentSize: true, resizable: false, fullscreenable: false, @@ -72,7 +72,7 @@ function openSplitWindow () { }); splitWindow.setMenu(null); - splitWindow.loadURL(path.join('file://', __dirname, 'expansion/split.html')); + splitWindow.loadURL(path.join('file://', __dirname, 'processing/split.html')); splitWindow.webContents.on('dom-ready', function () { @@ -117,7 +117,7 @@ function openExpansionWindow () { expansionWindow = new BrowserWindow({ width: 565, height: shrinkWindowHeight(575), - title: 'Expand AudioMoth T.WAV Recordings', + title: 'Expand AudioMoth T.WAV Files', useContentSize: true, resizable: false, fullscreenable: false, @@ -129,7 +129,7 @@ function openExpansionWindow () { }); expansionWindow.setMenu(null); - expansionWindow.loadURL(path.join('file://', __dirname, 'expansion/expansion.html')); + expansionWindow.loadURL(path.join('file://', __dirname, 'processing/expansion.html')); expansionWindow.webContents.on('dom-ready', function () { @@ -161,6 +161,63 @@ function openExpansionWindow () { } +function openDownsamplingWindow () { + + if (downsampleWindow) { + + return; + + } + + const iconLocation = (process.platform === 'linux') ? '/build/icon.png' : '/build/icon.ico'; + + downsampleWindow = new BrowserWindow({ + width: 565, + height: shrinkWindowHeight(380), + title: 'Downsample AudioMoth WAV Files', + useContentSize: true, + resizable: false, + fullscreenable: false, + icon: path.join(__dirname, iconLocation), + parent: mainWindow, + webPreferences: { + nodeIntegration: true + } + }); + + downsampleWindow.setMenu(null); + downsampleWindow.loadURL(path.join('file://', __dirname, 'processing/downsampling.html')); + + downsampleWindow.webContents.on('dom-ready', function () { + + mainWindow.webContents.send('poll-night-mode'); + + }); + + ipcMain.on('night-mode-poll-reply', (e, nightMode) => { + + if (downsampleWindow) { + + downsampleWindow.webContents.send('night-mode', nightMode); + + } + + }); + + downsampleWindow.on('close', function (e) { + + if (downsampleProgressBar) { + + e.preventDefault(); + + } + + downsampleWindow = null; + + }); + +} + function openAboutWindow () { if (aboutWindow) { @@ -351,7 +408,7 @@ app.on('ready', function () { }, { type: 'separator' }, { - label: 'Split AudioMoth WAV Recordings', + label: 'Split AudioMoth WAV Files', accelerator: 'CommandOrControl+P', click: function () { @@ -359,12 +416,20 @@ app.on('ready', function () { } }, { - label: 'Expand AudioMoth T.WAV Recordings', + label: 'Expand AudioMoth T.WAV Files', accelerator: 'CommandOrControl+E', click: function () { openExpansionWindow(); + } + }, { + label: 'Downsample AudioMoth WAV Files', + accelerator: 'CommandOrControl+D', + click: function () { + + openDownsamplingWindow(); + } }, { type: 'separator' @@ -715,6 +780,139 @@ ipcMain.on('poll-split-cancelled', (event) => { }); +/* Downsampling progress bar functions */ + +ipcMain.on('start-downsample-bar', (event, fileCount) => { + + if (downsampleProgressBar) { + + return; + + } + + let detail = 'Starting to downsample file'; + detail += (fileCount > 1) ? 's' : ''; + detail += '.'; + + downsampleProgressBar = new ProgressBar({ + title: 'AudioMoth Configuration App', + text: 'Downsampling files...', + detail: detail, + closeOnComplete: false, + indeterminate: false, + browserWindow: { + parent: splitWindow, + webPreferences: { + nodeIntegration: true + }, + closable: true, + modal: false + }, + maxValue: fileCount * 100 + }); + + downsampleProgressBar.on('aborted', () => { + + if (downsampleProgressBar) { + + downsampleProgressBar.close(); + downsampleProgressBar = null; + + } + + }); + +}); + +ipcMain.on('set-downsample-bar-progress', (event, fileNum, progress, name) => { + + const index = fileNum + 1; + const fileCount = downsampleProgressBar.getOptions().maxValue / 100; + + if (downsampleProgressBar) { + + downsampleProgressBar.value = (fileNum * 100) + progress; + downsampleProgressBar.detail = 'Downsampling ' + name + ' (' + index + ' of ' + fileCount + ').'; + + } + +}); + +ipcMain.on('set-downsample-bar-error', (event, name) => { + + if (downsampleProgressBar) { + + downsampleProgressBar.detail = 'Error when downsampling ' + name + '.'; + + } + +}); + +ipcMain.on('set-downsample-bar-completed', (event, successCount, errorCount, errorWritingLog) => { + + if (downsampleProgressBar) { + + let messageText; + + downsampleProgressBar.setCompleted(); + + if (errorCount > 0) { + + messageText = 'Errors occurred in ' + errorCount + ' file'; + messageText += (errorCount === 1 ? '' : 's'); + messageText += '.
'; + + if (errorWritingLog) { + + messageText += 'Failed to write ERRORS.TXT to destination.'; + + } else { + + messageText += 'See ERRORS.TXT for details.'; + + } + + } else { + + messageText = 'Successfully downsampled ' + successCount + ' file'; + messageText += (successCount === 1 ? '' : 's'); + messageText += '.'; + + } + + downsampleProgressBar.detail = messageText; + + setTimeout(function () { + + downsampleProgressBar.close(); + downsampleProgressBar = null; + + if (downsampleWindow) { + + downsampleWindow.send('downsample-summary-closed'); + + } + + }, 5000); + + } + +}); + +ipcMain.on('poll-downsample-cancelled', (event) => { + + if (downsampleProgressBar) { + + event.returnValue = false; + + } else { + + event.returnValue = true; + + } + +}); + /* Update which amplitude threshold scale option is checked in menu */ ipcMain.on('set-amplitude-threshold-scale', (event, index) => { diff --git a/package.json b/package.json index 1935d57..44afacc 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "AudioMoth-Config", - "version": "1.7.0", + "version": "1.8.0", "description": "The configuration app for the AudioMoth acoustic monitoring device.", "main": "main.js", "author": "openacousticdevices.info", @@ -68,7 +68,7 @@ }, "dependencies": { "audiomoth-hid": "^2.1.0", - "audiomoth-utils": "^1.1.0", + "audiomoth-utils": "^1.2.0", "bootstrap": "4.3.1", "bootstrap-slider": "^10.6.2", "electron-debug": "3.0.1", diff --git a/packetReader.js b/packetReader.js index d657b87..d0b52d9 100644 --- a/packetReader.js +++ b/packetReader.js @@ -296,7 +296,7 @@ exports.read = (packet) => { console.log('Active recording periods:', activeStartStopPeriods); - for (let j = 0; j < activeStartStopPeriods.length; j++) { + for (let j = 0; j < activeStartStopPeriods; j++) { console.log('Start: ' + formatTime(startStopPeriods[j].startMinutes) + ' - Stop: ' + formatTime(startStopPeriods[j].stopMinutes)); diff --git a/processing/downsampling.html b/processing/downsampling.html new file mode 100644 index 0000000..29cc0d9 --- /dev/null +++ b/processing/downsampling.html @@ -0,0 +1,222 @@ + + + + + + + Downsample AudioMoth WAV Files + + + + +
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
816324896192250384
Sample rate (kHz): + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ Writing WAV files to source folder. +
+
+
+ +
+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
FileFolder
Selection type: + + + +
+
+
+
+ +
+
+ +
+
+
+ No AudioMoth WAV files selected. +
+
+
+
+
+ +
+
+ +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/expansion/expansion.html b/processing/expansion.html similarity index 95% rename from expansion/expansion.html rename to processing/expansion.html index b785153..5418327 100644 --- a/expansion/expansion.html +++ b/processing/expansion.html @@ -4,7 +4,7 @@ - Expand AudioMoth T.WAV Recordings + Expand AudioMoth T.WAV Files @@ -48,7 +48,7 @@ - + @@ -60,7 +60,6 @@ - @@ -83,7 +82,10 @@ +
Maximum file length: - + + +
@@ -126,7 +128,7 @@ - + @@ -138,7 +140,6 @@ - @@ -161,7 +162,10 @@ +
Maximum file length: - + + +
@@ -284,7 +288,7 @@
- No AudioMoth T.WAV files selected. + No AudioMoth T.WAV files selected.
diff --git a/expansion/split.html b/processing/split.html similarity index 96% rename from expansion/split.html rename to processing/split.html index 91d2c48..c00c040 100644 --- a/expansion/split.html +++ b/processing/split.html @@ -4,7 +4,7 @@ - Split AudioMoth Recordings + Split AudioMoth WAV Files @@ -20,7 +20,7 @@ - + @@ -32,7 +32,6 @@ - @@ -57,6 +56,9 @@ +
Maximum file length: + +
@@ -156,7 +158,7 @@
- No AudioMoth WAV files selected. + No AudioMoth WAV files selected.
diff --git a/expansion/uiCommon.js b/processing/uiCommon.js similarity index 93% rename from expansion/uiCommon.js rename to processing/uiCommon.js index b6f2442..d629865 100644 --- a/expansion/uiCommon.js +++ b/processing/uiCommon.js @@ -219,19 +219,3 @@ prefixCheckbox.addEventListener('change', () => { } }); - -/* Update label to notify user if a custom output directtory is being used */ - -exports.updateOutputLabel = (outputDir) => { - - if (outputDir === '') { - - outputLabel.textContent = 'Writing WAV files to source folder.'; - - } else { - - outputLabel.textContent = 'Writing WAV files to custom folder.'; - - } - -}; diff --git a/processing/uiDownsampling.js b/processing/uiDownsampling.js new file mode 100644 index 0000000..cd95b8e --- /dev/null +++ b/processing/uiDownsampling.js @@ -0,0 +1,298 @@ +/**************************************************************************** + * uiDownsampling.js + * openacousticdevices.info + * June 2022 + *****************************************************************************/ + +'use strict'; + +/* global document */ + +const electron = require('electron'); +const dialog = electron.remote.dialog; + +/* Get functions which control elements common to the expansion, split, and downsample windows */ +const ui = require('./uiCommon.js'); +const uiOutput = require('./uiOutput.js'); + +const path = require('path'); +const fs = require('fs'); + +const audiomothUtils = require('audiomoth-utils'); + +var currentWindow = electron.remote.getCurrentWindow(); + +const SAMPLE_RATES = [8000, 16000, 32000, 48000, 96000, 192000, 250000, 384000]; + +const FILE_REGEX = /^(\d\d\d\d\d\d\d\d_)?\d\d\d\d\d\d.WAV$/; + +const sampleRateRadioHolder = document.getElementById('sample-rate-holder'); +const disabledSampleRateRadioHolder = document.getElementById('disabled-sample-rate-holder'); + +const sampleRateRadios = document.getElementsByName('sample-rate-radio'); +const disabledSampleRateRadios = document.getElementsByName('disabled-sample-rate-radio'); + +const selectionRadios = document.getElementsByName('selection-radio'); + +const prefixCheckbox = document.getElementById('prefix-checkbox'); +const prefixInput = document.getElementById('prefix-input'); + +const fileLabel = document.getElementById('file-label'); +const fileButton = document.getElementById('file-button'); +const downsampleButton = document.getElementById('downsample-button'); + +var files = []; +var downsampling = false; + +/* Disable UI elements in main window while progress bar is open and downsample is in progress */ + +function disableUI () { + + fileButton.disabled = true; + downsampleButton.disabled = true; + selectionRadios[0].disabled = true; + selectionRadios[1].disabled = true; + + sampleRateRadioHolder.style.display = 'none'; + disabledSampleRateRadioHolder.style.display = ''; + + uiOutput.disableOutputCheckbox(); + uiOutput.disableOutputButton(); + + prefixCheckbox.disabled = true; + prefixInput.disabled = true; + +} + +function enableUI () { + + fileButton.disabled = false; + downsampleButton.disabled = false; + selectionRadios[0].disabled = false; + selectionRadios[1].disabled = false; + + sampleRateRadioHolder.style.display = ''; + disabledSampleRateRadioHolder.style.display = 'none'; + + uiOutput.enableOutputCheckbox(); + uiOutput.enableOutputButton(); + + prefixCheckbox.disabled = false; + + if (prefixCheckbox.checked) { + + prefixInput.disabled = false; + + } + + downsampling = false; + +} + +/* Split selected files */ + +function downsampleFiles () { + + if (!files) { + + return; + + } + + let successCount = 0; + let errorCount = 0; + const errors = []; + const errorFiles = []; + + let errorFilePath; + + for (let i = 0; i < files.length; i++) { + + /* If progress bar is closed, the downsample task is considered cancelled. This will contact the main thread and ask if that has happened */ + + const cancelled = electron.ipcRenderer.sendSync('poll-downsample-cancelled'); + + if (cancelled) { + + console.log('Downsample cancelled.'); + enableUI(); + return; + + } + + /* Let the main thread know what value to set the progress bar to */ + + electron.ipcRenderer.send('set-downsample-bar-progress', i, 0, path.basename(files[i])); + + const sampleRate = SAMPLE_RATES[ui.getSelectedRadioValue('sample-rate-radio')]; + + console.log('Downsampling:', files[i]); + console.log('New sample rate:', sampleRate); + + console.log('-'); + + /* Check if the optional prefix/output directory setttings are being used. If left as null, splitter will put file(s) in the same directory as the input with no prefix */ + + const outputPath = uiOutput.isChecked() ? uiOutput.getOutputDir() : null; + const prefix = (prefixCheckbox.checked && prefixInput.value !== '') ? prefixInput.value : null; + + const response = audiomothUtils.downsample(files[i], outputPath, prefix, sampleRate, (progress) => { + + electron.ipcRenderer.send('set-downsample-bar-progress', i, progress, path.basename(files[i])); + + }); + + if (response.success) { + + successCount++; + + } else { + + /* Add error to log file */ + + errorCount++; + errors.push(response.error); + errorFiles.push(files[i]); + + electron.ipcRenderer.send('set-downsample-bar-error', path.basename(files[i])); + + if (errorCount === 1) { + + const errorFileLocation = uiOutput.isChecked() ? uiOutput.getOutputDir() : path.dirname(errorFiles[0]); + + errorFilePath = path.join(errorFileLocation, 'ERRORS.TXT'); + + } + + let fileContent = ''; + + for (let j = 0; j < errorCount; j++) { + + fileContent += path.basename(errorFiles[j]) + ' - ' + errors[j] + '\n'; + + } + + try { + + fs.writeFileSync(errorFilePath, fileContent); + + console.log('Error summary written to ' + errorFilePath); + + } catch (err) { + + console.error(err); + electron.ipcRenderer.send('set-downsample-bar-completed', successCount, errorCount, true); + return; + + } + + ui.sleep(3000); + + } + + } + + /* Notify main thread that split is complete so progress bar is closed */ + + electron.ipcRenderer.send('set-downsample-bar-completed', successCount, errorCount, false); + +} + +/* When the progress bar is complete and the summary window at the end has been displayed for a fixed amount of ttime, it will close and this re-enables the UI */ + +electron.ipcRenderer.on('downsample-summary-closed', enableUI); + +/* Update label to reflect new file/folder selection */ + +function updateInputDirectoryDisplay (directoryArray) { + + if (directoryArray.length === 0 || !directoryArray) { + + fileLabel.innerHTML = 'No AudioMoth WAV files selected.'; + downsampleButton.disabled = true; + + } else { + + fileLabel.innerHTML = 'Found '; + fileLabel.innerHTML += directoryArray.length + ' AudioMoth WAV file'; + fileLabel.innerHTML += (directoryArray.length === 1 ? '' : 's'); + fileLabel.innerHTML += '.'; + downsampleButton.disabled = false; + + } + +} + +/* Reset UI back to default state, clearing the selected files */ + +function resetUI () { + + files = []; + + fileLabel.innerHTML = 'No AudioMoth WAV files selected.'; + + downsampleButton.disabled = true; + + sampleRateRadioHolder.style.display = 'none'; + disabledSampleRateRadioHolder.style.display = ''; + + ui.updateButtonText(); + +} + +/* Whenever the file/folder radio button changes, reset the UI */ + +selectionRadios[0].addEventListener('change', resetUI); +selectionRadios[1].addEventListener('change', resetUI); + +/* Select/process file(s) buttons */ + +fileButton.addEventListener('click', () => { + + files = ui.selectRecordings(FILE_REGEX); + + updateInputDirectoryDisplay(files); + + ui.updateButtonText(); + +}); + +downsampleButton.addEventListener('click', () => { + + if (downsampling) { + + return; + + } + + if ((!prefixCheckbox.checked || prefixInput.value === '') && (!uiOutput.isChecked() || uiOutput.getOutputDir() === '')) { + + dialog.showMessageBox(currentWindow, { + type: 'error', + title: 'Cannot downsample with current settings', + message: 'Without a prefix or custom destination, downsampling will overwrite the original file. Set one of these values to continue.' + }); + + return; + + } + + downsampling = true; + disableUI(); + + electron.ipcRenderer.send('start-downsample-bar', files.length); + setTimeout(downsampleFiles, 2000); + +}); + +for (let i = 0; i < sampleRateRadios.length; i++) { + + sampleRateRadios[i].addEventListener('click', () => { + + // Match hidden sample rate radios which are only displayed when UI is disabled + + disabledSampleRateRadios[i].checked = true; + + }); + +} diff --git a/expansion/uiExpansion.js b/processing/uiExpansion.js similarity index 88% rename from expansion/uiExpansion.js rename to processing/uiExpansion.js index 1251a5b..eadf316 100644 --- a/expansion/uiExpansion.js +++ b/processing/uiExpansion.js @@ -9,18 +9,18 @@ /* global document */ const electron = require('electron'); -const dialog = electron.remote.dialog; -/* Get functions which control elements common to the expansion and split windows */ +/* Get functions which control elements common to the expansion, split, and downsample windows */ const ui = require('./uiCommon.js'); +const uiOutput = require('./uiOutput.js'); const path = require('path'); const fs = require('fs'); const audiomothUtils = require('audiomoth-utils'); -const MAX_LENGTHS = [5, 10, 15, 30, 60, 300, 600, 3600]; -const MAX_LENGTH_STRINGS = ['5 seconds', '10 seconds', '15 seconds', '30 seconds', '1 minute', '5 minutes', '10 minutes', '1 hour']; +const MAX_LENGTHS = [1, 5, 10, 15, 30, 60, 300, 600, 3600]; +const MAX_LENGTH_STRINGS = ['1 second', '5 seconds', '10 seconds', '15 seconds', '30 seconds', '1 minute', '5 minutes', '10 minutes', '1 hour']; const FILE_REGEX = /^(\d\d\d\d\d\d\d\d_)?\d\d\d\d\d\dT.WAV$/; @@ -43,12 +43,6 @@ const overviewPanel = document.getElementById('overview-panel'); const prefixCheckbox = document.getElementById('prefix-checkbox'); const prefixInput = document.getElementById('prefix-input'); -const outputCheckbox = document.getElementById('output-checkbox'); -const outputButton = document.getElementById('output-button'); -const outputLabel = document.getElementById('output-label'); - -var outputDir = ''; - const fileLabel = document.getElementById('file-label'); const fileButton = document.getElementById('file-button'); const expandButton = document.getElementById('expand-button'); @@ -83,8 +77,8 @@ function disableUI () { silentFilesCheckbox.disabled = true; alignmentCheckbox.disabled = true; - outputCheckbox.disabled = true; - outputButton.disabled = true; + uiOutput.disableOutputCheckbox(); + uiOutput.disableOutputButton(); prefixCheckbox.disabled = true; prefixInput.disabled = true; @@ -123,8 +117,8 @@ function enableUI () { silentFilesCheckbox.disabled = !durationMaxLengthCheckbox.checked; alignmentCheckbox.disabled = false; - outputCheckbox.disabled = false; - outputButton.disabled = false; + uiOutput.enableOutputCheckbox(); + uiOutput.enableOutputButton(); prefixCheckbox.disabled = false; @@ -236,7 +230,7 @@ function expandFiles () { /* Check if the optional prefix/output directory setttings are being used. If left as null, expander will put expanded file(s) in the same directory as the input with no prefix */ - const outputPath = outputCheckbox.checked ? outputDir : null; + const outputPath = uiOutput.isChecked() ? uiOutput.getOutputDir() : null; const prefix = (prefixCheckbox.checked && prefixInput.value !== '') ? prefixInput.value : null; const response = audiomothUtils.expand(files[i], outputPath, prefix, expansionType, maxLength, generateSilentFiles, alignToSecondTransitions, (progress) => { @@ -261,7 +255,7 @@ function expandFiles () { if (errorCount === 1) { - const errorFileLocation = outputCheckbox.checked ? outputDir : path.dirname(errorFiles[0]); + const errorFileLocation = uiOutput.isChecked() ? uiOutput.getOutputDir() : path.dirname(errorFiles[0]); errorFilePath = path.join(errorFileLocation, 'ERRORS.TXT'); @@ -476,43 +470,6 @@ updateOverviewPanel(); updateFileMaxLengthUI('duration-max-length-ui', durationMaxLengthCheckbox); updateFileMaxLengthUI('event-max-length-ui', eventMaxLengthCheckbox); -/* Add listener which handles enabling/disabling custom output directory UI */ - -outputCheckbox.addEventListener('change', () => { - - if (outputCheckbox.checked) { - - outputLabel.classList.remove('grey'); - outputButton.disabled = false; - - } else { - - outputLabel.classList.add('grey'); - outputButton.disabled = true; - outputDir = ''; - ui.updateOutputLabel(outputDir); - - } - -}); - -/* Select a custom output directory. If Cancel is pressed, assume no custom direcotry is wantted */ - -outputButton.addEventListener('click', () => { - - const destinationName = dialog.showOpenDialogSync({ - title: 'Select Destination', - nameFieldLabel: 'Destination', - multiSelections: false, - properties: ['openDirectory'] - }); - - outputDir = (destinationName !== undefined) ? destinationName[0] : ''; - - ui.updateOutputLabel(outputDir); - -}); - /* Whenever tthe file/folder radio button changes, reset the UI */ selectionRadios[0].addEventListener('change', resetUI); diff --git a/processing/uiOutput.js b/processing/uiOutput.js new file mode 100644 index 0000000..0e91562 --- /dev/null +++ b/processing/uiOutput.js @@ -0,0 +1,107 @@ +/**************************************************************************** + * uiOutput.js + * openacousticdevices.info + * June 2022 + *****************************************************************************/ + +'use strict'; + +/* global document */ + +const electron = require('electron'); +const dialog = electron.remote.dialog; + +const outputCheckbox = document.getElementById('output-checkbox'); +const outputButton = document.getElementById('output-button'); +const outputLabel = document.getElementById('output-label'); + +var outputDir = ''; + +/* Update label to notify user if a custom output directtory is being used */ + +function updateOutputLabel (outputDir) { + + if (outputDir === '') { + + outputLabel.textContent = 'Writing WAV files to source folder.'; + + } else { + + outputLabel.textContent = 'Writing WAV files to custom folder.'; + + } + +}; + +/* Add listener which handles enabling/disabling custom output directory UI */ + +outputCheckbox.addEventListener('change', () => { + + if (outputCheckbox.checked) { + + outputLabel.classList.remove('grey'); + outputButton.disabled = false; + + } else { + + outputLabel.classList.add('grey'); + outputButton.disabled = true; + outputDir = ''; + updateOutputLabel(outputDir); + + } + +}); + +/* Select a custom output directory. If Cancel is pressed, assume no custom direcotry is wantted */ + +outputButton.addEventListener('click', () => { + + const destinationName = dialog.showOpenDialogSync({ + title: 'Select Destination', + nameFieldLabel: 'Destination', + multiSelections: false, + properties: ['openDirectory'] + }); + + outputDir = (destinationName !== undefined) ? destinationName[0] : ''; + + updateOutputLabel(outputDir); + +}); + +exports.disableOutputCheckbox = () => { + + outputCheckbox.disabled = true; + +}; + +exports.disableOutputButton = () => { + + outputButton.disabled = true; + +}; + +exports.enableOutputCheckbox = () => { + + outputCheckbox.disabled = false; + +}; + +exports.enableOutputButton = () => { + + outputButton.disabled = false; + +}; + +exports.isChecked = () => { + + return outputCheckbox.checked; + +}; + +exports.getOutputDir = () => { + + return outputDir; + +}; diff --git a/expansion/uiSplit.js b/processing/uiSplit.js similarity index 79% rename from expansion/uiSplit.js rename to processing/uiSplit.js index 06b713d..a011816 100644 --- a/expansion/uiSplit.js +++ b/processing/uiSplit.js @@ -11,8 +11,9 @@ const electron = require('electron'); const dialog = electron.remote.dialog; -/* Get functions which control elements common to the expansion and split windows */ +/* Get functions which control elements common to the expansion, split, and downsample windows */ const ui = require('./uiCommon.js'); +const uiOutput = require('./uiOutput.js'); const path = require('path'); const fs = require('fs'); @@ -21,7 +22,7 @@ const audiomothUtils = require('audiomoth-utils'); var currentWindow = electron.remote.getCurrentWindow(); -const MAX_LENGTHS = [5, 10, 15, 30, 60, 300, 600, 3600]; +const MAX_LENGTHS = [1, 5, 10, 15, 30, 60, 300, 600, 3600]; const FILE_REGEX = /^(\d\d\d\d\d\d\d\d_)?\d\d\d\d\d\d.WAV$/; @@ -32,12 +33,6 @@ const selectionRadios = document.getElementsByName('selection-radio'); const prefixCheckbox = document.getElementById('prefix-checkbox'); const prefixInput = document.getElementById('prefix-input'); -const outputCheckbox = document.getElementById('output-checkbox'); -const outputButton = document.getElementById('output-button'); -const outputLabel = document.getElementById('output-label'); - -var outputDir = ''; - const fileLabel = document.getElementById('file-label'); const fileButton = document.getElementById('file-button'); const splitButton = document.getElementById('split-button'); @@ -60,8 +55,8 @@ function disableUI () { } - outputCheckbox.disabled = true; - outputButton.disabled = true; + uiOutput.disableOutputCheckbox(); + uiOutput.disableOutputButton(); prefixCheckbox.disabled = true; prefixInput.disabled = true; @@ -81,8 +76,8 @@ function enableUI () { } - outputCheckbox.disabled = false; - outputButton.disabled = false; + uiOutput.enableOutputCheckbox(); + uiOutput.enableOutputButton(); prefixCheckbox.disabled = false; @@ -140,7 +135,7 @@ function splitFiles () { /* Check if the optional prefix/output directory setttings are being used. If left as null, splitter will put file(s) in the same directory as the input with no prefix */ - const outputPath = outputCheckbox.checked ? outputDir : null; + const outputPath = uiOutput.isChecked() ? uiOutput.getOutputDir() : null; const prefix = (prefixCheckbox.checked && prefixInput.value !== '') ? prefixInput.value : null; const response = audiomothUtils.split(files[i], outputPath, prefix, maxLength, (progress) => { @@ -165,7 +160,7 @@ function splitFiles () { if (errorCount === 1) { - const errorFileLocation = outputCheckbox.checked ? outputDir : path.dirname(errorFiles[0]); + const errorFileLocation = uiOutput.isChecked() ? uiOutput.getOutputDir() : path.dirname(errorFiles[0]); errorFilePath = path.join(errorFileLocation, 'ERRORS.TXT'); @@ -244,44 +239,7 @@ function resetUI () { } -/* Add listener which handles enabling/disabling custom output directory UI */ - -outputCheckbox.addEventListener('change', () => { - - if (outputCheckbox.checked) { - - outputLabel.classList.remove('grey'); - outputButton.disabled = false; - - } else { - - outputLabel.classList.add('grey'); - outputButton.disabled = true; - outputDir = ''; - ui.updateOutputLabel(outputDir); - - } - -}); - -/* Select a custom output directory. If Cancel is pressed, assume no custom direcotry is wantted */ - -outputButton.addEventListener('click', () => { - - const destinationName = dialog.showOpenDialogSync({ - title: 'Select Destination', - nameFieldLabel: 'Destination', - multiSelections: false, - properties: ['openDirectory'] - }); - - outputDir = (destinationName !== undefined) ? destinationName[0] : ''; - - ui.updateOutputLabel(outputDir); - -}); - -/* Whenever tthe file/folder radio button changes, reset the UI */ +/* Whenever the file/folder radio button changes, reset the UI */ selectionRadios[0].addEventListener('change', resetUI); selectionRadios[1].addEventListener('change', resetUI); @@ -306,7 +264,7 @@ splitButton.addEventListener('click', () => { } - if ((!prefixCheckbox.checked || prefixInput.value === '') && (!outputCheckbox.checked || outputDir === '')) { + if ((!prefixCheckbox.checked || prefixInput.value === '') && (!uiOutput.isChecked() || uiOutput.getOutputDir() === '')) { dialog.showMessageBox(currentWindow, { type: 'error',