Skip to content
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
20 changes: 20 additions & 0 deletions gui/public/i18n/en/translation.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,26 @@ settings-osc-vmc-mirror_tracking = Mirror tracking
settings-osc-vmc-mirror_tracking-description = Mirror the tracking horizontally.
settings-osc-vmc-mirror_tracking-label = Mirror tracking

## Spatial Headphones OSC settings
settings-osc-spatial-headphones = Spatial Headphones
settings-osc-spatial-headphones-description =
Configure OSC output for spatial headphone software.
Compatible with Neumann RIME and other software that accepts /ypr <yaw> <pitch> <roll> in degrees.
settings-osc-spatial-headphones-enable = Enable
settings-osc-spatial-headphones-enable-description = Toggle the sending of head tracking data to compatible spatial headphone software, including Neumann RIME.
settings-osc-spatial-headphones-enable-label = Enable
settings-osc-spatial-headphones-network = Network ports
settings-osc-spatial-headphones-network-description = Set the port used to send data to compatible spatial headphone software.
settings-osc-spatial-headphones-network-port_out =
.label = Port Out
.placeholder = Port out (default: 7001)
settings-osc-spatial-headphones-network-address = Network address
settings-osc-spatial-headphones-network-address-description = Choose the address of the device running compatible spatial headphone software, such as Neumann RIME.
settings-osc-spatial-headphones-network-address-placeholder = IPV4 address

## Sidebar settings
settings-sidebar-osc_spatial_headphones = Spatial Headphones

## Common OSC settings
settings-osc-common-network-ports_match_error = The OSC Router in and out ports can't be the same!
settings-osc-common-network-port_banned_error = The port { $port } can't be used!
Expand Down
5 changes: 5 additions & 0 deletions gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { StayAlignedSetup } from './components/onboarding/pages/stay-aligned/Sta
import { TrackingChecklistProvider } from './components/tracking-checklist/TrackingChecklistProvider';
import { HomeScreenSettings } from './components/settings/pages/HomeScreenSettings';
import { ChecklistPage } from './components/tracking-checklist/TrackingChecklist';
import { SpatialHeadphonesOscSettings } from './components/settings/pages/SpatialHeadphonesOscSettings';
import { QuizSlimeSetQuestion } from './components/onboarding/pages/quiz/SlimeSetQuestion';
import { QuizUsageQuestion } from './components/onboarding/pages/quiz/UsageQuestion';
import { QuizRuntimeQuestion } from './components/onboarding/pages/quiz/RuntimeQuestion';
Expand Down Expand Up @@ -142,6 +143,10 @@ function Layout() {
<Route path="osc/router" element={<OSCRouterSettings />} />
<Route path="osc/vrchat" element={<VRCOSCSettings />} />
<Route path="osc/vmc" element={<VMCSettings />} />
<Route
path="osc/spatial-headphones"
element={<SpatialHeadphonesOscSettings />}
/>
<Route path="interface" element={<InterfaceSettings />} />
<Route path="interface/home" element={<HomeScreenSettings />} />
<Route path="advanced" element={<AdvancedSettings />} />
Expand Down
5 changes: 5 additions & 0 deletions gui/src/components/settings/SettingsSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ export function SettingsSidebar() {
scrollTo="vmc"
id="settings-sidebar-osc_vmc"
/>
<SettingsLink
to="/settings/osc/spatial-headphones"
scrollTo="spatialHeadphones"
id="settings-sidebar-osc_spatial_headphones"
/>
</div>
</div>
<div className="flex flex-col gap-3">
Expand Down
187 changes: 187 additions & 0 deletions gui/src/components/settings/pages/SpatialHeadphonesOscSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { Localized, useLocalization } from '@fluent/react';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import {
ChangeSettingsRequestT,
RpcMessage,
SettingsRequestT,
SettingsResponseT,
SpatialHeadphonesOscSettingsT
} from 'solarxr-protocol';
import { useWebsocketAPI } from '@/hooks/websocket-api';
import { CheckBox } from '@/components/commons/Checkbox';
import { RouterIcon } from '@/components/commons/icon/RouterIcon';
import { Input } from '@/components/commons/Input';
import { Typography } from '@/components/commons/Typography';
import {
SettingsPageLayout,
SettingsPagePaneLayout,
} from '@/components/settings/SettingsPageLayout';
import { yupResolver } from '@hookform/resolvers/yup';
import { object } from 'yup';
import {
OSCSettings,
useSpatialHeadphonesOscSettingsValidator,
} from '@/hooks/osc-setting-validator';

interface SpatialHeadphonesOscSettingsForm {
spatialHeadphones: {
oscSettings: OSCSettings;
};
}

const defaultValues = {
spatialHeadphones: {
oscSettings: {
enabled: false,
portOut: 7001,
address: '127.0.0.1',
},
},
};

export function SpatialHeadphonesOscSettings() {
const { l10n } = useLocalization();
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
const { oscValidator } = useSpatialHeadphonesOscSettingsValidator();

const { reset, control, watch, handleSubmit } =
useForm<SpatialHeadphonesOscSettingsForm>({
defaultValues,
reValidateMode: 'onChange',
mode: 'onChange',
resolver: yupResolver(
object({
spatialHeadphones: object({
oscSettings: oscValidator,
}),
}) as any,
),
});

const onSubmit = (values: SpatialHeadphonesOscSettingsForm) => {
const settings = new ChangeSettingsRequestT();

const osc = new SpatialHeadphonesOscSettingsT();
Object.assign(osc, values.spatialHeadphones.oscSettings);
settings.spatialHeadphonesOsc = osc;
sendRPCPacket(RpcMessage.ChangeSettingsRequest, settings);
};

useEffect(() => {
const subscription = watch(() => handleSubmit(onSubmit as any)());
return () => subscription.unsubscribe();
}, []);

useEffect(() => {
sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT());
}, []);

useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => {
const formData: SpatialHeadphonesOscSettingsForm = defaultValues;
if (settings.spatialHeadphonesOsc) {
formData.spatialHeadphones.oscSettings.enabled =
settings.spatialHeadphonesOsc.enabled;
formData.spatialHeadphones.oscSettings.portOut =
settings.spatialHeadphonesOsc.portOut;
if (settings.spatialHeadphonesOsc.address) {
formData.spatialHeadphones.oscSettings.address =
settings.spatialHeadphonesOsc.address.toString();
}
}
reset(formData);
});

return (
<SettingsPageLayout>
<form className="flex flex-col gap-2 w-full">
<SettingsPagePaneLayout icon={<RouterIcon />} id="spatialHeadphones">
<>
<Typography variant="main-title">
{l10n.getString('settings-osc-spatial-headphones')}
</Typography>
<div className="flex flex-col pt-2 pb-4">
<>
{l10n
.getString('settings-osc-spatial-headphones-description')
.split('\n')
.map((line, i) => (
<Typography key={i}>{line}</Typography>
))}
</>
</div>
<Typography variant="section-title">
{l10n.getString('settings-osc-spatial-headphones-enable')}
</Typography>
<div className="flex flex-col pb-2">
<Typography>
{l10n.getString(
'settings-osc-spatial-headphones-enable-description',
)}
</Typography>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<CheckBox
variant="toggle"
outlined
control={control}
name="spatialHeadphones.oscSettings.enabled"
label={l10n.getString(
'settings-osc-spatial-headphones-enable-label',
)}
/>
</div>

<Typography variant="section-title">
{l10n.getString('settings-osc-spatial-headphones-network')}
</Typography>
<div className="flex flex-col pb-2">
<Typography>
{l10n.getString(
'settings-osc-spatial-headphones-network-description',
)}
</Typography>
</div>
<div className="grid grid-cols-2 gap-3 pb-5">
<Localized
id="settings-osc-spatial-headphones-network-port_out"
attrs={{ placeholder: true, label: true }}
>
<Input
type="number"
control={control}
name="spatialHeadphones.oscSettings.portOut"
placeholder="7001"
label=""
/>
</Localized>
</div>
<Typography variant="section-title">
{l10n.getString(
'settings-osc-spatial-headphones-network-address',
)}
</Typography>
<div className="flex flex-col pb-2">
<Typography>
{l10n.getString(
'settings-osc-spatial-headphones-network-address-description',
)}
</Typography>
</div>
<div className="grid gap-3 pb-5">
<Input
type="text"
control={control}
name="spatialHeadphones.oscSettings.address"
placeholder={l10n.getString(
'settings-osc-spatial-headphones-network-address-placeholder',
)}
label=""
/>
</div>
</>
</SettingsPagePaneLayout>
</form>
</SettingsPageLayout>
);
}
30 changes: 30 additions & 0 deletions gui/src/hooks/osc-setting-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export type OSCSettings = {
address: string;
};

export type SpatialHeadphoneOSCSettings = {
enabled: boolean;
portOut: number;
address: string;
};

export function useOscSettingsValidator() {
const bannedPorts = [6969, 21110];
const { l10n } = useLocalization();
Expand Down Expand Up @@ -39,3 +45,27 @@ export function useOscSettingsValidator() {

return { oscValidator };
}
export function useSpatialHeadphonesOscSettingsValidator() {
const bannedPorts = [6969, 21110];
const { l10n } = useLocalization();

return object({
enabled: boolean().required(),

portOut: number()
.typeError(' ')
.required()
.notOneOf(bannedPorts, (ctx) =>
l10n.getString('settings-osc-common-network-port_banned_error', {
port: ctx.originalValue,
})
),

address: string()
.required(' ')
.matches(
/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/i,
{ message: ' ' }
),
});
}
15 changes: 11 additions & 4 deletions server/core/src/main/java/dev/slimevr/VRServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import dev.slimevr.osc.OSCHandler
import dev.slimevr.osc.OSCRouter
import dev.slimevr.osc.VMCHandler
import dev.slimevr.osc.VRCOSCHandler
import dev.slimevr.osc.SpatialHeadphonesOscHandler
import dev.slimevr.posestreamer.BVHRecorder
import dev.slimevr.protocol.ProtocolAPI
import dev.slimevr.protocol.rpc.TransactionInfo
Expand Down Expand Up @@ -76,11 +77,11 @@ class VRServer @JvmOverloads constructor(
private val trackerStatusListeners: MutableList<TrackerStatusListener> = FastList()
private val onTick: MutableList<Runnable> = FastList()
private val lock = acquireMulticastLock()
val oSCRouter: OSCRouter

@JvmField
val vrcOSCHandler: VRCOSCHandler
val vMCHandler: VMCHandler
lateinit var vrcOSCHandler: VRCOSCHandler
lateinit var vMCHandler: VMCHandler
lateinit var spatialHeadphonesOscHandler: SpatialHeadphonesOscHandler
lateinit var oSCRouter: OSCRouter

@JvmField
val deviceManager: DeviceManager
Expand Down Expand Up @@ -172,11 +173,16 @@ class VRServer @JvmOverloads constructor(
humanPoseManager,
configManager.vrConfig.vmc,
)
spatialHeadphonesOscHandler = SpatialHeadphonesOscHandler(
this,
configManager.vrConfig.spatialHeadphonesOsc,
)

// Initialize OSC router
val oscHandlers = FastList<OSCHandler>()
oscHandlers.add(vrcOSCHandler)
oscHandlers.add(vMCHandler)
oscHandlers.add(spatialHeadphonesOscHandler)
oSCRouter = OSCRouter(configManager.vrConfig.oscRouter, oscHandlers)
bvhRecorder = BVHRecorder(this)
for (tracker in computedTrackers) {
Expand Down Expand Up @@ -263,6 +269,7 @@ class VRServer @JvmOverloads constructor(
}
vrcOSCHandler.update()
vMCHandler.update()
spatialHeadphonesOscHandler.update()
// final long time = System.currentTimeMillis() - start;
try {
sleep(1) // 1000Hz
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dev.slimevr.config

class SpatialHeadphonesOscConfig {
// Are the OSC receiver and sender enabled?
var enabled = false

// Port to send out OSC messages at
var portOut = 7001

// Address to send out OSC messages at
var address = "127.0.0.1"
}
5 changes: 5 additions & 0 deletions server/core/src/main/java/dev/slimevr/config/VRConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class VRConfig {
@get:JvmName("getVMC")
val vmc: VMCConfig = VMCConfig()

val spatialHeadphonesOsc: SpatialHeadphonesOscConfig = SpatialHeadphonesOscConfig()

val autoBone: AutoBoneConfig = AutoBoneConfig()

val keybindings: KeybindingsConfig = KeybindingsConfig()
Expand Down Expand Up @@ -87,6 +89,9 @@ class VRConfig {
// Initialize default settings for VMC
vmc.portIn = 39540
vmc.portOut = 39539

// Initialize default settings for Spatial Headphones OSC
spatialHeadphonesOsc.portOut = 7001
}

fun getTrackers(): Map<String, TrackerConfig> = trackers
Expand Down
Loading