Skip to content
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

Add UI for Configuring DSP including Parametric Equalizers #756

Merged
merged 8 commits into from
Dec 23, 2024
453 changes: 453 additions & 0 deletions src/components/dsp/DSPParametricEQ.vue

Large diffs are not rendered by default.

203 changes: 203 additions & 0 deletions src/components/dsp/DSPPipeline.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
<template>
<v-timeline density="compact" side="end">
<!-- Input -->
<v-timeline-item
:dot-color="isDotActive('input')"
size="x-small"
@click="handleSelect('input')"
>
<v-btn
flat
rounded="pill"
:color="getButtonColor('input')"
:class="getButtonClass('input')"
class="dsp-pipeline-card"
>
<v-card-text class="px-3 py-1">{{
$t("settings.dsp.input")
}}</v-card-text>
</v-btn>
</v-timeline-item>

<v-timeline-item :hide-dot="true" size="x-small" />

<!-- DSP Filters -->
<v-timeline-item
v-for="(filter, index) in dsp.filters"
:key="index"
:dot-color="isDotActive(index)"
size="x-small"
:hide-dot="isDisabled(index)"
@click="handleSelect(index)"
@contextmenu.prevent="(e) => openFilterContextMenu(e, index)"
>
<v-btn
flat
rounded="pill"
:color="getButtonColor(index)"
:class="getButtonClass(index)"
class="dsp-pipeline-card"
>
<v-card-text class="px-3 py-1">{{
$t(`settings.dsp.types.${filter.type}`)
}}</v-card-text>
</v-btn>
</v-timeline-item>

<!-- Add Filter Button -->
<v-timeline-item dot-color="secondary" size="x-small" :hide-dot="true">
<v-btn
outlined
rounded="pill"
class="dsp-pipeline-card add-filter-btn"
:elevation="0"
@click="emit('onAddFilter')"
>
<v-card-text class="py-1 d-flex align-center">
<v-icon size="small" class="mr-1">mdi-plus</v-icon>
{{ $t("settings.dsp.filter.add") }}
</v-card-text>
</v-btn>
</v-timeline-item>

<v-timeline-item :hide-dot="true" size="x-small" />

<!-- Output -->
<v-timeline-item
:dot-color="isDotActive('output')"
size="x-small"
@click="handleSelect('output')"
>
<v-btn
flat
rounded="pill"
:color="getButtonColor('output')"
:class="getButtonClass('output')"
class="dsp-pipeline-card"
>
<v-card-text class="px-3 py-1">{{
$t("settings.dsp.output")
}}</v-card-text>
</v-btn>
</v-timeline-item>
</v-timeline>
</template>

<script setup lang="ts">
import { DSPConfig } from "@/plugins/api/interfaces";
import { eventbus } from "@/plugins/eventbus";
import { useTheme } from "vuetify";

const theme = useTheme();

type SelectionType = number | "input" | "output";

const props = defineProps<{
dsp: DSPConfig;
selected: SelectionType | null;
}>();

const emit = defineEmits<{
(e: "onSelect", selected: SelectionType): void;
(e: "onAddFilter"): void;
(e: "onMoveFilter", data: { index: number; direction: "up" | "down" }): void;
(e: "onDeleteFilter", index: number): void;
}>();

const isDotActive = (value: SelectionType): string =>
props.selected === value ? "primary" : "secondary";

const isDisabled = (value: SelectionType): boolean => {
if (value === "input" || value === "output") {
return false;
} else {
return !props.dsp.filters[value].enabled;
}
};

const getButtonColor = (value: SelectionType): string => {
if (props.selected === value) {
return "primary";
}
return theme.global.current.value.dark ? "grey-darken-3" : "grey-lighten-3";
};

const getButtonClass = (value: SelectionType) => ({
"filter-selected": props.selected === value,
});

const handleSelect = (value: SelectionType): void => {
emit("onSelect", value);
};

const moveFilter = (index: number, direction: "up" | "down"): void => {
emit("onMoveFilter", { index, direction });
};

const deleteFilter = (index: number): void => {
emit("onDeleteFilter", index);
};

const openFilterContextMenu = function (evt: Event, index: number) {
const menuItems = [
{
label: "settings.dsp.move_up",
labelArgs: [],
action: () => {
moveFilter(index, "up");
},
disabled: index === 0,
icon: "mdi-arrow-up",
},
{
label: "settings.dsp.move_down",
labelArgs: [],
action: () => {
moveFilter(index, "down");
},
disabled: index === props.dsp.filters.length - 1,
icon: "mdi-arrow-down",
},
{
label: "settings.dsp.delete_filter",
labelArgs: [],
action: () => {
deleteFilter(index);
},
icon: "mdi-delete",
},
];
eventbus.emit("contextmenu", {
items: menuItems,
posX: (evt as PointerEvent).clientX,
posY: (evt as PointerEvent).clientY,
});
};
</script>

<style scoped>
.dsp-pipeline-card {
min-width: 160px;
transition: all 0.2s ease;
}

.dsp-pipeline-card:hover,
.filter-selected {
transform: translateX(4px);
}

.add-filter-btn {
background: transparent;
border: 1px dashed rgba(0, 0, 0, 0.5);
cursor: pointer;
}

.v-theme--dark .add-filter-btn {
border-color: rgba(255, 255, 255, 0.5);
}

.add-filter-btn:hover {
border-color: rgb(var(--v-theme-primary));
transform: none;
}
</style>
94 changes: 94 additions & 0 deletions src/components/dsp/DSPSlider.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<template>
<v-card flat>
<v-card-text class="d-flex align-center gap-4">
<v-slider
v-model="model"
:min="config.min"
:max="config.max"
:step="config.step"
:label="config.label"
hide-details
class="flex-grow-1 pr-4"
density="compact"
/>
<v-text-field
v-model="displayValue"
type="number"
hide-details
density="compact"
style="max-width: 100px"
@focus="isEditing = true"
@blur="isEditing = false"
/>
<span style="min-width: 40px" class="pl-2">{{ config.unit }}</span>
</v-card-text>
</v-card>
</template>

<script setup lang="ts">
import { $t } from "@/plugins/i18n";
import { ref, computed } from "vue";

type ParameterConfig = {
// Minimum value for the slider
min: number;
// Maximum value for the slider
max: number;
// Step increment for the slider
step: number;
label: string;
unit: string;
};

const props = defineProps<{
type: "gain" | "q" | "frequency" | ParameterConfig;
}>();

const model = defineModel<number>({ required: true });
const isEditing = ref(false);

const config = computed((): ParameterConfig => {
if (props.type === "gain") {
return {
min: -15,
max: 15,
step: 0.1,
label: $t("settings.dsp.parameter.gain"),
unit: "dB",
};
} else if (props.type === "q") {
return {
min: 0.1,
max: 20,
step: 0.1,
label: $t("settings.dsp.parameter.q_factor"),
unit: "",
};
} else if (props.type === "frequency") {
return {
min: 20,
max: 20000,
step: 1,
label: $t("settings.dsp.parameter.frequency"),
unit: "Hz",
};
} else {
return props.type;
}
});

// Computed property for compact display of the value
const displayValue = computed({
get: () => {
if (isEditing.value) {
return model.value.toString();
}
if (model.value > 1000) return model.value.toFixed(0);
if (model.value > 100) return model.value.toFixed(1);
return model.value.toFixed(2);
},
set: (value: string) => {
model.value = Number(value);
},
});
</script>
44 changes: 44 additions & 0 deletions src/components/dsp/DSPToneControl.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<template>
<v-card-item>
<DSPSlider
v-model="tone_control.bass_level"
:type="{
min: -10,
max: 10,
step: 0.1,
label: $t('settings.dsp.tone_control.bass_level'),
unit: 'dB',
}"
/>
</v-card-item>
<v-card-item>
<DSPSlider
v-model="tone_control.mid_level"
:type="{
min: -10,
max: 10,
step: 0.1,
label: $t('settings.dsp.tone_control.mid_level'),
unit: 'dB',
}"
/>
</v-card-item>
<v-card-item>
<DSPSlider
v-model="tone_control.treble_level"
:type="{
min: -10,
max: 10,
step: 0.1,
label: $t('settings.dsp.tone_control.treble_level'),
unit: 'dB',
}"
/>
</v-card-item>
</template>
<script setup lang="ts">
import { ToneControlFilter } from "@/plugins/api/interfaces";
import DSPSlider from "./DSPSlider.vue";

const tone_control = defineModel<ToneControlFilter>({ required: true });
</script>
12 changes: 12 additions & 0 deletions src/helpers/player_menu_items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,5 +178,17 @@ export const getPlayerMenuItems = (
icon: "mdi-cog-outline",
});

// add shortcut to dsp settings
menuItems.push({
label: "open_dsp_settings",
labelArgs: [],
action: () => {
store.showFullscreenPlayer = false;
store.showPlayersMenu = false;
router.push(`/settings/editplayer/${player.player_id}/dsp`);
},
icon: "mdi-equalizer",
});

return menuItems;
};
19 changes: 19 additions & 0 deletions src/plugins/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
CoreConfig,
ItemMapping,
AlbumType,
DSPConfig,
} from "./interfaces";

const DEBUG = process.env.NODE_ENV === "development";
Expand Down Expand Up @@ -1049,6 +1050,24 @@ export class MusicAssistantApi {
});
}

// DSP related functions

public async getDSPConfig(player_id: string): Promise<DSPConfig> {
// Return the DSP configuration for a player.
return this.sendCommand("config/players/dsp/get", { player_id });
}

public async saveDSPConfig(
player_id: string,
config: DSPConfig,
): Promise<DSPConfig> {
// Save/update the DSP configuration for a player.
return this.sendCommand("config/players/dsp/save", {
player_id,
config,
});
}

// Core Config related functions

public async getCoreConfigs(): Promise<CoreConfig[]> {
Expand Down
Loading
Loading