diff --git a/packages/app/app/app.global.scss b/packages/app/app/app.global.scss index 53a5e42699..cab16972dc 100644 --- a/packages/app/app/app.global.scss +++ b/packages/app/app/app.global.scss @@ -62,16 +62,22 @@ table { ::-webkit-scrollbar { width: 0.5rem !important; + height: 0.5rem !important; } ::-webkit-scrollbar-track { background-color: $background !important; - border-radius: 0; + border-radius: 0.55rem !important; } ::-webkit-scrollbar-thumb { border-radius: 0.5rem !important; background-color: $background2 !important; + transition: $short-duration; +} + +::-webkit-scrollbar-corner { + background-color: transparent !important; } //Semantic UI section diff --git a/packages/app/app/components/Normalizer/index.tsx b/packages/app/app/components/Normalizer/index.tsx new file mode 100644 index 0000000000..ea9b45fda8 --- /dev/null +++ b/packages/app/app/components/Normalizer/index.tsx @@ -0,0 +1,119 @@ +/* + Adapted from https://github.com/est31/js-audio-normalizer + + The MIT License (MIT) + Permission is hereby granted, free of charge, to any + person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the + Software without restriction, including without + limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software + is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice + shall be included in all copies or substantial portions + of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF + ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT + SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR + IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +import { pluginFactory } from 'react-hifi'; +import { Plugin } from 'react-hifi/dist/types/plugins/Plugin'; + +export interface NormalizerProps { + /** url of stream that is played */ + url: string; + normalize: boolean; +} + +export class Normalizer implements Plugin { + private audioContext: AudioContext; + constructor() { + this.createNode = this.createNode.bind(this); + } + + shouldNotUpdate(prevProps: NormalizerProps, nextProps: NormalizerProps) { + return prevProps.url === nextProps.url; + } + + normalizeTrack = (gainNode: GainNode, url: string, normalize) => { + if (!normalize) { + gainNode.gain.value = 1; + return; + } + + // delay workaround with suspending audiocontext until fetch is finished + this.audioContext.suspend(); + + fetch(url) + .then((res) => res.arrayBuffer()) + .then((arrayBuffer) => { + // use ArrayBuffer + this.audioContext.decodeAudioData(arrayBuffer).then((audioBuffer) => { + // use AudioBuffer + + // perform calculations on audio data + const decodedBuffer = audioBuffer.getChannelData(0); + const sliceLen = Math.floor(audioBuffer.sampleRate * 0.05); + const averages = []; + let sum = 0.0; + for (let i = 0; i < decodedBuffer.length; i++) { + sum += decodedBuffer[i] ** 2; + if (i % sliceLen === 0) { + sum = Math.sqrt(sum / sliceLen); + averages.push(sum); + sum = 0; + } + } + + // get average + sum = 0; + for (let i = 0; i< averages.length; i++){ + sum += averages[i]; + } + const a = sum/averages.length; + + let gain = 1.0 / a; + // Perform some clamping + gain = Math.max(gain, 0.02); + gain = Math.min(gain, 100.0); + + // ReplayGain uses pink noise for this one one but we just take + // some arbitrary value... we're no standard + // Important is only that we don't output on levels + // too different from other websites + gain = gain / 10.0; + + // round the float value to 3 decimal places + + gain = parseFloat(gain.toFixed(3)); + + gainNode.gain.value = gain; + this.audioContext.resume(); + }); + }); + } + + createNode = (audioContext: AudioContext, { url, normalize }: NormalizerProps) => { + this.audioContext = audioContext; + const gainNode = audioContext.createGain(); + this.normalizeTrack(gainNode, url, normalize); + return gainNode; + } + + updateNode = (node: GainNode, {url, normalize}: NormalizerProps) => { + this?.normalizeTrack(node, url, normalize); + } +} + +export default pluginFactory(new Normalizer()); diff --git a/packages/app/app/components/Settings/styles.scss b/packages/app/app/components/Settings/styles.scss index 418cfae286..6c1f5b2d28 100644 --- a/packages/app/app/components/Settings/styles.scss +++ b/packages/app/app/components/Settings/styles.scss @@ -31,6 +31,7 @@ line-height: 1rem; .settings_item_name { + margin-bottom: 0.5em; } } diff --git a/packages/app/app/containers/SettingsContainer/__snapshots__/SettingsContainer.test.tsx.snap b/packages/app/app/containers/SettingsContainer/__snapshots__/SettingsContainer.test.tsx.snap index 05ff8ff6f9..e07a5a9436 100644 --- a/packages/app/app/containers/SettingsContainer/__snapshots__/SettingsContainer.test.tsx.snap +++ b/packages/app/app/containers/SettingsContainer/__snapshots__/SettingsContainer.test.tsx.snap @@ -268,6 +268,53 @@ exports[`Settings view container should render settings 1`] = ` +
+
+ Audio +
+
+
+
+ + +

+ Automatically adjust volume of tracks so that they are played at the same level. This needs to fetch the whole track to work, so it may cause a delay between tracks. +

+
+
+
+ +
+
+
+
@@ -1511,68 +1558,6 @@ exports[`Settings view container should render settings 1`] = `
-
-
- Social -
-
-
-
- - - -
-
- -
-
-
- - - -
-
- -
-
-
-
diff --git a/packages/app/app/containers/SoundContainer/index.js b/packages/app/app/containers/SoundContainer/index.js index 3c1b9f19c0..90f22f8737 100644 --- a/packages/app/app/containers/SoundContainer/index.js +++ b/packages/app/app/containers/SoundContainer/index.js @@ -17,6 +17,7 @@ import * as LyricsActions from '../../actions/lyrics'; import { filterFrequencies } from '../../components/Equalizer/chart'; import * as Autoradio from './autoradio'; import VisualizerContainer from '../../containers/VisualizerContainer'; +import Normalizer from '../../components/Normalizer'; import globals from '../../globals'; import HlsPlayer from '../../components/HLSPlayer'; import { ipcRenderer } from 'electron'; @@ -206,6 +207,10 @@ class SoundContainer extends React.Component { position={player.seek} onError={this.handleError} > + ({ diff --git a/packages/core/src/settings/index.ts b/packages/core/src/settings/index.ts index 44792e2f74..ecd2f9df6f 100644 --- a/packages/core/src/settings/index.ts +++ b/packages/core/src/settings/index.ts @@ -55,6 +55,14 @@ export const settingsConfig: Array = [ default: 50, hide: true }, + { + name: 'normalize', + category: 'audio', + description: 'normalize-description', + type: SettingType.BOOLEAN, + prettyName: 'normalize', + default: false + }, { name: 'loopAfterQueueEnd', category: 'playback', @@ -330,13 +338,15 @@ export const settingsConfig: Array = [ prettyName: 'nuclear-identity-service-url', category: 'social', type: SettingType.STRING, - default: 'http://localhost:3000' + default: 'http://localhost:3000', + hide: true }, { name: 'nuclearPlaylistsServiceUrl', prettyName: 'nuclear-playlists-service-url', category: 'social', type: SettingType.STRING, - default: 'http://localhost:3010' + default: 'http://localhost:3010', + hide: true } ]; diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index 0564a55817..6f21c7a821 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -247,6 +247,7 @@ "settings": { "api-port": "Port used by the api", "api-url": "", + "audio": "Audio", "autoradio": "Autoradio", "autoradio-craziness": "Autoradio craziness", "autoradio-craziness-description": "Autoradio will select songs that are less similar to the ones already in the queue the crazier it is", @@ -291,6 +292,8 @@ "mastodon-post-format-description": "Nuclear will post a status on Mastodon after each track completes playing. The above string will be the template for each post, with {{artist}} and {{title}} replaced with the artist and title of the track.", "mastodon-post-format-label": "Post format", "mastodon-title": "", + "normalize": "Normalize volume", + "normalize-description": "Automatically adjust volume of tracks so that they are played at the same level. This needs to fetch the whole track to work, so it may cause a delay between tracks.", "nuclear-identity-service-url": "Nuclear identity service URL", "nuclear-playlists-service-url": "Nuclear playlists service URL", "mini-player": "Use mini player style",