Skip to content

Spectrum sonification component #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
"@kyvg/vue3-notification": "^2.9.1",
"@mdi/font": "^7.4.47",
"@wwtelescope/engine-pinia": "^0.9.0",
"chart.js": "^4.4.9",
"leaflet": "^1.9.4",
"pinia": "~2.1.7",
"screenfull": "^6.0.2",
"vue": "^3.4",
"vue-chartjs": "^5.3.2",
"vuetify": "^3.3.3"
},
"devDependencies": {
Expand Down
163 changes: 163 additions & 0 deletions src/components/SpectrumSonifier.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<template>
<div
class="spectrum-sonifier-root"
:style="cssVars"
>
<Scatter
:data="chartData"
:options="options"
ref="chartRef"
@mousemove="onMove"
@mouseout="onOut"
></Scatter>
<v-btn @click="start">Start</v-btn>
</div>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import {
Chart as ChartJS,
LinearScale,
LineElement,
PointElement,
} from "chart.js";
import {
ChartComponentRef,
Scatter,
} from "vue-chartjs";
import { dbToGain } from "../sound";

ChartJS.register(LinearScale, LineElement, PointElement);

interface ToneOptions {
pitch: number;
db: number;
}

type Sonifier = (wavelength: number, intensity: number) => ToneOptions;

interface SonifierProps {
spectrum: [number, number][];
xLabel?: string;
yLabel?: string;
sonifier: Sonifier;
color?: string;
backgroundColor?: string;
}

const props = withDefaults(defineProps<SonifierProps>(), {
xLabel: "x",
yLabel: "y",
color: "#ff0000",
backgroundColor: "#ffffff",
});

const options = ref({
responsive: true,
maintainAspectRatio: false,
});

type Point = { x: number; y: number; };

const chartData = computed(() => {
const data: Point[] = props.spectrum.map(([x, y]: [number, number]) => ({ x, y }));
const x = data.map(p => p.x);
const y = data.map(p => p.y);
return {
datasets: [{
data,
showLine: true,
borderColor: props.color,
backgroundColor: props.color,
}],
options: {
events: ["mousemove", "mouseout"],
scales: {
x: {
min: Math.min(...x),
max: Math.max(...x),
},
y: {
min: Math.min(...y),
max: Math.max(...y),
}
}
}
};
});

const cssVars = computed(() => ({
"--background-color": props.backgroundColor,
}));

const chartRef = ref<ChartComponentRef>();
let oscillator: OscillatorNode | null = null;
let gainNode: GainNode | null = null;

function start() {
// Ignore complaints about `webkitAudioContext` not being defined
// eslint-disable-next-line
// @ts-ignore
const context: AudioContext = new (window.AudioContext || window.webkitAudioContext);
oscillator = new OscillatorNode(context, {
type: "triangle",
frequency: 0,
});
gainNode = context.createGain();
oscillator.connect(gainNode).connect(context.destination);
oscillator.start();
}

function playTone(wavelength: number, intensity: number) {
if (!oscillator || !gainNode) {
return;
}
const { pitch, db } = props.sonifier(wavelength, intensity);
console.log(db, dbToGain(db));
gainNode.gain.value = dbToGain(db);
oscillator.frequency.value = pitch;
}

function onOut(_event: MouseEvent) {
const chart = chartRef.value?.chart;
if (!chart || !oscillator) {
return;
}
oscillator.frequency.value = 0;
}

function onMove(event: MouseEvent) {
const chart = chartRef.value?.chart;
if (!chart || !oscillator) {
return;
}

if (event.type !== "mousemove") {
return;
}
const points = chart.getElementsAtEventForMode(event, "nearest", { axis: "x" }, true);
if (points.length) {
const point = points[0].element;
const positionX = chart.scales.x.getValueForPixel(point.x);
const positionY = chart.scales.y.getValueForPixel(point.y);
if (positionX !== undefined && positionY !== undefined) {
playTone(positionX, positionY);
}
}

}

</script>


<style>
.spectrum-sonifier-root {
width: 100%;
height: 400px;

canvas {
background: var(--background-color);
}
}
</style>
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import IconButton from "./components/IconButton.vue";
import LocationSearch from "./components/LocationSearch.vue";
import LocationSelector from "./components/LocationSelector.vue";
import PlaybackControl from "./components/PlaybackControl.vue";
import SpectrumSonifier from "./components/SpectrumSonifier.vue";
import SpeedControl from "./components/SpeedControl.vue";
import TapToInput from "./components/TapToInput.vue";
import WwtHud from "./components/WwtHud.vue";
Expand Down Expand Up @@ -44,6 +45,7 @@ export {
LocationSearch,
LocationSelector,
PlaybackControl,
SpectrumSonifier,
SpeedControl,
TapToInput,
WwtHud,
Expand Down
7 changes: 7 additions & 0 deletions src/sound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function gainToDB(gain: number): number {
return 20 * Math.log10(gain);
}

export function dbToGain(db: number): number {
return Math.pow(10, db / 20);
}
29 changes: 29 additions & 0 deletions src/stories/SpectrumSonifier.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint-disable @typescript-eslint/naming-convention */

import { Meta, StoryObj } from "@storybook/vue3";
import { SpectrumSonifier } from "..";
import spectrum from "./spectrum";

const meta: Meta<typeof SpectrumSonifier> = {
component: SpectrumSonifier,
tags: ["autodocs"],
};

export default meta;
type Story = StoryObj<typeof SpectrumSonifier>;

export const Primary: Story = {
render: (args: unknown) => ({
components: { SpectrumSonifier },
template: `<SpectrumSonifier v-bind="args" />`,
setup() {
return { args };
}
}),
args: {
spectrum,
xLabel: "X Axis",
yLabel: "Y Axis",
sonifier: (_wavelength: number, intensity: number) => ({ pitch: intensity * 10, db: 5 + (intensity / 10) }),
}
};
2 changes: 2 additions & 0 deletions src/stories/spectrum.ts

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ __metadata:
"@vue/compiler-sfc": ^3.4.25
"@vue/eslint-config-typescript": ^12.0.0
"@wwtelescope/engine-pinia": ^0.9.0
chart.js: ^4.4.9
copy-webpack-plugin: ^12.0.2
css-loader: ^7.1.1
eslint: ^8.31.0
Expand All @@ -540,6 +541,7 @@ __metadata:
typedoc-plugin-vue: ^1.1.0
typescript: ^4.9.4
vue: ^3.4
vue-chartjs: ^5.3.2
vue-loader: ^17.4.2
vue-template-compiler: ^2.7.14
vuetify: ^3.3.3
Expand Down Expand Up @@ -923,6 +925,13 @@ __metadata:
languageName: node
linkType: hard

"@kurkle/color@npm:^0.3.0":
version: 0.3.4
resolution: "@kurkle/color@npm:0.3.4"
checksum: b95c6abe0241ba1745b3c84de3b464296b95ce577110b54f46e6c6dcc9a0966491533df43812bd6c66f92cf818e385d1390b280cd5851d4afb52fc37f8a6c0b9
languageName: node
linkType: hard

"@kyvg/vue3-notification@npm:^2.9.1":
version: 2.9.1
resolution: "@kyvg/vue3-notification@npm:2.9.1"
Expand Down Expand Up @@ -3628,6 +3637,15 @@ __metadata:
languageName: node
linkType: hard

"chart.js@npm:^4.4.9":
version: 4.4.9
resolution: "chart.js@npm:4.4.9"
dependencies:
"@kurkle/color": ^0.3.0
checksum: f41f3a2bb835c32431fe95ae765028d08fb4844347ea307f803dbe9fd84df4bf8d02750ff9c084d7ab9c47b22ae243f40eb97600bc9536192089a035929db88c
languageName: node
linkType: hard

"check-error@npm:^2.1.1":
version: 2.1.1
resolution: "check-error@npm:2.1.1"
Expand Down Expand Up @@ -11072,6 +11090,16 @@ __metadata:
languageName: node
linkType: hard

"vue-chartjs@npm:^5.3.2":
version: 5.3.2
resolution: "vue-chartjs@npm:5.3.2"
peerDependencies:
chart.js: ^4.1.1
vue: ^3.0.0-0 || ^2.7.0
checksum: a1c7c90d1375ebfd6af4fe08f242da80918528ea647fd98d327f14bff267a284e355c69e972f46a39db73489f915af01417c5bbd156453cb8bb646c2c0d1be18
languageName: node
linkType: hard

"vue-component-type-helpers@npm:latest":
version: 2.2.8
resolution: "vue-component-type-helpers@npm:2.2.8"
Expand Down