feat: 增加扬声器音量控制

This commit is contained in:
zhanghao 2026-07-22 14:01:38 +08:00
parent 974b4ed736
commit a3ca31a1e0
3 changed files with 167 additions and 24 deletions

View File

@ -8,6 +8,7 @@ import {
createAGVGrpcClient, createAGVGrpcClient,
createARMGrpcClient, createARMGrpcClient,
createMicrophoneGrpcClient, createMicrophoneGrpcClient,
createSpeakerGrpcClient,
__dirname __dirname
} from './grpcClient.js'; } from './grpcClient.js';
import { attachAudioWebSocket } from './audioWebSocket.js'; import { attachAudioWebSocket } from './audioWebSocket.js';
@ -83,6 +84,72 @@ app.post('/api/audio/microphone/volume/set', (req, res) => {
}) })
}) })
// 获取机器人扬声器音量0-100
app.post('/api/audio/speaker/volume/get', (req, res) => {
const { ip, deviceId } = req.body
if (!ip || !deviceId) {
res.status(400).json({ code: 400, message: '机器人地址和扬声器设备 ID 不能为空' })
return
}
const grpcClient = createSpeakerGrpcClient(ip)
grpcClient.getVolume(
{ header: { device_id: deviceId } },
{ deadline: Date.now() + 5000 },
(error, feedback) => {
grpcClient.close()
if (error) {
console.error('[audio] get speaker volume failed:', error)
res.status(502).json({ code: 502, message: error.details || error.message || '获取扬声器音量失败' })
return
}
if (feedback?.header?.success === false) {
res.status(502).json({
code: 502,
message: feedback.header.error_message || '机器人拒绝获取扬声器音量'
})
return
}
res.json({ code: 200, data: { volume: feedback?.volume ?? 0 } })
}
)
})
// 设置机器人扬声器音量0-100
app.post('/api/audio/speaker/volume/set', (req, res) => {
const { ip, deviceId } = req.body
const volume = Number(req.body.volume)
if (!ip || !deviceId) {
res.status(400).json({ code: 400, message: '机器人地址和扬声器设备 ID 不能为空' })
return
}
if (!Number.isInteger(volume) || volume < 0 || volume > 100) {
res.status(400).json({ code: 400, message: '音量必须是 0 到 100 的整数' })
return
}
const grpcClient = createSpeakerGrpcClient(ip)
grpcClient.setVolume({
header: { device_id: deviceId },
volume
}, { deadline: Date.now() + 5000 }, (error, feedback) => {
grpcClient.close()
if (error) {
console.error('[audio] set speaker volume failed:', error)
res.status(502).json({ code: 502, message: error.details || error.message || '设置扬声器音量失败' })
return
}
if (feedback?.header?.success === false) {
res.status(502).json({
code: 502,
message: feedback.header.error_message || '机器人拒绝设置扬声器音量'
})
return
}
res.json({ code: 200, data: { volume } })
})
})
// 获取agv运行时状态 // 获取agv运行时状态
app.post('/api/agv/getRuntimeState', async (req, res) => { app.post('/api/agv/getRuntimeState', async (req, res) => {
const request = { const request = {

View File

@ -7,3 +7,11 @@ export function getMicrophoneVolume(params) {
export function setMicrophoneVolume(params) { export function setMicrophoneVolume(params) {
return request('/api/audio/microphone/volume/set', params, 'POST') return request('/api/audio/microphone/volume/set', params, 'POST')
} }
export function getSpeakerVolume(params) {
return request('/api/audio/speaker/volume/get', params, 'POST')
}
export function setSpeakerVolume(params) {
return request('/api/audio/speaker/volume/set', params, 'POST')
}

View File

@ -55,15 +55,29 @@
<div class="robot-volume"> <div class="robot-volume">
<span class="robot-volume__label">机器人麦克风音量</span> <span class="robot-volume__label">机器人麦克风音量</span>
<el-slider <el-slider
v-model="robotVolume" v-model="microphoneVolume"
class="robot-volume__slider" class="robot-volume__slider"
:min="0" :min="0"
:max="100" :max="100"
:disabled="isVolumeLoading" :disabled="isMicrophoneVolumeLoading"
:show-tooltip="true" :show-tooltip="true"
@change="updateRobotVolume" @change="updateMicrophoneVolume"
/> />
<span class="robot-volume__value">{{ robotVolume }}%</span> <span class="robot-volume__value">{{ microphoneVolume }}%</span>
</div>
<div class="robot-volume">
<span class="robot-volume__label">机器人扬声器音量</span>
<el-slider
v-model="speakerVolume"
class="robot-volume__slider"
:min="0"
:max="100"
:disabled="isSpeakerVolumeLoading"
:show-tooltip="true"
@change="updateSpeakerVolume"
/>
<span class="robot-volume__value">{{ speakerVolume }}%</span>
</div> </div>
<section class="status-card"> <section class="status-card">
@ -87,7 +101,12 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { ElMessage, ElNotification } from 'element-plus' import { ElMessage, ElNotification } from 'element-plus'
import { useRobotStore } from '@/stores/robot' import { useRobotStore } from '@/stores/robot'
import { getMicrophoneVolume, setMicrophoneVolume } from '@/api/audio' import {
getMicrophoneVolume,
getSpeakerVolume,
setMicrophoneVolume,
setSpeakerVolume
} from '@/api/audio'
// 48kHz 48kHz // 48kHz 48kHz
const TARGET_SAMPLE_RATE = 48000 const TARGET_SAMPLE_RATE = 48000
@ -101,9 +120,12 @@ const isStarting = ref(false)
const isMuted = ref(false) const isMuted = ref(false)
const connectionStatus = ref('idle') const connectionStatus = ref('idle')
const errorMessage = ref('') const errorMessage = ref('')
const robotVolume = ref(50) const microphoneVolume = ref(50)
const isVolumeLoading = ref(false) const speakerVolume = ref(50)
let confirmedRobotVolume = 50 const isMicrophoneVolumeLoading = ref(false)
const isSpeakerVolumeLoading = ref(false)
let confirmedMicrophoneVolume = 50
let confirmedSpeakerVolume = 50
// //
const localStream = ref(null) const localStream = ref(null)
@ -128,46 +150,89 @@ const statusMeta = computed(() => {
return statusMap[connectionStatus.value] || statusMap.idle return statusMap[connectionStatus.value] || statusMap.idle
}) })
function volumeRequestParams() { function microphoneVolumeRequestParams() {
return { return {
ip: robotStore.ip, ip: robotStore.ip,
deviceId: robotStore.micDeviceId deviceId: robotStore.micDeviceId
} }
} }
async function loadRobotVolume({ silent = false } = {}) { async function loadMicrophoneVolume({ silent = false } = {}) {
if (!robotStore.ip || !robotStore.micDeviceId) return if (!robotStore.ip || !robotStore.micDeviceId) return
isVolumeLoading.value = true isMicrophoneVolumeLoading.value = true
try { try {
const response = await getMicrophoneVolume(volumeRequestParams()) const response = await getMicrophoneVolume(microphoneVolumeRequestParams())
if (response.code !== 200) throw new Error(response.message || '获取机器人音量失败') if (response.code !== 200) throw new Error(response.message || '获取机器人音量失败')
const volume = Math.max(0, Math.min(100, Number(response.data?.volume) || 0)) const volume = Math.max(0, Math.min(100, Number(response.data?.volume) || 0))
robotVolume.value = volume microphoneVolume.value = volume
confirmedRobotVolume = volume confirmedMicrophoneVolume = volume
} catch (error) { } catch (error) {
if (!silent) ElMessage.error(error.message || '获取机器人音量失败') if (!silent) ElMessage.error(error.message || '获取机器人音量失败')
} finally { } finally {
isVolumeLoading.value = false isMicrophoneVolumeLoading.value = false
} }
} }
async function updateRobotVolume(volume) { async function updateMicrophoneVolume(volume) {
const previousVolume = confirmedRobotVolume const previousVolume = confirmedMicrophoneVolume
isVolumeLoading.value = true isMicrophoneVolumeLoading.value = true
try { try {
const response = await setMicrophoneVolume({ const response = await setMicrophoneVolume({
...volumeRequestParams(), ...microphoneVolumeRequestParams(),
volume: Math.round(volume) volume: Math.round(volume)
}) })
if (response.code !== 200) throw new Error(response.message || '设置机器人音量失败') if (response.code !== 200) throw new Error(response.message || '设置机器人音量失败')
robotVolume.value = response.data.volume microphoneVolume.value = response.data.volume
confirmedRobotVolume = response.data.volume confirmedMicrophoneVolume = response.data.volume
ElMessage.success(`机器人麦克风音量已设置为 ${response.data.volume}%`) ElMessage.success(`机器人麦克风音量已设置为 ${response.data.volume}%`)
} catch (error) { } catch (error) {
robotVolume.value = previousVolume microphoneVolume.value = previousVolume
ElMessage.error(error.message || '设置机器人音量失败') ElMessage.error(error.message || '设置机器人音量失败')
} finally { } finally {
isVolumeLoading.value = false isMicrophoneVolumeLoading.value = false
}
}
function speakerVolumeRequestParams() {
return {
ip: robotStore.ip,
deviceId: robotStore.spkDeviceId
}
}
async function loadSpeakerVolume({ silent = false } = {}) {
if (!robotStore.ip || !robotStore.spkDeviceId) return
isSpeakerVolumeLoading.value = true
try {
const response = await getSpeakerVolume(speakerVolumeRequestParams())
if (response.code !== 200) throw new Error(response.message || '获取扬声器音量失败')
const volume = Math.max(0, Math.min(100, Number(response.data?.volume) || 0))
speakerVolume.value = volume
confirmedSpeakerVolume = volume
} catch (error) {
if (!silent) ElMessage.error(error.message || '获取扬声器音量失败')
} finally {
isSpeakerVolumeLoading.value = false
}
}
async function updateSpeakerVolume(volume) {
const previousVolume = confirmedSpeakerVolume
isSpeakerVolumeLoading.value = true
try {
const response = await setSpeakerVolume({
...speakerVolumeRequestParams(),
volume: Math.round(volume)
})
if (response.code !== 200) throw new Error(response.message || '设置扬声器音量失败')
speakerVolume.value = response.data.volume
confirmedSpeakerVolume = response.data.volume
ElMessage.success(`机器人扬声器音量已设置为 ${response.data.volume}%`)
} catch (error) {
speakerVolume.value = previousVolume
ElMessage.error(error.message || '设置扬声器音量失败')
} finally {
isSpeakerVolumeLoading.value = false
} }
} }
@ -472,7 +537,10 @@ function endCall() {
} }
// AudioWorklet WebSocket // AudioWorklet WebSocket
onMounted(() => loadRobotVolume()) onMounted(() => {
loadMicrophoneVolume()
loadSpeakerVolume()
})
onBeforeUnmount(cleanupResources) onBeforeUnmount(cleanupResources)
</script> </script>