Skip to content

Commit

Permalink
added loading spinner
Browse files Browse the repository at this point in the history
  • Loading branch information
Geoxor committed Jul 25, 2022
1 parent 1345bb6 commit 288a059
Show file tree
Hide file tree
Showing 11 changed files with 76 additions and 114 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "amethyst",
"author": "amethyst <geoxor123@outlook.com>",
"productName": "Amethyst",
"version": "1.3.15",
"version": "1.3.16",
"main": "./release/dist/main/main.js",
"licenses": [
{
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/Menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ onKeyStroke("Escape", () => (isShowing.value = false));
<div class="hover:text-menu-text-hover cursor-default flex items-center mt-0.25 px-2 h-full" @click.stop="isShowing = !isShowing">
{{ title }}
</div>
<div v-if="isShowing" class="absolute z-30 flex items-center bg-menu-background text-menu-text py-2 flex-col w-96" @click="isShowing = false">
<div v-if="isShowing" class="absolute z-30 flex select-none items-center bg-menu-background text-menu-text py-2 flex-col w-96" @click="isShowing = false">
<slot />
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/MenuBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const openPreferences = () => {
</script>

<template>
<div class="bg-menu-bar font-cozette drag text-xs flex justify-between items-center">
<div class="bg-menu-bar font-cozette drag text-xs select-none flex justify-between items-center">
<div class="flex no-drag h-full items-center">
<img v-if="state.isDev.value" src="../icon-dev.png" class="transform active:rotate-360 active:scale-50 transition duration-200 cursor-heart-pointer ml-1 h-4" alt="" @click="playPop">
<img v-else src="../icon.png" class="transform active:rotate-360 active:scale-50 transition duration-200 cursor-heart-pointer ml-1 h-4" alt="" @click="playPop">
Expand Down
10 changes: 5 additions & 5 deletions src/renderer/components/Queue.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { useKeyModifier } from "@vueuse/core";
import { ref } from "vue";
import { usePlayer, useState } from "../amethyst";
import { BPM_COMPUTATION_CONCURRENCY, COVERART_RENDERING_CONCURRENCY } from "../state";
import Cover from "./Cover.vue";
const state = useState();
const player = usePlayer();
const isHoldingControl = useKeyModifier("Control");
Expand Down Expand Up @@ -56,14 +54,16 @@ const parseTitle = (path: string) => {

<input v-model="filterText" type="text" class="border-2 border-gray-400 indent-xs text-xs w-full mb-2" placeholder="artists, title & format...">

<!-- TODO: refactor this mess into a component -->
<li
v-for="([song, i]) of player.getQueue().map((song, i) => song.toLowerCase().includes(filterText.toLowerCase()) ? [song, i] : undefined).filter(song => !!song) as [string, number][]"
:key="song" :class="[isHoldingControl && 'control-hover', isHoldingControl ? 'cursor-external-pointer' : 'cursor-default', i === player.getCurrentlyPlayingIndex() && 'text-queue-text-active']" class=" h-3 mb-0.5 hover:text-queue-text-hover select-none" @keypress.prevent
:key="song" :class="[isHoldingControl && 'control-hover', isHoldingControl ? 'cursor-external-pointer' : 'cursor-default', i === player.getCurrentlyPlayingIndex() && 'text-queue-text-active']" class=" h-3 mb-0.5 hover:text-queue-text-hover relative select-none" @keypress.prevent
@click="isHoldingControl ? invoke('show-item', [player.getQueue()[i]]) : player.setCurrentlyPlayingIndex(i)"
>
<cover class="inline align-top w-3 h-3" :song-path="song" />
<!-- <cover class="inline align-top w-3 h-3" :song-path="song" /> -->
<img v-if="state.state.processQueue.has(song)" src="../spinners/spinner.gif" alt="" class="w-4 h-4 absolute -top-0.25 -left-0.5">

<p class=" inline align-top text-xs ml-2 max-w-40 overflow-hidden overflow-ellipsis ">
<p :class="[state.state.processQueue.has(song) && 'ml-5']" class=" inline align-top text-xs max-w-40 overflow-hidden overflow-ellipsis ">
{{ i === player.getCurrentlyPlayingIndex() ? "⏵ " : "" }}{{ trimString(isHoldingAlt ? `${song.substring(0, (MAX_CHARS - 3) / 2)}...${song.substring(song.length - (MAX_CHARS - 3) / 2)}` : parseTitle(song), i === player.getCurrentlyPlayingIndex() ? MAX_CHARS - 2 : MAX_CHARS) }}
</p>
</li>
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/components/Spectrum.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { onMounted, onUnmounted } from "vue";
const props = defineProps<{ node: MediaElementAudioSourceNode }>();
const SPECTRUM_WIDTH = 500;
const SPECTRUM_HEIGHT = 150;
const DOWNSCALE_FACTOR = 7;
// const DOWNSCALE_FACTOR = 7;
const TILT_MULTIPLIER = 0.005; // 3dB/octave
const FFT_SIZE = 8192;
const VERTICAL_ZOOM_FACTOR = 1.5;
const DOWNSCALED_WIDTH = SPECTRUM_WIDTH / DOWNSCALE_FACTOR;
const DOWNSCALED_HEIGHT = SPECTRUM_HEIGHT / DOWNSCALE_FACTOR;
// const DOWNSCALED_WIDTH = SPECTRUM_WIDTH / DOWNSCALE_FACTOR;
// const DOWNSCALED_HEIGHT = SPECTRUM_HEIGHT / DOWNSCALE_FACTOR;
const defaultSpectrumColor = "#868aff";
let shouldFuckOff = false;
Expand Down
65 changes: 2 additions & 63 deletions src/renderer/player.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,12 @@
import { useLocalStorage } from "@vueuse/core";
import type { IAudioMetadata } from "music-metadata";
import { reactive, watch } from "vue";
// import { analyze } from "web-audio-beat-detector";
// import { useElectron } from "./amethyst";
import type ElectronEventManager from "./electronEventManager";
import type AppState from "./state";
// import { BPM_COMPUTATION_CONCURRENCY, COVERART_RENDERING_CONCURRENCY } from "./state";
import mitt from 'mitt';


export const ALLOWED_EXTENSIONS = ["ogg", "flac", "wav", "opus", "aac", "aiff", "mp3", "m4a"];

// async function analyzeBpm(path: string) {
// // get an AudioBuffer from the file
// const uint: Uint8Array = await useElectron().invoke("read-file", [path]);
// const audioContext = new AudioContext();
// const audioBuffer = await audioContext.decodeAudioData(uint.buffer);

// // analyze the audio buffer
// const bpm = Math.round(await analyze(audioBuffer));

// return bpm;
// }

// Turns seconds from 80 to 1:20
export const secondsHuman = (time: number) => {
const seconds = ~~time;
Expand All @@ -32,7 +16,7 @@ export const secondsHuman = (time: number) => {
};

export const Events = Object.freeze({
"play": undefined,
"play": "" as string,
"pause": undefined,
"setVolume": 0 as number,
"seekTo": 0 as number,
Expand Down Expand Up @@ -89,58 +73,13 @@ export default class Player {
return array;
}

// public getCoverArt = async (path: string) => {
// if (this.appState.state.coverProcessQueue < COVERART_RENDERING_CONCURRENCY) {
// this.appState.state.coverProcessQueue++;
// try {
// this.appState.state.coverCache[path] = await this.electron.invoke<string>("get-cover", [path]);
// }
// catch (error) { }
// this.appState.state.coverProcessQueue--;
// }
// else {
// setTimeout(async () =>
// this.getCoverArt(path), 100,
// );
// }
// };

// public getBpm = async (path: string) => {
// if (this.appState.state.bpmProcessQueue < BPM_COMPUTATION_CONCURRENCY) {
// this.appState.state.bpmProcessQueue++;
// try {
// this.appState.state.bpmCache[path] = await analyzeBpm(path);
// }
// catch (error) { }
// this.appState.state.bpmProcessQueue--;
// }
// else {
// setTimeout(async () =>
// this.getBpm(path), 100,
// );
// }
// };

// public analyzeQueueForBpm() {
// for (let i = 0; i < this.getQueue().length; i++) {
// const path = this.getQueue()[i];
// if (path && !this.appState.state.bpmCache[path])
// this.getBpm(path);
// }
// }

public loadSoundAndPlay(path: string) {
this.state.sound && this.pause();
this.state.sound = new Audio(`file://${path}`);
this.state.sound.volume = this.state.volume;
this.play();
this.state.sound.onended = () => this.next();

// Pixelated covers
// invoke<Buffer>("get-cover-pixelized", [path]).then((cover) => {
// currentCover.value = `data:image/png;base64,${cover}`;
// });

// Discord rich presence timer that updates discord every second
this.state.richPresenceTimer && clearInterval(this.state.richPresenceTimer);
this.state.richPresenceTimer = setInterval(() => {
Expand Down Expand Up @@ -176,7 +115,7 @@ export default class Player {
public play() {
this.state.sound.play();
this.state.isPlaying = true;
this.emit("play");
this.emit("play", this.state.currentlyPlayingFilePath);
}

public pause() {
Expand Down
Binary file added src/renderer/spinners/spinner.aseprite
Binary file not shown.
Binary file added src/renderer/spinners/spinner.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/renderer/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default class AppState {
version: "",
isMinimized: false,
isMaximized: false,
processQueue: new Set(),
coverProcessQueue: 0,
bpmProcessQueue: 0,
coverCache: useLocalStorage<Record<string, string>>("cover-cache", {}),
Expand Down
14 changes: 9 additions & 5 deletions src/renderer/waveformRenderWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,15 @@ function render(canvas: HTMLCanvasElement, audioData: Float32Array): ImageBitmap

ctx.stroke();

// Need to create an image from the OffscreenCanvas to send it back to the main process.
// https://developer.mozilla.org/en-US/docs/Web/API/OffScreenCanvas/transferToImageBitmap
// @ts-ignore
const img: ImageBitmap = canvas.transferToImageBitmap();
return img;
try {
// Need to create an image from the OffscreenCanvas to send it back to the main process.
// https://developer.mozilla.org/en-US/docs/Web/API/OffScreenCanvas/transferToImageBitmap
// @ts-ignore
const img: ImageBitmap = canvas.transferToImageBitmap();
return img;
} catch (error) {
return undefined
}
}

// TODO: make this transfer an array buffer instead of using postMessage, it's way faster to share pointers
Expand Down
88 changes: 53 additions & 35 deletions src/renderer/waveformRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useState } from "./amethyst";
import Player from "./player";


export class WaveformRenderer {
public canvas: HTMLCanvasElement;

Expand All @@ -14,8 +16,20 @@ export class WaveformRenderer {
this.audioCtx = new AudioContext();
this.audioBuffer = null;
this.currentWorker = null;
const state = useState();

this.player.on('play', async (filePath) => {
// TODO: refactor this system so amethyst automatically determines when processing has finished from child plugins
state.state.processQueue.add(filePath);

try {
await this.handlePlayAudio()
} catch (error) {
console.log(`Failed to render waveform for audio: ${filePath}`);
}

this.player.on('play', this.handlePlayAudio);
state.state.processQueue.delete(filePath);
});
}

private handlePlayAudio = async () => {
Expand All @@ -36,11 +50,10 @@ export class WaveformRenderer {


const tempBuffer = await this.fetchAudioBuffer(this.player.state.sound.src, offlineAudioCtx);

if (currentSound != this.player.state.sound) return;

this.audioBuffer = tempBuffer;
this.renderWaveform();
await this.renderWaveform();
};

private setCanvasSize = () => {
Expand Down Expand Up @@ -93,39 +106,44 @@ export class WaveformRenderer {
})
};

private renderWaveform = () => {
this.setCanvasSize();
const ctx = this.canvas.getContext('2d');
ctx?.clearRect(0, 0, this.canvas.width, this.canvas.height);
const backCanvas = document.createElement('canvas');
backCanvas.width = this.canvas.width;
backCanvas.height = this.canvas.height;

// Electron 18.0.3 is using Chrome 100.0.4896.75
// https://www.electronjs.org/releases/stable?version=18&page=2#18.0.3
//
// transferControlToOffscreen has been available since Chrome 69
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/transferControlToOffscreen#browser_compatibility
// @ts-ignore
const offscreen: OffscreenCanvas = backCanvas.transferControlToOffscreen();

if (this.audioBuffer === null) return;

const audioData = this.audioBuffer.getChannelData(0);

if (this.currentWorker !== null) {
this.currentWorker.postMessage({ stop: true });
this.currentWorker.terminate();
}

this.currentWorker = new Worker("waveformRenderWorker.ts");
// TODO: cache the analysis FFT data
this.currentWorker.onmessage = ({data}) => {
private renderWaveform = async () => {
return new Promise<void>((resolve, reject) => {
this.setCanvasSize();
const ctx = this.canvas.getContext('2d');
ctx?.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx?.drawImage(data, 0, 0);
this.currentWorker = null;
};
this.currentWorker.postMessage({ canvas: offscreen, audioData }, [offscreen]);
const backCanvas = document.createElement('canvas');
backCanvas.width = this.canvas.width;
backCanvas.height = this.canvas.height;

// Electron 18.0.3 is using Chrome 100.0.4896.75
// https://www.electronjs.org/releases/stable?version=18&page=2#18.0.3
//
// transferControlToOffscreen has been available since Chrome 69
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/transferControlToOffscreen#browser_compatibility
// @ts-ignore
const offscreen: OffscreenCanvas = backCanvas.transferControlToOffscreen();

if (this.audioBuffer === null) return reject();

const audioData = this.audioBuffer.getChannelData(0);

if (this.currentWorker !== null) {
this.currentWorker.postMessage({ stop: true });
this.currentWorker.terminate();
reject();
}

this.currentWorker = new Worker("waveformRenderWorker.ts");
this.currentWorker.postMessage({ canvas: offscreen, audioData }, [offscreen])

// TODO: cache the analysis FFT data
this.currentWorker.onmessage = ({data}) => {
ctx?.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx?.drawImage(data, 0, 0);
this.currentWorker = null;
resolve();
};
})
};

public clean = () => {
Expand Down

0 comments on commit 288a059

Please sign in to comment.