Skip to content

Commit

Permalink
Moved settings to the right, and the settings nav button to the nav
Browse files Browse the repository at this point in the history
some context-menu actions moved to the main click handler (fixes play)
updated file lists with proxy file if file not found
adjustments for encoder padding to ensure selection analyses have the correct results
BEXT/Guano metadata persists after files saved to library.
Mattk70 committed Nov 12, 2024
1 parent 928db8c commit e6a407b
Showing 3 changed files with 102 additions and 103 deletions.
31 changes: 7 additions & 24 deletions index.html
Original file line number Diff line number Diff line change
@@ -354,25 +354,7 @@ <h5 class="modal-title" id="locationModalLabel">Set Location</h5>
</div>
</div>
<!-- context menu-->
<div class="dropdown-menu dropdown-menu-sm" id="context-menu">
<a class="dropdown-item play"><span class="material-symbols-outlined">play_circle</span> Play</a>
<a class="dropdown-item" href="#" id="context-analyse-selection"><span class="material-symbols-outlined">
search
</span> Analyse</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" id="create-manual-record" href="#"><span class="material-symbols-outlined">
edit_document
</span> Edit Record</a>
<a class="dropdown-item" id="context-create-clip" href="#"><span class="material-symbols-outlined">
music_note
</span> Export Audio Clip</a>
<a class="dropdown-item d-none" id="context-xc" href="#" target="xc">
<img src="img/logo/XC.png" alt="" style="filter:grayscale(100%);height: 1.5em"> Explore on
Xeno-Canto</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item d-none" id="context-delete" href="#">
<span class="delete material-symbols-outlined">delete_forever</span> Delete Record</a>
</div>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu"> </div>
<!-- End context menu -->
<!-- Records container -->
<div class="d-none h-100 overflow-auto" id="recordsContainer">
@@ -514,7 +496,7 @@ <h5 class="modal-title" id="locationModalLabel">Set Location</h5>
</div>
</fieldset>
</div>
<div class="offcanvas offcanvas-start text-bg-dark" tabindex="-1" id="settings" aria-labelledby="offcanvasExampleLabel">
<div class="offcanvas offcanvas-end text-bg-dark" tabindex="-1" id="settings" aria-labelledby="offcanvasExampleLabel">
<div class="offcanvas-header pb-2">
<h5 class="offcanvas-title" id="offcanvasExampleLabel">Settings</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" id="close-settings" aria-label="Close"></button>
@@ -1301,12 +1283,13 @@ <h6 class="fs-6 ps-3">FAQs</h6>
</li>
</ul>
</li>
<div class="nav-item">
<li class="nav-link" onclick="placeMap('settingsMap')" id="navbarSettings"
data-bs-toggle="offcanvas" data-bs-target="#settings" aria-controls="settings">Settings <span class="material-symbols-outlined fs-5 align-middle">settings</span></li>
</div>
</ul>
</div>
<div class="container-fluid">
<button class="btn btn-outline-secondary border-dark float-end text-nowrap" style="--bs-btn-padding-y: .25rem;" type="button" onclick="placeMap('settingsMap')" id="navbarSettings"
data-bs-toggle="offcanvas" data-bs-target="#settings" aria-controls="settings">Settings <span class="material-symbols-outlined fs-5 align-middle">settings</span></button>
</div>

<!-- navbar-collapse.// -->
<a href="https://chirpity.mattkirkland.co.uk" title="Visit the Chirpity website" target="_blank"><img id="primaryLogo" src="img/logo/chirpity_logo2.png" alt="Chirpity bird calls: Identifying birds by sound"></a>
</div>
79 changes: 40 additions & 39 deletions js/ui.js
Original file line number Diff line number Diff line change
@@ -869,7 +869,7 @@ async function setCustomLocation() {
STATE.openFiles.length > 1 ? batchWrapper.classList.remove('d-none') : batchWrapper.classList.add('d-none');
// Use the current file location for lat, lon, place or use defaults
showLocation(false);
savedLocationSelect.addEventListener('change', function (e) {
savedLocationSelect.addEventListener('change', function () {
showLocation(true);
})
const addOrDelete = () => {
@@ -1344,7 +1344,7 @@ async function resultClick(e) {
return
}

const [file, start, end, sname, label] = row.getAttribute('name').split('|');
const [file, start, end, _, label] = row.getAttribute('name').split('|');
// if (row.classList.contains('table-active')){
// createRegion(start - bufferBegin, end - bufferBegin, label, true);
// e.target.classList.contains('circle') && getSelectionResults(true);
@@ -1852,7 +1852,7 @@ window.onload = async () => {
myAllowList.td = [];
myAllowList.th = [];
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]')
const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl, {allowList: myAllowList}))
const _ = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl, {allowList: myAllowList}))


// check for new version on mac platform. pkg containers are not an auto-updatable target
@@ -2054,7 +2054,7 @@ function generateBirdOptionList({ store, rows, selected }) {
// International language sorting, recommended for large arrays - 'en_uk' not valid, but same as 'en'
sortedList.sort(new Intl.Collator(config[config.model].locale.replace('_uk', '')).compare);
// Check if we have prepared this before
const all = document.getElementById('allSpecies');

const lastSelectedSpecies = selected || STATE.birdList.lastSelectedSpecies;
listHTML += '<div class="form-floating"><select spellcheck="false" id="bird-list-all" class="input form-select mb-3" aria-label=".form-select" required>';
listHTML += '<option value="">All</option>';
@@ -2423,7 +2423,7 @@ function onChartData(args) {
padding: {
top: 2
},
formatter: function (value, context) {
formatter: function (value, _) {
return value; // Customize the displayed value as needed
}
}
@@ -2769,7 +2769,7 @@ function centreSpec(){
z: function (e) {
if (( e.ctrlKey || e.metaKey) && DELETE_HISTORY.length) insertManualRecord(...DELETE_HISTORY.pop());
},
Escape: function (e) {
Escape: function () {
if (PREDICTING) {
console.log('Operation aborted');
PREDICTING = false;
@@ -2853,8 +2853,8 @@ function centreSpec(){
'=': function (e) {e.metaKey || e.ctrlKey ? reduceFFT() : zoomSpec('zoomIn')},
'+': function (e) {e.metaKey || e.ctrlKey ? reduceFFT() : zoomSpec('zoomIn')},
'-': function (e) {e.metaKey || e.ctrlKey ? increaseFFT() : zoomSpec('zoomOut')},
'F5': function (e) { reduceFFT() },
'F4': function (e) { increaseFFT() },
'F5': function () { reduceFFT() },
'F4': function () { increaseFFT() },
' ': function () { wavesurfer && wavesurfer.playPause() },
Tab: function (e) {
if ((e.metaKey || e.ctrlKey) && ! PREDICTING) { // If you did this when predicting, your results would go straight to the archive
@@ -4128,12 +4128,12 @@ function formatDuration(seconds){
//element.innerHTML = savedContent;
}
})
picker.on('show', (e) =>{
picker.on('show', () =>{
picker.setStartTime('12:00')
picker.setEndTime('12:00')

})
picker.on('hide', (e) =>{
picker.on('hide', () =>{
const id = STATE.mode === 'chart' ? 'chartRange' : 'exploreRange';
const element = document.getElementById(id);
if (! element.textContent){
@@ -4399,7 +4399,8 @@ function playRegion(){


document.addEventListener('click', function (e) {
const target = e.target.closest('[id]')?.id;
const element = e.target;
const target = element.closest('[id]')?.id;
switch (target)
{
// File menu
@@ -4477,6 +4478,14 @@ function playRegion(){
}).catch(error => console.warn(error)) ;
break;
}

// Context-menu
case 'play-region': { playRegion(); break }
case 'context-analyse-selection': {getSelectionResults(); break }
case 'context-create-clip': {
element.closest('#inSummary') ? batchExportAudio() : exportAudio();
break;
}
// --- Backends
case 'tensorflow':
case 'webgl':
@@ -4556,10 +4565,6 @@ function playRegion(){
break
}
case 'speciesFilter': { speciesFilter(e); break}
case 'context-menu': {
e.target.closest('.play') && typeof region !== 'undefined' ? playRegion() : console.log('Region undefined')
break;
}
case 'audioFiltersIcon': { toggleFilters(); break }
case 'context-mode': { toggleContextAwareMode(); break }
case 'frequency-range': {
@@ -4929,41 +4934,37 @@ async function readLabels(labelFile, updating){
const createOrEdit = ((region?.attributes.label || target.closest('#summary'))) ? 'Edit' : 'Create';

contextMenu.innerHTML = `
<a class="dropdown-item play ${hideInSummary}"><span class='material-symbols-outlined'>play_circle</span> Play</a>
<a class="dropdown-item ${hideInSummary} ${hideInSelection}" href="#" id="context-analyse-selection">
<span class="material-symbols-outlined">search</span> Analyse
</a>
<div class="dropdown-divider ${hideInSummary}"></div>
<a class="dropdown-item" id="create-manual-record" href="#">
<span class="material-symbols-outlined">edit_document</span> ${createOrEdit} Record${plural}
</a>
<a class="dropdown-item" id="context-create-clip" href="#">
<span class="material-symbols-outlined">music_note</span> Export Audio Clip${plural}
</a>
<span class="dropdown-item" id="context-xc" href='#' target="xc">
<img src='img/logo/XC.png' alt='' style="filter:grayscale(100%);height: 1.5em"> Compare with Reference Calls
</span>
<div class="dropdown-divider ${hideInSelection}"></div>
<a class="dropdown-item ${hideInSelection}" id="context-delete" href="#">
<span class='delete material-symbols-outlined'>delete_forever</span> Delete Record${plural}
</a>
<div id="${inSummary ? 'inSummary' : 'inResults'}">
<a class="dropdown-item ${hideInSummary}" id="play-region"><span class='material-symbols-outlined'>play_circle</span> Play</a>
<a class="dropdown-item ${hideInSummary} ${hideInSelection}" href="#" id="context-analyse-selection">
<span class="material-symbols-outlined">search</span> Analyse
</a>
<div class="dropdown-divider ${hideInSummary}"></div>
<a class="dropdown-item" id="create-manual-record" href="#">
<span class="material-symbols-outlined">edit_document</span> ${createOrEdit} Record${plural}
</a>
<a class="dropdown-item" id="context-create-clip" href="#">
<span class="material-symbols-outlined">music_note</span> Export Audio Clip${plural}
</a>
<span class="dropdown-item" id="context-xc" href='#' target="xc">
<img src='img/logo/XC.png' alt='' style="filter:grayscale(100%);height: 1.5em"> Compare with Reference Calls
</span>
<div class="dropdown-divider ${hideInSelection}"></div>
<a class="dropdown-item ${hideInSelection}" id="context-delete" href="#">
<span class='delete material-symbols-outlined'>delete_forever</span> Delete Record${plural}
</a>
</div>
`;
const modalTitle = document.getElementById('record-entry-modal-label');
const contextDelete = document.getElementById('context-delete');
modalTitle.textContent = `${createOrEdit} Record`;
if (!hideInSelection) {
const contextAnalyseSelectionLink = document.getElementById('context-analyse-selection');
contextAnalyseSelectionLink.addEventListener('click', getSelectionResults);

resultContext ? contextDelete.addEventListener('click', deleteRecord) :
contextDelete.addEventListener('click', function () {
deleteSpecies(target);
});
}
// Add event Handlers
const exportLink = document.getElementById('context-create-clip');
hideInSummary ? exportLink.addEventListener('click', batchExportAudio) :
exportLink.addEventListener('click', exportAudio);
if (!hideInSelection) {
document.getElementById('create-manual-record').addEventListener('click', function (e) {
if (e.target.textContent.includes('Edit')) {
95 changes: 55 additions & 40 deletions js/worker.js
Original file line number Diff line number Diff line change
@@ -119,7 +119,7 @@ const setupFfmpegCommand = ({
file,
start = 0,
end = undefined,
sampleRate = 24000,
sampleRate = undefined,
channels = 1,
format = 's16le', //<= outputs audio without header
additionalFilters = [],
@@ -130,8 +130,8 @@ const setupFfmpegCommand = ({
}) => {
const command = ffmpeg('file:' + file)
.format(format)
.audioChannels(channels)
.audioFrequency(sampleRate)
.audioChannels(channels);
sampleRate && command.audioFrequency(sampleRate);
//.audioFilters('aresample=filter_type=kaiser:kaiser_beta=9.90322');

// Add filters if provided
@@ -1332,6 +1332,12 @@ const getPredictBuffers = async ({
if (! fs.existsSync(file)) {
const found = await getWorkingFile(file);
if (!found) throw new Error('Unable to locate ' + file);
const index = filesBeingProcessed.indexOf(file);
filesBeingProcessed[index] = found;
// Need to update state too
const stateIndex = STATE.filesToAnalyse.indexOf(file);
STATE.filesToAnalyse[stateIndex] = found;
file = found;
}
// Ensure max and min are within range
start = Math.max(0, start);
@@ -1357,6 +1363,13 @@ const getPredictBuffers = async ({

async function processAudio (file, start, end, chunkStart, highWaterMark, samplesInBatch){
return new Promise((resolve, reject) => {
// Many compressed files start with a small section of silence due to encoder padding, which affects predictions
// To compensate, we move the start back a small amount, and slice the data to remove the silence
let remainingTrim;
if (start > 0) {
remainingTrim = sampleRate * 0.1;
start -= 0.05;
}
let currentIndex = 0;
const audioBuffer = Buffer.allocUnsafe(highWaterMark);
const additionalFilters = STATE.filters.sendToModel ? setAudioFilters() : [];
@@ -1379,6 +1392,17 @@ async function processAudio (file, start, end, chunkStart, highWaterMark, sample
STREAM.destroy();
return;
}
if (remainingTrim){
if (chunk.length <= remainingTrim) {
// Reduce the remaining trim by the chunk length and skip this chunk
remainingTrim -= chunk.length;
return; // Ignore this chunk and move to the next
} else {
// Trim the current chunk by the remaining amount
chunk = chunk.subarray(remainingTrim, highWaterMark);
remainingTrim = 0; // Reset the remainder after trimming
}
}
// Copy incoming chunk into the audioBuffer
const remainingSpace = highWaterMark - currentIndex;
if (chunk.length <= remainingSpace) {
@@ -1762,18 +1786,18 @@ async function uploadOpus({ file, start, end, defaultName, metadata, mode }) {
}

const bufferToAudio = async ({
file = '', start = 0, end = 3, meta = {}, format = undefined, folder = undefined, filename = undefined
file = '', start = 0, end = 3, meta = {}, format = STATE.audio.format, folder = undefined, filename = undefined
}) => {
if (! fs.existsSync(file)) {
const found = await getWorkingFile(file);
if (!found) return
file = found;
}
let padding = STATE.audio.padding;
let fade = STATE.audio.fade;
let bitrate = STATE.audio.bitrate;
let quality = parseInt(STATE.audio.quality);
let bitrate = ['mp3', 'aac', 'opus'].includes(format) ? STATE.audio.bitrate : undefined;
let quality = ['flac'].includes(format) ? parseInt(STATE.audio.quality): undefined;
let downmix = STATE.audio.downmix;
format ??= STATE.audio.format;
const formatMap = {
mp3: { audioCodec: 'libmp3lame', soundFormat: 'mp3' },
aac: { audioCodec: 'aac', soundFormat: 'mp4' },
@@ -1783,46 +1807,24 @@ const bufferToAudio = async ({
};
const { audioCodec, soundFormat } = formatMap[format] || {};

METADATA[file] || await getWorkingFile(file);
if (padding) {
start -= 1;
end += 1;
start = Math.max(0, start);
end = Math.min(end, METADATA[file].duration);
start = Math.max(0, start - 1);
end = Math.min(METADATA[file].duration, end + 1);
}

return new Promise(function (resolve, reject) {
let command = ffmpeg('file:' + file)
.toFormat(soundFormat)
.audioChannels(downmix ? 1 : -1)
// I can't get this to work with Opus
// .audioFrequency(METADATA[file].sampleRate)
.audioCodec(audioCodec)
.seekInput(start).duration(end - start)
if (['mp3', 'aac', 'opus'].includes(format)) {
//if (format === 'opus') bitrate *= 1000;
command = command.audioBitrate(bitrate)
} else if (['flac'].includes(format)) {
command = command.audioQuality(quality)
}
const filters = setAudioFilters();
if (filters.length > 0) {
command = command.audioFilters(filters);
}

if (fade && padding) {
const duration = end - start;
command = command.audioFilters(
{
filter: 'afade',
options: `t=in:ss=${start}:d=1`
filters.push(
{ filter: 'afade',
options: `t=in:ss=${start}:d=1`
},
{
filter: 'afade',
options: `t=out:st=${duration - 1}:d=1`
{ filter: 'afade',
options: `t=out:st=${end - start - 1}:d=1`
}
)
}

if (Object.entries(meta).length){
meta = Object.entries(meta).flatMap(([k, v]) => {
if (typeof v === 'string') {
@@ -1831,9 +1833,22 @@ const bufferToAudio = async ({
};
return ['-metadata', `${k}=${v}`]
});
command.addOutputOptions(meta)
}
//const destination = p.join((folder || tempPath), 'file.mp3');

let command = setupFfmpegCommand({
file: file,
start: start,
end: end,
sampleRate: undefined,
audioBitrate: bitrate,
audioQuality: quality,
audioCodec: audioCodec,
format: soundFormat,
channels: downmix ? 1 : -1,
metadata: meta,
additionalFilters: filters
})

const destination = p.join((folder || tempPath), filename);
command.save(destination);

@@ -2709,7 +2724,7 @@ const getSavedFileInfo = async (file) => {
// Get rid of archive (library) location prefix
const archiveFile = file.replace(prefix , '');
let row = await diskDB.getAsync(`
SELECT duration, filestart AS fileStart, metadata AS guano, locationID
SELECT duration, filestart AS fileStart, metadata, locationID
FROM files LEFT JOIN locations ON files.locationID = locations.id
WHERE name = ? OR archiveName = ?`,file, archiveFile);
if (!row) {

0 comments on commit e6a407b

Please sign in to comment.