iNaturalist Spectrogram

Add a spectrogram to iNaturalist audio

  1. // ==UserScript==
  2. // @name iNaturalist Spectrogram
  3. // @namespace https://greasyfork.org/users/170755
  4. // @version 2024-01-26
  5. // @description Add a spectrogram to iNaturalist audio
  6. // @author w_biggs
  7. // @match https://www.inaturalist.org/observations/*
  8. // @grant none
  9. // @require https://cdn.jsdelivr.net/gh/w-biggs/spectrogramJS@68733ac9f61f21dd21ec9b0f4c5727c9da8a5bb4/js/spectrogram.js
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js
  11. // @require https://cdn.jsdelivr.net/gh/stdlib-js/stats-base-dists-beta-cdf@48e844d06dc88c834ac95568af5207d56438297a/browser.js
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. const spectrogramCSS = `
  19. .spectrogram canvas, .spectrogram svg {
  20. position: absolute;
  21. top: 0;
  22. left: 0;
  23. }
  24.  
  25. .spectrogram {
  26. position: relative;
  27. color: black;
  28. pointer-events: auto;
  29. }
  30.  
  31. .axis {
  32. font: 14px sans-serif;
  33. }
  34.  
  35. .axis path, .axis line {
  36. fill: none;
  37. }
  38.  
  39. .axis line {
  40. shape-rendering: crispEdges;
  41. stroke: #444;
  42. stroke-width: 1.0px;
  43. stroke-dasharray: 2, 4;
  44. }
  45.  
  46. #progress-line {
  47. stroke: #a50f15;
  48. stroke-width: 4px;
  49. }
  50.  
  51. #ObservationShow .top_row .photos_column .PhotoBrowser .image-gallery-slide .sound-container {
  52. transform: translateY(-50%);
  53. }`
  54.  
  55. const style = document.createElement('style');
  56. style.textContent = spectrogramCSS;
  57. document.head.appendChild(style);
  58.  
  59. const rgbToHex = (r, g, b) => '#' + [r, g, b].map(x => {
  60. const hex = x.toString(16)
  61. return hex.length === 1 ? '0' + hex : hex
  62. }).join('');
  63.  
  64. const a = 1;
  65. const b = 1;
  66.  
  67. const threshold = 1;
  68. const granularity = 100;
  69.  
  70. const colorSchemeBase = [];
  71. for (let i = 0; i <= granularity; i++) {
  72. colorSchemeBase.push(1 - ((1 / granularity) * i));
  73. }
  74. // console.log(colorSchemeBase);
  75.  
  76. const colorScheme = colorSchemeBase.map(num => Math.min(1, 1 - ((1 - num) / threshold)))
  77. .map(num => cdf(num, a, b))
  78. .map(y => {
  79. const black = Math.round(y * 255);
  80. return rgbToHex(black, black, black);
  81. });
  82.  
  83. // console.log(`color scheme: ${colorScheme.join(', ')}`);
  84.  
  85. const waitForEl = selector => {
  86. return new Promise(resolve => {
  87. if (document.querySelector(selector)) {
  88. return resolve(document.querySelector(selector));
  89. }
  90.  
  91. const observer = new MutationObserver(mutations => {
  92. if (document.querySelector(selector)) {
  93. observer.disconnect();
  94. resolve(document.querySelector(selector));
  95. }
  96. });
  97.  
  98. observer.observe(document.body, {
  99. childList: true,
  100. subtree: true
  101. });
  102. });
  103. }
  104.  
  105. const specs = [];
  106.  
  107. const createSpec = (soundContainer, index) => {
  108. if (soundContainer.querySelector('.sound').hidden) {
  109. soundContainer.querySelector('.sound').hidden = false;
  110. soundContainer.querySelector('.spectrogram').hidden = true;
  111. } else {
  112. const spectrogramEl = document.createElement('div');
  113. const specId = `spec-${index}`;
  114. spectrogramEl.id = specId;
  115. spectrogramEl.classList.add('spectrogram');
  116. const sourceEl = soundContainer.querySelector('source');
  117. const audioUrl = sourceEl.src;
  118. soundContainer.prepend(spectrogramEl);
  119.  
  120. const spec = new Spectrogram(audioUrl, `#${specId}`, {
  121. width: 480,
  122. height: 200,
  123. maxFrequency: 10000,
  124. colorScheme: colorScheme,
  125. decRange: [-100, 0],
  126. sampleSize: 256
  127. });
  128.  
  129. specs.push(spec);
  130.  
  131. soundContainer.querySelector('.sound').hidden = true;
  132. }
  133.  
  134. /* for (const spec of specs) {
  135. console.log(spec.colorScheme);
  136. } */
  137. };
  138.  
  139. waitForEl('.sound-container source').then(() => {
  140. const soundContainers = document.getElementsByClassName('sound-container');
  141.  
  142. for (let i = 0; i < soundContainers.length; i++) {
  143. const soundContainer = soundContainers[i];
  144.  
  145. const captionsBox = soundContainer.querySelector('.captions-box');
  146.  
  147. const button = document.createElement('button');
  148. button.classList.add('btn');
  149. button.classList.add('btn-nostyle');
  150. button.textContent = 'Toggle spectrogram';
  151. button.addEventListener('click', () => createSpec(soundContainer, i));
  152.  
  153. captionsBox.appendChild(button);
  154. }
  155. });
  156. })();