diff --git a/main.js b/main.js index 01d6fc9..7ef34c9 100644 --- a/main.js +++ b/main.js @@ -59,7 +59,7 @@ function openSplitWindow () { splitWindow = new BrowserWindow({ width: 565, - height: shrinkWindowHeight(403), + height: shrinkWindowHeight(468), title: 'Split AudioMoth WAV Files', useContentSize: true, resizable: false, @@ -116,7 +116,7 @@ function openExpansionWindow () { expansionWindow = new BrowserWindow({ width: 565, - height: shrinkWindowHeight(575), + height: shrinkWindowHeight(643), title: 'Expand AudioMoth T.WAV Files', useContentSize: true, resizable: false, @@ -173,7 +173,7 @@ function openDownsamplingWindow () { downsampleWindow = new BrowserWindow({ width: 565, - height: shrinkWindowHeight(380), + height: shrinkWindowHeight(448), title: 'Downsample AudioMoth WAV Files', useContentSize: true, resizable: false, @@ -558,14 +558,23 @@ ipcMain.on('start-expansion-bar', (event, fileCount) => { }); -ipcMain.on('set-expansion-bar-progress', (event, fileNum, progress, name) => { +ipcMain.on('set-expansion-bar-progress', (event, fileNum, progress) => { + + if (expandProgressBar) { + + expandProgressBar.value = (fileNum * 100) + progress; + + } + +}); + +ipcMain.on('set-expansion-bar-file', (event, fileNum, name) => { const index = fileNum + 1; const fileCount = expandProgressBar.getOptions().maxValue / 100; if (expandProgressBar) { - expandProgressBar.value = (fileNum * 100) + progress; expandProgressBar.detail = 'Expanding ' + name + ' (' + index + ' of ' + fileCount + ').'; } @@ -691,14 +700,23 @@ ipcMain.on('start-split-bar', (event, fileCount) => { }); -ipcMain.on('set-split-bar-progress', (event, fileNum, progress, name) => { +ipcMain.on('set-split-bar-progress', (event, fileNum, progress) => { + + if (splitProgressBar) { + + splitProgressBar.value = (fileNum * 100) + progress; + + } + +}); + +ipcMain.on('set-split-bar-file', (event, fileNum, name) => { const index = fileNum + 1; const fileCount = splitProgressBar.getOptions().maxValue / 100; if (splitProgressBar) { - splitProgressBar.value = (fileNum * 100) + progress; splitProgressBar.detail = 'Splitting ' + name + ' (' + index + ' of ' + fileCount + ').'; } @@ -824,14 +842,23 @@ ipcMain.on('start-downsample-bar', (event, fileCount) => { }); -ipcMain.on('set-downsample-bar-progress', (event, fileNum, progress, name) => { +ipcMain.on('set-downsample-bar-progress', (event, fileNum, progress) => { + + if (downsampleProgressBar) { + + downsampleProgressBar.value = (fileNum * 100) + progress; + + } + +}); + +ipcMain.on('set-downsample-bar-file', (event, fileNum, 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 + ').'; } diff --git a/package.json b/package.json index 206c722..64b1c38 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "AudioMoth-Config", - "version": "1.8.1", + "version": "1.9.0", "description": "The configuration app for the AudioMoth acoustic monitoring device.", "main": "main.js", "author": "openacousticdevices.info", @@ -57,8 +57,6 @@ } }, "devDependencies": { - "electron": "8.5.2", - "electron-builder": "^22.11.7", "eslint": "^7.27.0", "eslint-config-standard": "^14.1.0", "eslint-plugin-import": "^2.22.1", @@ -67,6 +65,8 @@ "eslint-plugin-standard": "^4.0.2" }, "dependencies": { + "electron": "8.5.2", + "electron-builder": "^22.11.7", "audiomoth-hid": "^2.1.0", "audiomoth-utils": "^1.2.0", "bootstrap": "4.3.1", diff --git a/processing/downsampling.html b/processing/downsampling.html index 29cc0d9..1567e9c 100644 --- a/processing/downsampling.html +++ b/processing/downsampling.html @@ -109,104 +109,64 @@ -
- -
-
- -
-
- -
-
-
- -
-
-
-
- -
-
-
- -
-
+
+
-
-
- -
-
- -
-
- -
-
-
- Writing WAV files to source folder. -
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
FileFolder
Selection type: + + + +
-
-
-
-
- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
FileFolder
Selection type: - - - -
-
-
+
+
+
- -
-
- -
-
-
- No AudioMoth WAV files selected. -
+
+
+ No AudioMoth WAV files selected.
+
+ +
+ +
- +
@@ -215,6 +175,7 @@ diff --git a/processing/expansion.html b/processing/expansion.html index 5418327..ce7c1b7 100644 --- a/processing/expansion.html +++ b/processing/expansion.html @@ -49,14 +49,14 @@ - - - - - - - - + + + + + + + + Maximum file length: @@ -67,7 +67,7 @@ - + @@ -129,14 +129,14 @@ - - - - - - - - + + + + + + + + Maximum file length: @@ -147,7 +147,7 @@ - + @@ -195,55 +195,13 @@
-
+
-
-
- -
-
- -
-
-
- -
-
-
-
- -
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- Writing WAV files to source folder. -
-
-
- -
-
-
@@ -295,9 +253,11 @@
+
+
- +
@@ -306,6 +266,7 @@ diff --git a/processing/output.html b/processing/output.html new file mode 100644 index 0000000..597221a --- /dev/null +++ b/processing/output.html @@ -0,0 +1,56 @@ +
+
+ +
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ Enable destination folder: +
+
+ +
+
+ +
+
+ Generate subfolders in destination: +
+
+ +
+
+ +
+
+ +
+
+
+ Writing WAV files to source folder. +
+
+
+ +
+
\ No newline at end of file diff --git a/processing/split.html b/processing/split.html index c00c040..e2b818a 100644 --- a/processing/split.html +++ b/processing/split.html @@ -21,14 +21,14 @@ - - - - - - - - + + + + + + + + Maximum file length: @@ -39,7 +39,7 @@ - + @@ -57,7 +57,7 @@ - + @@ -68,52 +68,8 @@
- -
-
-
- -
-
- -
-
-
- -
-
-
-
- -
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- Writing WAV files to source folder. -
-
-
- -
-
-
@@ -165,9 +121,11 @@
+
+
- +
@@ -176,6 +134,7 @@ diff --git a/processing/uiCommon.js b/processing/uiCommon.js index 6262386..b6dce1b 100644 --- a/processing/uiCommon.js +++ b/processing/uiCommon.js @@ -7,23 +7,11 @@ /* Functions which control elements common to the expansion and split windows */ const electron = require('electron'); -const dialog = electron.remote.dialog; - -const path = require('path'); -const fs = require('fs'); const nightMode = require('../nightMode.js'); -const currentWindow = electron.remote.getCurrentWindow(); - const fileButton = document.getElementById('file-button'); -const prefixInput = document.getElementById('prefix-input'); -const prefixCheckbox = document.getElementById('prefix-checkbox'); -const prefixLabel = document.getElementById('prefix-label'); - -const outputLabel = document.getElementById('output-label'); - function getSelectedRadioValue (radioName) { return parseInt(document.querySelector('input[name="' + radioName + '"]:checked').value); @@ -78,148 +66,3 @@ exports.updateButtonText = () => { } }; - -/* Open dialog and set files to be expanded */ - -exports.selectRecordings = (fileRegex) => { - - let folderContents, filePath, fileName, recordings; - - const selectionTypes = ['openFile', 'openDirectory']; - const selectionType = getSelectedRadioValue('selection-radio'); - const properties = [selectionTypes[selectionType], 'multiSelections']; - - /* If files are being selected, allow users to selectt more than one item. Only a single folder can be selected */ - - if (selectionType === 0) { - - properties.push('multiSelections'); - - } - - /* If files are being selected, limit selection to .wav files */ - - const filters = (selectionType === 0) ? [{name: 'wav', extensions: ['wav']}] : []; - - const selection = dialog.showOpenDialogSync(currentWindow, { - title: 'Select recording file or folder containing recordings', - nameFieldLabel: 'Recordings', - properties: properties, - filters: filters - }); - - if (selection) { - - recordings = []; - - if (selectionType === 0) { - - for (let i = 0; i < selection.length; i++) { - - filePath = selection[i]; - fileName = path.basename(filePath); - - /* Check if wav files match a given regex and thus can be expanded/split */ - - if (filePath.charAt(0) !== '.' && fileRegex.test(fileName.toUpperCase())) { - - recordings.push(filePath); - - } - - } - - } else { - - for (let i = 0; i < selection.length; i++) { - - folderContents = fs.readdirSync(selection[i]); - - for (let j = 0; j < folderContents.length; j++) { - - filePath = folderContents[j]; - - if (filePath.charAt(0) !== '.' && fileRegex.test(filePath.toUpperCase())) { - - recordings.push(path.join(selection[i], filePath)); - - } - - } - - } - - } - - return recordings; - - } - -}; - -/* Remove all characters which aren't A-Z, a-z, 0-9, and _ */ - -prefixInput.addEventListener('keydown', (e) => { - - if (prefixInput.disabled) { - - e.preventDefault(); - return; - - } - - var reg = /[^A-Za-z-_0-9]{1}/g; - - if (reg.test(e.key)) { - - e.preventDefault(); - - } - -}); - -prefixInput.addEventListener('paste', (e) => { - - e.stopPropagation(); - e.preventDefault(); - - if (prefixInput.disabled) { - - return; - - } - - /* Read text from clipboard */ - - const clipboardData = e.clipboardData || window.clipboardData; - const pastedData = clipboardData.getData('Text'); - - /* Perform paste, but remove all unsupported characters */ - - prefixInput.value += pastedData.replace(/[^A-Za-z_0-9]{1}/g, ''); - - /* Limit max number of characters */ - - prefixInput.value = prefixInput.value.substring(0, prefixInput.maxLength); - -}); - -/* Add listener to handle enabling/disabling prefix UI */ - -prefixCheckbox.addEventListener('change', () => { - - if (prefixCheckbox.checked) { - - prefixLabel.classList.remove('grey'); - prefixInput.classList.remove('grey'); - prefixInput.disabled = false; - - } else { - - prefixLabel.classList.add('grey'); - prefixInput.classList.add('grey'); - prefixInput.disabled = true; - - } - -}); diff --git a/processing/uiDownsampling.js b/processing/uiDownsampling.js index 55a4744..0ec3748 100644 --- a/processing/uiDownsampling.js +++ b/processing/uiDownsampling.js @@ -44,7 +44,7 @@ const downsampleButton = document.getElementById('downsample-button'); var files = []; var downsampling = false; -const DEFAULT_SLEEP_AMOUNT = 3000; +const DEFAULT_SLEEP_AMOUNT = 2000; /* Disable UI elements in main window while progress bar is open and downsample is in progress */ @@ -91,8 +91,6 @@ function enableUI () { } -/* Split selected files */ - function downsampleFiles () { if (!files) { @@ -111,6 +109,11 @@ function downsampleFiles () { let sleepAmount = DEFAULT_SLEEP_AMOUNT; let successesWithoutError = 0; + const unwrittenErrors = []; + let lastErrorWrite = -1; + + let errorFileStream; + 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 */ @@ -127,7 +130,7 @@ function downsampleFiles () { /* 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])); + electron.ipcRenderer.send('set-downsample-bar-progress', i, 0); const sampleRate = SAMPLE_RATES[ui.getSelectedRadioValue('sample-rate-radio')]; @@ -138,12 +141,36 @@ function downsampleFiles () { /* 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; + let outputPath = null; + + if (uiOutput.isCustomDestinationEnabled()) { + + outputPath = uiOutput.getOutputDir(); + + if (uiOutput.isCreateSubdirectoriesEnabled() && selectionRadios[1].checked) { + + const dirnames = path.dirname(files[i]).replace(/\\/g, '/').split('/'); + + const folderName = dirnames[dirnames.length - 1]; + + outputPath = path.join(outputPath, folderName); + + if (!fs.existsSync(outputPath)) { + + fs.mkdirSync(outputPath); + + } + + } + + } + 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])); + electron.ipcRenderer.send('set-downsample-bar-progress', i, progress); + electron.ipcRenderer.send('set-downsample-bar-file', i, path.basename(files[i])); }); @@ -162,6 +189,7 @@ function downsampleFiles () { /* Add error to log file */ + unwrittenErrors.push(errorCount); successesWithoutError = 0; errorCount++; errors.push(response.error); @@ -171,31 +199,47 @@ function downsampleFiles () { if (errorCount === 1) { - const errorFileLocation = uiOutput.isChecked() ? uiOutput.getOutputDir() : path.dirname(errorFiles[0]); - + const errorFileLocation = uiOutput.isCustomDestinationEnabled() ? uiOutput.getOutputDir() : path.dirname(errorFiles[0]); errorFilePath = path.join(errorFileLocation, 'ERRORS.TXT'); + errorFileStream = fs.createWriteStream(errorFilePath, {flags: 'a'}); + + errorFileStream.write('-- Downsample --\n'); } - let fileContent = ''; + const currentTime = new Date(); + const timeSinceLastErrorWrite = currentTime - lastErrorWrite; - for (let j = 0; j < errorCount; j++) { + if (timeSinceLastErrorWrite > 1000 || lastErrorWrite === -1) { - fileContent += path.basename(errorFiles[j]) + ' - ' + errors[j] + '\n'; + lastErrorWrite = new Date(); - } + const unwrittenErrorCount = unwrittenErrors.length; - try { + console.log('Writing', unwrittenErrorCount, 'errors'); - fs.writeFileSync(errorFilePath, fileContent); + let fileContent = ''; - console.log('Error summary written to ' + errorFilePath); + for (let e = 0; e < unwrittenErrorCount; e++) { - } catch (err) { + const unwrittenErrorIndex = unwrittenErrors.pop(); + fileContent += path.basename(errorFiles[unwrittenErrorIndex]) + ' - ' + errors[unwrittenErrorIndex] + '\n'; - console.error(err); - electron.ipcRenderer.send('set-downsample-bar-completed', successCount, errorCount, true); - return; + } + + try { + + errorFileStream.write(fileContent); + + console.log('Error summary written to ' + errorFilePath); + + } catch (err) { + + console.error(err); + electron.ipcRenderer.send('set-downsample-bar-completed', successCount, errorCount, true); + return; + + } } @@ -206,6 +250,41 @@ function downsampleFiles () { } + /* If any errors occurred, do a final error write */ + + const unwrittenErrorCount = unwrittenErrors.length; + + if (unwrittenErrorCount > 0) { + + console.log('Writing remaining', unwrittenErrorCount, 'errors'); + + let fileContent = ''; + + for (let e = 0; e < unwrittenErrorCount; e++) { + + const unwrittenErrorIndex = unwrittenErrors.pop(); + fileContent += path.basename(errorFiles[unwrittenErrorIndex]) + ' - ' + errors[unwrittenErrorIndex] + '\n'; + + } + + try { + + errorFileStream.write(fileContent); + + console.log('Error summary written to ' + errorFilePath); + + errorFileStream.end(); + + } catch (err) { + + console.error(err); + electron.ipcRenderer.send('set-downsample-bar-completed', successCount, errorCount, true); + return; + + } + + } + /* Notify main thread that split is complete so progress bar is closed */ electron.ipcRenderer.send('set-downsample-bar-completed', successCount, errorCount, false); @@ -260,7 +339,7 @@ selectionRadios[1].addEventListener('change', resetUI); fileButton.addEventListener('click', () => { - files = ui.selectRecordings(FILE_REGEX); + files = uiOutput.selectRecordings(FILE_REGEX); updateInputDirectoryDisplay(files); @@ -276,7 +355,7 @@ downsampleButton.addEventListener('click', () => { } - if ((!prefixCheckbox.checked || prefixInput.value === '') && (!uiOutput.isChecked() || uiOutput.getOutputDir() === '')) { + if ((!prefixCheckbox.checked || prefixInput.value === '') && (!uiOutput.isCustomDestinationEnabled() || uiOutput.getOutputDir() === '')) { dialog.showMessageBox(currentWindow, { type: 'error', @@ -288,6 +367,50 @@ downsampleButton.addEventListener('click', () => { } + /* Check if output location is the same as input */ + + for (let i = 0; i < files.length; i++) { + + if (!prefixCheckbox.checked || prefixInput.value === '') { + + /* If folder mode is enabled, the input folder is the same as the output and subdirectories are enabled, files will be overwritten as the paths will match */ + + if (selectionRadios[1].checked && uiOutput.isCustomDestinationEnabled() && uiOutput.isCreateSubdirectoriesEnabled()) { + + /* Get the parent folder of the selected file and compare that to the output directory */ + + const fileFolderPath = path.dirname(path.dirname(files[i])); + + if (uiOutput.getOutputDir() === fileFolderPath) { + + dialog.showMessageBox(currentWindow, { + type: 'error', + title: 'Cannot downsample with current settings', + message: 'Output destination is the same as input destination and no prefix is selected.' + }); + + return; + + } + + } + + if (uiOutput.getOutputDir() === path.dirname(files[i])) { + + dialog.showMessageBox(currentWindow, { + type: 'error', + title: 'Cannot downsample with current settings', + message: 'Output destination is the same as input destination and no prefix is selected.' + }); + + return; + + } + + } + + } + downsampling = true; disableUI(); diff --git a/processing/uiExpansion.js b/processing/uiExpansion.js index 1a753d8..74be30b 100644 --- a/processing/uiExpansion.js +++ b/processing/uiExpansion.js @@ -9,6 +9,7 @@ /* 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'); @@ -19,6 +20,8 @@ const fs = require('fs'); const audiomothUtils = require('audiomoth-utils'); +var currentWindow = electron.remote.getCurrentWindow(); + 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']; @@ -52,7 +55,7 @@ var expanding = false; var expansionType = 'DURATION'; -const DEFAULT_SLEEP_AMOUNT = 3000; +const DEFAULT_SLEEP_AMOUNT = 2000; /* Disable UI elements in main window while progress bar is open and expansion is in progress */ @@ -187,6 +190,11 @@ function expandFiles () { let sleepAmount = DEFAULT_SLEEP_AMOUNT; let successesWithoutError = 0; + const unwrittenErrors = []; + let lastErrorWrite = -1; + + let errorFileStream; + for (let i = 0; i < files.length; i++) { /* If progress bar is closed, the expansion task is considered cancelled. This will contact the main thread and ask if that has happened */ @@ -203,7 +211,7 @@ function expandFiles () { /* Let the main thread know what value to set the progress bar to */ - electron.ipcRenderer.send('set-expansion-bar-progress', i, 0, path.basename(files[i])); + electron.ipcRenderer.send('set-expansion-bar-progress', i, 0); /* If max length is enabled for the current expansion mode (there is separate UI for each and only the relevant one is shown) */ @@ -235,12 +243,36 @@ 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 = uiOutput.isChecked() ? uiOutput.getOutputDir() : null; + let outputPath = null; + + if (uiOutput.isCustomDestinationEnabled()) { + + outputPath = uiOutput.getOutputDir(); + + if (uiOutput.isCreateSubdirectoriesEnabled() && selectionRadios[1].checked) { + + const dirnames = path.dirname(files[i]).replace(/\\/g, '/').split('/'); + + const folderName = dirnames[dirnames.length - 1]; + + outputPath = path.join(outputPath, folderName); + + if (!fs.existsSync(outputPath)) { + + fs.mkdirSync(outputPath); + + } + + } + + } + const prefix = (prefixCheckbox.checked && prefixInput.value !== '') ? prefixInput.value : null; const response = audiomothUtils.expand(files[i], outputPath, prefix, expansionType, maxLength, generateSilentFiles, alignToSecondTransitions, (progress) => { - electron.ipcRenderer.send('set-expansion-bar-progress', i, progress, path.basename(files[i])); + electron.ipcRenderer.send('set-expansion-bar-progress', i, progress); + electron.ipcRenderer.send('set-expansion-bar-file', i, path.basename(files[i])); }); @@ -259,6 +291,7 @@ function expandFiles () { /* Add error to log file */ + unwrittenErrors.push(errorCount); successesWithoutError = 0; errorCount++; errors.push(response.error); @@ -268,31 +301,47 @@ function expandFiles () { if (errorCount === 1) { - const errorFileLocation = uiOutput.isChecked() ? uiOutput.getOutputDir() : path.dirname(errorFiles[0]); - + const errorFileLocation = uiOutput.isCustomDestinationEnabled() ? uiOutput.getOutputDir() : path.dirname(errorFiles[0]); errorFilePath = path.join(errorFileLocation, 'ERRORS.TXT'); + errorFileStream = fs.createWriteStream(errorFilePath, {flags: 'a'}); + + errorFileStream.write('-- Expansion --\n'); } - let fileContent = ''; + const currentTime = new Date(); + const timeSinceLastErrorWrite = currentTime - lastErrorWrite; - for (let j = 0; j < errorCount; j++) { + if (timeSinceLastErrorWrite > 1000 || lastErrorWrite === -1) { - fileContent += path.basename(errorFiles[j]) + ' - ' + errors[j] + '\n'; + lastErrorWrite = new Date(); - } + const unwrittenErrorCount = unwrittenErrors.length; - try { + console.log('Writing', unwrittenErrorCount, 'errors'); - fs.writeFileSync(errorFilePath, fileContent); + let fileContent = ''; - console.log('Error summary written to ' + errorFilePath); + for (let e = 0; e < unwrittenErrorCount; e++) { - } catch (err) { + const unwrittenErrorIndex = unwrittenErrors.pop(); + fileContent += path.basename(errorFiles[unwrittenErrorIndex]) + ' - ' + errors[unwrittenErrorIndex] + '\n'; - console.error(err); - electron.ipcRenderer.send('set-expansion-bar-completed', successCount, errorCount, true); - return; + } + + try { + + errorFileStream.write(fileContent); + + console.log('Error summary written to ' + errorFilePath); + + } catch (err) { + + console.error(err); + electron.ipcRenderer.send('set-expansion-bar-completed', successCount, errorCount, true); + return; + + } } @@ -303,6 +352,41 @@ function expandFiles () { } + /* If any errors occurred, do a final error write */ + + const unwrittenErrorCount = unwrittenErrors.length; + + if (unwrittenErrorCount > 0) { + + console.log('Writing remaining', unwrittenErrorCount, 'errors'); + + let fileContent = ''; + + for (let e = 0; e < unwrittenErrorCount; e++) { + + const unwrittenErrorIndex = unwrittenErrors.pop(); + fileContent += path.basename(errorFiles[unwrittenErrorIndex]) + ' - ' + errors[unwrittenErrorIndex] + '\n'; + + } + + try { + + errorFileStream.write(fileContent); + + console.log('Error summary written to ' + errorFilePath); + + errorFileStream.end(); + + } catch (err) { + + console.error(err); + electron.ipcRenderer.send('set-expansion-bar-completed', successCount, errorCount, true); + return; + + } + + } + /* Notify main thread that expansion is complete so progress bar is closed */ electron.ipcRenderer.send('set-expansion-bar-completed', successCount, errorCount, false); @@ -493,7 +577,7 @@ selectionRadios[1].addEventListener('change', resetUI); fileButton.addEventListener('click', () => { - files = ui.selectRecordings(FILE_REGEX); + files = uiOutput.selectRecordings(FILE_REGEX); updateInputDirectoryDisplay(files); @@ -509,6 +593,50 @@ expandButton.addEventListener('click', () => { } + /* Check if output location is the same as input */ + + for (let i = 0; i < files.length; i++) { + + if (!prefixCheckbox.checked || prefixInput.value === '') { + + /* If folder mode is enabled, the input folder is the same as the output and subdirectories are enabled, files will be overwritten as the paths will match */ + + if (selectionRadios[1].checked && uiOutput.isCustomDestinationEnabled() && uiOutput.isCreateSubdirectoriesEnabled()) { + + /* Get the parent folder of the selected file and compare that to the output directory */ + + const fileFolderPath = path.dirname(path.dirname(files[i])); + + if (uiOutput.getOutputDir() === fileFolderPath) { + + dialog.showMessageBox(currentWindow, { + type: 'error', + title: 'Cannot downsample with current settings', + message: 'Output destination is the same as input destination and no prefix is selected.' + }); + + return; + + } + + } + + if (uiOutput.getOutputDir() === path.dirname(files[i])) { + + dialog.showMessageBox(currentWindow, { + type: 'error', + title: 'Cannot downsample with current settings', + message: 'Output destination is the same as input destination and no prefix is selected.' + }); + + return; + + } + + } + + } + expanding = true; disableUI(); diff --git a/processing/uiOutput.js b/processing/uiOutput.js index 0e91562..2b17183 100644 --- a/processing/uiOutput.js +++ b/processing/uiOutput.js @@ -10,47 +10,76 @@ const electron = require('electron'); const dialog = electron.remote.dialog; +const currentWindow = electron.remote.getCurrentWindow(); + +const path = require('path'); +const fs = require('fs'); const outputCheckbox = document.getElementById('output-checkbox'); const outputButton = document.getElementById('output-button'); const outputLabel = document.getElementById('output-label'); +const subdirectoriesLabel = document.getElementById('subdirectories-label'); +const subdirectoriesCheckbox = document.getElementById('subdirectories-checkbox'); + var outputDir = ''; +const prefixInput = document.getElementById('prefix-input'); +const prefixCheckbox = document.getElementById('prefix-checkbox'); +const prefixLabel = document.getElementById('prefix-label'); + +const selectionRadios = document.getElementsByName('selection-radio'); + +function getSelectedRadioValue (radioName) { + + return parseInt(document.querySelector('input[name="' + radioName + '"]:checked').value); + +} + /* Update label to notify user if a custom output directtory is being used */ -function updateOutputLabel (outputDir) { +function updateOutputLabel () { - if (outputDir === '') { + if (outputDir === '' || !outputCheckbox.checked) { outputLabel.textContent = 'Writing WAV files to source folder.'; } else { - outputLabel.textContent = 'Writing WAV files to custom folder.'; + outputLabel.textContent = 'Writing WAV files to destination folder.'; } }; -/* Add listener which handles enabling/disabling custom output directory UI */ +function updateSubdirectoriesCheckbox () { -outputCheckbox.addEventListener('change', () => { + const selectionType = getSelectedRadioValue('selection-radio'); - if (outputCheckbox.checked) { + if (outputCheckbox.checked && outputDir !== '' && selectionType === 1) { - outputLabel.classList.remove('grey'); - outputButton.disabled = false; + subdirectoriesCheckbox.disabled = false; + subdirectoriesLabel.classList.remove('grey'); } else { - outputLabel.classList.add('grey'); - outputButton.disabled = true; - outputDir = ''; - updateOutputLabel(outputDir); + subdirectoriesCheckbox.disabled = true; + subdirectoriesLabel.classList.add('grey'); } +} + +/* Add listener which handles enabling/disabling custom output directory UI */ + +outputCheckbox.addEventListener('change', () => { + + updateOutputLabel(); + + updateSubdirectoriesCheckbox(); + + outputButton.disabled = !outputCheckbox.checked; + }); /* Select a custom output directory. If Cancel is pressed, assume no custom direcotry is wantted */ @@ -66,7 +95,154 @@ outputButton.addEventListener('click', () => { outputDir = (destinationName !== undefined) ? destinationName[0] : ''; - updateOutputLabel(outputDir); + updateOutputLabel(); + + updateSubdirectoriesCheckbox(); + +}); + +/* Open dialog and set files to be expanded */ + +exports.selectRecordings = (fileRegex) => { + + let folderContents, filePath, fileName, recordings; + + const selectionTypes = ['openFile', 'openDirectory']; + const selectionType = getSelectedRadioValue('selection-radio'); + const properties = [selectionTypes[selectionType], 'multiSelections']; + + /* If files are being selected, allow users to selectt more than one item. Only a single folder can be selected */ + + if (selectionType === 0) { + + properties.push('multiSelections'); + + } + + /* If files are being selected, limit selection to .wav files */ + + const filters = (selectionType === 0) ? [{name: 'wav', extensions: ['wav']}] : []; + + const selection = dialog.showOpenDialogSync(currentWindow, { + title: 'Select recording file or folder containing recordings', + nameFieldLabel: 'Recordings', + properties: properties, + filters: filters + }); + + if (selection) { + + recordings = []; + + if (selectionType === 0) { + + for (let i = 0; i < selection.length; i++) { + + filePath = selection[i]; + fileName = path.basename(filePath); + + /* Check if wav files match a given regex and thus can be expanded/split */ + + if (filePath.charAt(0) !== '.' && fileRegex.test(fileName.toUpperCase())) { + + recordings.push(filePath); + + } + + } + + } else { + + for (let i = 0; i < selection.length; i++) { + + folderContents = fs.readdirSync(selection[i]); + + for (let j = 0; j < folderContents.length; j++) { + + filePath = folderContents[j]; + + if (filePath.charAt(0) !== '.' && fileRegex.test(filePath.toUpperCase())) { + + recordings.push(path.join(selection[i], filePath)); + + } + + } + + } + + } + + return recordings; + + } + +}; + +/* Remove all characters which aren't A-Z, a-z, 0-9, and _ */ + +prefixInput.addEventListener('keydown', (e) => { + + if (prefixInput.disabled) { + + e.preventDefault(); + return; + + } + + var reg = /[^A-Za-z-_0-9]{1}/g; + + if (reg.test(e.key)) { + + e.preventDefault(); + + } + +}); + +prefixInput.addEventListener('paste', (e) => { + + e.stopPropagation(); + e.preventDefault(); + + if (prefixInput.disabled) { + + return; + + } + + /* Read text from clipboard */ + + const clipboardData = e.clipboardData || window.clipboardData; + const pastedData = clipboardData.getData('Text'); + + /* Perform paste, but remove all unsupported characters */ + + prefixInput.value += pastedData.replace(/[^A-Za-z_0-9]{1}/g, ''); + + /* Limit max number of characters */ + + prefixInput.value = prefixInput.value.substring(0, prefixInput.maxLength); + +}); + +/* Add listener to handle enabling/disabling prefix UI */ + +prefixCheckbox.addEventListener('change', () => { + + if (prefixCheckbox.checked) { + + prefixLabel.classList.remove('grey'); + prefixInput.classList.remove('grey'); + prefixInput.disabled = false; + + } else { + + prefixLabel.classList.add('grey'); + prefixInput.classList.add('grey'); + prefixInput.disabled = true; + + } }); @@ -94,7 +270,7 @@ exports.enableOutputButton = () => { }; -exports.isChecked = () => { +exports.isCustomDestinationEnabled = () => { return outputCheckbox.checked; @@ -105,3 +281,12 @@ exports.getOutputDir = () => { return outputDir; }; + +exports.isCreateSubdirectoriesEnabled = () => { + + return subdirectoriesCheckbox.checked; + +}; + +selectionRadios[0].addEventListener('change', updateSubdirectoriesCheckbox); +selectionRadios[1].addEventListener('change', updateSubdirectoriesCheckbox); diff --git a/processing/uiSplit.js b/processing/uiSplit.js index de17986..74aad04 100644 --- a/processing/uiSplit.js +++ b/processing/uiSplit.js @@ -40,7 +40,7 @@ const splitButton = document.getElementById('split-button'); var files = []; var splitting = false; -const DEFAULT_SLEEP_AMOUNT = 3000; +const DEFAULT_SLEEP_AMOUNT = 2000; /* Disable UI elements in main window while progress bar is open and split is in progress */ @@ -113,6 +113,11 @@ function splitFiles () { let sleepAmount = DEFAULT_SLEEP_AMOUNT; let successesWithoutError = 0; + const unwrittenErrors = []; + let lastErrorWrite = -1; + + let errorFileStream; + for (let i = 0; i < files.length; i++) { /* If progress bar is closed, the split task is considered cancelled. This will contact the main thread and ask if that has happened */ @@ -129,7 +134,7 @@ function splitFiles () { /* Let the main thread know what value to set the progress bar to */ - electron.ipcRenderer.send('set-split-bar-progress', i, 0, path.basename(files[i])); + electron.ipcRenderer.send('set-split-bar-progress', i, 0); const maxLength = MAX_LENGTHS[ui.getSelectedRadioValue('max-length-radio')]; @@ -140,12 +145,36 @@ 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 = uiOutput.isChecked() ? uiOutput.getOutputDir() : null; + let outputPath = null; + + if (uiOutput.isCustomDestinationEnabled()) { + + outputPath = uiOutput.getOutputDir(); + + if (uiOutput.isCreateSubdirectoriesEnabled() && selectionRadios[1].checked) { + + const dirnames = path.dirname(files[i]).replace(/\\/g, '/').split('/'); + + const folderName = dirnames[dirnames.length - 1]; + + outputPath = path.join(outputPath, folderName); + + if (!fs.existsSync(outputPath)) { + + fs.mkdirSync(outputPath); + + } + + } + + } + const prefix = (prefixCheckbox.checked && prefixInput.value !== '') ? prefixInput.value : null; const response = audiomothUtils.split(files[i], outputPath, prefix, maxLength, (progress) => { - electron.ipcRenderer.send('set-split-bar-progress', i, progress, path.basename(files[i])); + electron.ipcRenderer.send('set-split-bar-progress', i, progress); + electron.ipcRenderer.send('set-split-bar-file', i, path.basename(files[i])); }); @@ -164,6 +193,7 @@ function splitFiles () { /* Add error to log file */ + unwrittenErrors.push(errorCount); successesWithoutError = 0; errorCount++; errors.push(response.error); @@ -173,31 +203,47 @@ function splitFiles () { if (errorCount === 1) { - const errorFileLocation = uiOutput.isChecked() ? uiOutput.getOutputDir() : path.dirname(errorFiles[0]); - + const errorFileLocation = uiOutput.isCustomDestinationEnabled() ? uiOutput.getOutputDir() : path.dirname(errorFiles[0]); errorFilePath = path.join(errorFileLocation, 'ERRORS.TXT'); + errorFileStream = fs.createWriteStream(errorFilePath, {flags: 'a'}); + + errorFileStream.write('-- Split --\n'); } - let fileContent = ''; + const currentTime = new Date(); + const timeSinceLastErrorWrite = currentTime - lastErrorWrite; - for (let j = 0; j < errorCount; j++) { + if (timeSinceLastErrorWrite > 1000 || lastErrorWrite === -1) { - fileContent += path.basename(errorFiles[j]) + ' - ' + errors[j] + '\n'; + lastErrorWrite = new Date(); - } + const unwrittenErrorCount = unwrittenErrors.length; - try { + console.log('Writing', unwrittenErrorCount, 'errors'); - fs.writeFileSync(errorFilePath, fileContent); + let fileContent = ''; - console.log('Error summary written to ' + errorFilePath); + for (let e = 0; e < unwrittenErrorCount; e++) { - } catch (err) { + const unwrittenErrorIndex = unwrittenErrors.pop(); + fileContent += path.basename(errorFiles[unwrittenErrorIndex]) + ' - ' + errors[unwrittenErrorIndex] + '\n'; - console.error(err); - electron.ipcRenderer.send('set-split-bar-completed', successCount, errorCount, true); - return; + } + + try { + + errorFileStream.write(fileContent); + + console.log('Error summary written to ' + errorFilePath); + + } catch (err) { + + console.error(err); + electron.ipcRenderer.send('set-split-bar-completed', successCount, errorCount, true); + return; + + } } @@ -208,6 +254,41 @@ function splitFiles () { } + /* If any errors occurred, do a final error write */ + + const unwrittenErrorCount = unwrittenErrors.length; + + if (unwrittenErrorCount > 0) { + + console.log('Writing remaining', unwrittenErrorCount, 'errors'); + + let fileContent = ''; + + for (let e = 0; e < unwrittenErrorCount; e++) { + + const unwrittenErrorIndex = unwrittenErrors.pop(); + fileContent += path.basename(errorFiles[unwrittenErrorIndex]) + ' - ' + errors[unwrittenErrorIndex] + '\n'; + + } + + try { + + errorFileStream.write(fileContent); + + console.log('Error summary written to ' + errorFilePath); + + errorFileStream.end(); + + } catch (err) { + + console.error(err); + electron.ipcRenderer.send('set-split-bar-completed', successCount, errorCount, true); + return; + + } + + } + /* Notify main thread that split is complete so progress bar is closed */ electron.ipcRenderer.send('set-split-bar-completed', successCount, errorCount, false); @@ -262,7 +343,7 @@ selectionRadios[1].addEventListener('change', resetUI); fileButton.addEventListener('click', () => { - files = ui.selectRecordings(FILE_REGEX); + files = uiOutput.selectRecordings(FILE_REGEX); updateInputDirectoryDisplay(files); @@ -278,7 +359,7 @@ splitButton.addEventListener('click', () => { } - if ((!prefixCheckbox.checked || prefixInput.value === '') && (!uiOutput.isChecked() || uiOutput.getOutputDir() === '')) { + if ((!prefixCheckbox.checked || prefixInput.value === '') && (!uiOutput.isCustomDestinationEnabled() || uiOutput.getOutputDir() === '')) { dialog.showMessageBox(currentWindow, { type: 'error', @@ -290,6 +371,50 @@ splitButton.addEventListener('click', () => { } + /* Check if output location is the same as input */ + + for (let i = 0; i < files.length; i++) { + + if (!prefixCheckbox.checked || prefixInput.value === '') { + + /* If folder mode is enabled, the input folder is the same as the output and subdirectories are enabled, files will be overwritten as the paths will match */ + + if (selectionRadios[1].checked && uiOutput.isCustomDestinationEnabled() && uiOutput.isCreateSubdirectoriesEnabled()) { + + /* Get the parent folder of the selected file and compare that to the output directory */ + + const fileFolderPath = path.dirname(path.dirname(files[i])); + + if (uiOutput.getOutputDir() === fileFolderPath) { + + dialog.showMessageBox(currentWindow, { + type: 'error', + title: 'Cannot downsample with current settings', + message: 'Output destination is the same as input destination and no prefix is selected.' + }); + + return; + + } + + } + + if (uiOutput.getOutputDir() === path.dirname(files[i])) { + + dialog.showMessageBox(currentWindow, { + type: 'error', + title: 'Cannot downsample with current settings', + message: 'Output destination is the same as input destination and no prefix is selected.' + }); + + return; + + } + + } + + } + splitting = true; disableUI(); diff --git a/settings/advanced.html b/settings/advanced.html index e65fd74..5c4c8d8 100644 --- a/settings/advanced.html +++ b/settings/advanced.html @@ -43,7 +43,7 @@
Enable energy saver mode:
-
+
@@ -52,7 +52,7 @@
Enable low gain range:
-
+
diff --git a/settings/filtering.html b/settings/filtering.html index bd8d35d..4c93cfa 100644 --- a/settings/filtering.html +++ b/settings/filtering.html @@ -54,7 +54,7 @@
- +
@@ -164,7 +164,7 @@
- + @@ -175,7 +175,7 @@ - +
0 130 60
Minimum trigger duration (s): @@ -245,22 +245,22 @@ - + - + - + - + - + - + diff --git a/settings/settings.html b/settings/settings.html index 089fbf4..c248595 100644 --- a/settings/settings.html +++ b/settings/settings.html @@ -118,7 +118,7 @@
Enable LED:
-
+