diff --git a/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs b/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs index 0607505..eba4e8c 100644 --- a/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs +++ b/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs @@ -583,7 +583,7 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable _lastSoundTime = DateTime.Now; _lastSpeechTime = DateTime.Now; // 初始化最后说话时间 _silenceTimer = new Timer(CheckSilence, recordingCompletionSource, 1000, 1000); - + // 启动风铃提示计时器 _windChimeTimer = new Timer(CheckWindChimePrompt, null, 1000, 1000); @@ -871,12 +871,12 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable _silenceTimer?.Change(Timeout.Infinite, Timeout.Infinite); _silenceTimer?.Dispose(); _silenceTimer = null; - + // 停止风铃提示音计时器 _windChimeTimer?.Change(Timeout.Infinite, Timeout.Infinite); _windChimeTimer?.Dispose(); _windChimeTimer = null; - + // 停止风铃提示音 _ = StopWindChime(false); @@ -946,23 +946,23 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable } if (DateTime.Now.Subtract(_lasHasoudDateTime).TotalSeconds > 1) { - Console.WriteLine($"当前音量:{maxVolume},设置的音量灵敏度:{_config.Recording.SilenceThreshold},是否已监测到在说话中:{_isSpeaking}"); + Console.WriteLine($"当前音量:{maxVolume},设置的音量灵敏度:{_config.Recording.SilenceThreshold},是否已监测到在说话中:{_isSpeaking},风铃提示音:{_windChimeDevice?.Volume ?? 0},bgm:{_backgroundMusicDevice?.Volume ?? 0}"); _lasHasoudDateTime = DateTime.Now; _isSpeaking = false; } - + // 如果监测到说话,更新最后说话时间 if (isSpeaking) { _lastSpeechTime = DateTime.Now; - + // 如果风铃声正在播放,平滑停止它 if (_isWindChimePlaying) { _ = StopWindChime(true); // 平滑淡出 } } - + // 如果最大音量超过阈值,则认为有声音 return isSpeaking; } @@ -979,7 +979,7 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable // 确保背景音乐被停止 StopBackgroundMusic(); - + // 确保风铃提示音被停止 _ = StopWindChime(false); @@ -1304,7 +1304,7 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable if (_keyToneDevices.TryAdd(digit, (device, reader))) { - + device.Play(); // 确保至少播放最小时长 @@ -1361,7 +1361,7 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable { device = new WaveOutEvent(); reader = new AudioFileReader(audioPath); - + // 如果没有指定音量,根据音频文件类型设置音量 if (volume == null) { @@ -1396,9 +1396,9 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable { device.Volume = Math.Clamp(volume.Value, 0.0f, 1.0f); } - + device.Init(reader); - + // 如果需要循环播放,创建一个循环播放的任务 if (loop && maxDuration.HasValue) { @@ -1519,14 +1519,15 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable _backgroundMusicDevice = new WaveOutEvent(); _backgroundMusicReader = new AudioFileReader(musicFilePath); - // 设置音量(默认为10%) - _backgroundMusicReader.Volume = _config.Recording.BackgroundMusicVolume; - + // 初始化设备 _backgroundMusicDevice.Init(_backgroundMusicReader); + // 设置音量到设备而非Reader对象,确保独立控制(默认为10%) + _backgroundMusicDevice.Volume = Math.Clamp(_config.Recording.BackgroundMusicVolume, 0.0f, 1.0f); + // 标记背景音乐开始播放 _isBackgroundMusicPlaying = true; - Console.WriteLine($"开始播放背景音乐,音量: {_backgroundMusicReader.Volume * 100}%"); + Console.WriteLine($"开始播放背景音乐,音量: {_backgroundMusicDevice.Volume * 100}%"); // 循环播放背景音乐 while (!_backgroundMusicCts.Token.IsCancellationRequested) @@ -1538,10 +1539,12 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable { _backgroundMusicReader.Position = 0; } - + // 设置音量到设备而非Reader对象,确保独立控制(默认为10%) + _backgroundMusicDevice.Volume = Math.Clamp(_config.Recording.BackgroundMusicVolume, 0.0f, 1.0f); // 如果没有在播放,则开始播放 if (_backgroundMusicDevice.PlaybackState != PlaybackState.Playing) { + _backgroundMusicDevice.Play(); } @@ -1625,8 +1628,8 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable double secondsSinceLastSpeech = (DateTime.Now - _lastSpeechTime).TotalSeconds; // 如果超过风铃提示时间但未达到挂断时间,并且风铃没在播放,开始播放风铃 - if (secondsSinceLastSpeech >= windChimePromptSeconds && - secondsSinceLastSpeech < silenceTimeoutSeconds && + if (secondsSinceLastSpeech >= windChimePromptSeconds && + secondsSinceLastSpeech < silenceTimeoutSeconds && !_isWindChimePlaying) { Console.WriteLine($"检测到{windChimePromptSeconds}秒无声音,播放风铃提示音..."); @@ -1670,14 +1673,17 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable return; } - // 初始化播放设备 + // 初始化播放设备(创建完全独立的音频流) _windChimeDevice = new WaveOutEvent(); _windChimeReader = new AudioFileReader(windChimeFilePath); _windChimeDevice.Init(_windChimeReader); - _windChimeDevice.Volume = Math.Clamp(1.0f, 0.0f, 1.0f); // 初始音量设为最大 + + // 设置初始音量 + float initialVolume = 0.8f; // 使用稍低于最大的音量,避免音量混合问题 + _windChimeDevice.Volume = Math.Clamp(initialVolume, 0.0f, 1.0f); _isWindChimePlaying = true; - Console.WriteLine("开始播放风铃提示音"); + Console.WriteLine($"开始播放风铃提示音,初始音量: {_windChimeDevice.Volume * 100}%"); // 开始播放 _windChimeDevice.Play(); @@ -1696,13 +1702,20 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable await StopWindChime(true); // 平滑淡出 break; } - + // 如果到达音频文件末尾,循环播放 if (_windChimeReader != null && _windChimeReader.Position >= _windChimeReader.Length) { _windChimeReader.Position = 0; } + // 确保背景音乐音量不受风铃音量影响 + if (_isBackgroundMusicPlaying && _backgroundMusicDevice != null) + { + // 重新应用背景音乐的原始音量设置 + _backgroundMusicDevice.Volume = Math.Clamp(_config.Recording.BackgroundMusicVolume, 0.0f, 1.0f); + } + await Task.Delay(100, token); } } @@ -1754,13 +1767,20 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable float ratio = 1.0f - ((float)i / steps); _windChimeDevice.Volume = startVolume * ratio; + + // 确保背景音乐音量不受影响 + if (_isBackgroundMusicPlaying && _backgroundMusicDevice != null) + { + _backgroundMusicDevice.Volume = Math.Clamp(_config.Recording.BackgroundMusicVolume, 0.0f, 1.0f); + } + await Task.Delay(stepDelay); } } // 正式停止 _windChimeCts?.Cancel(); - + if (_windChimeDevice != null) { _windChimeDevice.Stop(); @@ -1776,6 +1796,12 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable _isWindChimePlaying = false; Console.WriteLine("风铃提示音已停止"); + + // 再次确保背景音乐音量恢复正常 + if (_isBackgroundMusicPlaying && _backgroundMusicDevice != null) + { + _backgroundMusicDevice.Volume = Math.Clamp(_config.Recording.BackgroundMusicVolume, 0.0f, 1.0f); + } } catch (Exception ex) { diff --git a/ShengShengBuXi.ConsoleApp/config.json b/ShengShengBuXi.ConsoleApp/config.json index 6f9867b..31e172c 100644 --- a/ShengShengBuXi.ConsoleApp/config.json +++ b/ShengShengBuXi.ConsoleApp/config.json @@ -26,12 +26,12 @@ "Channels": 1, "BufferMilliseconds": 50, "SilenceThreshold": 0.1, - "SilenceTimeoutSeconds": 15, + "SilenceTimeoutSeconds": 60, "AllowUserHangup": true, "UploadRecordingToServer": false, "EnableBackgroundMusic": true, "BackgroundMusicFile": "bj.mp3", - "BackgroundMusicVolume": 0.3, + "BackgroundMusicVolume": 0.15, "WindChimePromptSeconds": 7.5, "WindChimeFadeOutMs": 3000 }, diff --git a/ShengShengBuXi/Pages/Index.cshtml b/ShengShengBuXi/Pages/Index.cshtml index e58f9aa..8f88435 100644 --- a/ShengShengBuXi/Pages/Index.cshtml +++ b/ShengShengBuXi/Pages/Index.cshtml @@ -164,7 +164,7 @@ let currentSentenceIndex = 0; // 当前句子索引 let isAnimating = false; // 动画状态标志 // 设置总页数为1000页,实际上是循环使用有限的内容 - var numberOfPages = 11; + var numberOfPages = 1000; /** * 检查文字容器是否需要翻页 @@ -202,11 +202,6 @@ if (isAnimating) return; isAnimating = true; - // 获取渐变相关配置 - const fadeChars = CONFIG.rightContainer.fadeChars || 4; // 渐变字符数量 - const fadeStepTime = CONFIG.rightContainer.fadeStepTime || 500; // 过渡时间 - const fadeDelayFactor = CONFIG.rightContainer.fadeDelayFactor || 0.25; // 透明度递减因子 - // 清空容器并设置初始可见度 $(selector).html('').css('opacity', 1); @@ -215,8 +210,7 @@ const charSpan = $('').text(text.charAt(i)); charSpan.css({ 'opacity': 0, - 'display': 'inline-block', // 确保每个字符是独立的块 - 'transition': `opacity ${fadeStepTime}ms ease-in-out` // 平滑的透明度过渡效果 + 'display': 'inline-block' // 确保每个字符是独立的块 }); $(selector).append(charSpan); } @@ -225,68 +219,57 @@ const chars = $(selector).find('span'); const totalChars = chars.length; - // 设置初始不透明度,形成从左到右的渐变效果 - // 左侧字符较深,右侧字符较浅 - const setInitialOpacity = () => { - for (let i = 0; i < totalChars; i++) { - // 计算不透明度:第一个字符不透明度最高,往后逐渐降低 - let position = i / totalChars; // 字符在文本中的相对位置 (0-1) - let opacity = Math.max(0, 0.8 - (position * fadeChars * 0.2)); - - // 超出可见范围的字符完全透明 - if (i >= fadeChars) { - opacity = 0; - } - - // 设置不透明度 - $(chars[i]).css('opacity', opacity); - } - }; - // 执行渐变动画 const animateText = () => { - let progress = 0; + // 确保speed值是合理的,至少有2秒用于总体动画 + const totalDuration = Math.max(2000, speed); - const step = () => { - if (progress >= 1) { - // 完成动画 - chars.css('opacity', 1); - isAnimating = false; - if (callback) callback(); - - // 等待随机4-7秒后请求下一条文本 - const delay = Math.floor(Math.random() * 3000) + 4000; // 4000-7000毫秒 - console.log(`文本显示完成,将在${delay/1000}秒后请求下一条文本`); - setTimeout(requestNextText, delay); + // 每个字符显示的间隔时间 + const charInterval = Math.max(100, totalDuration / totalChars / 3); + console.log(charInterval, totalDuration / totalChars / 3); + // 单个字符的动画时间,要足够长以观察到渐变效果 + const charAnimDuration = 1500; // 固定为1.5秒,确保足够慢以观察到渐变 + + // 逐个字符渐显 + function animateChar(index) { + if (index >= totalChars) { + // 所有字符都已经开始动画 + setTimeout(() => { + isAnimating = false; + if (callback) callback(); + + // 等待随机4-7秒后请求下一条文本 + const delay = Math.floor(Math.random() * 3000) + 3000; // 4000-6000毫秒 + console.log(`文本显示完成,将在${delay/1000}秒后请求下一条文本`); + setTimeout(requestNextText, delay); + }, charAnimDuration); // 确保所有动画完成 return; } - // 更新每个字符的透明度 - for (let i = 0; i < totalChars; i++) { - let targetOpacity = 1; // 目标不透明度(完全显示) - let currentPosition = i / totalChars; // 字符的相对位置 - - // 计算当前动画进度下该位置的字符应有的透明度 - // 小于当前进度的字符应该更不透明 - let opacity = targetOpacity * Math.max(0, 1 - Math.max(0, (currentPosition - progress) * 3)); - - // 设置不透明度 - $(chars[i]).css('opacity', opacity); - } + // 获取当前字符 + const char = $(chars[index]); - // 增加进度 - progress += 0.02; + // 使用jQuery的animate实现平滑的从0到1的渐变 + char.animate( + { opacity: 1 }, + charAnimDuration, // 固定的较长动画时间 + 'swing', // 使用jQuery内置的swing缓动函数 + function() { + // 动画完成回调(可选) + } + ); - // 继续下一步动画 - setTimeout(step, speed / 50); - }; + // 安排下一个字符的渐变 + setTimeout(() => { + animateChar(index + 1); + }, charInterval); + } - // 开始动画 - step(); + // 开始第一个字符的渐变 + animateChar(0); }; - // 设置初始不透明度并开始动画 - setInitialOpacity(); + // 开始动画 setTimeout(animateText, 50); } @@ -395,6 +378,8 @@ // SignalR连接变量 let hubConnection; + let reconnectAttempts = 0; // 记录重连尝试次数 + const MAX_RECONNECT_ATTEMPTS = 5; // 最大重连尝试次数 // 初始化SignalR连接 function initializeSignalRConnection() { @@ -404,6 +389,21 @@ .withAutomaticReconnect() .build(); + // 注册重连事件处理 + hubConnection.onreconnecting(error => { + console.log("SignalR重新连接中: " + (error ? error.message : "未知错误")); + reconnectAttempts++; // 增加重连尝试次数 + console.log(`重连尝试次数: ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`); + }); + + hubConnection.onreconnected(connectionId => { + console.log("SignalR已重新连接,ID: " + connectionId); + // 重连成功,重置尝试次数 + reconnectAttempts = 0; + // 重连成功后重新注册客户端 + registerAsDisplayClient(); + }); + // 接收显示文本的处理函数 hubConnection.on("ReceiveDisplayText", function (text) { console.log("收到显示文本:", text); @@ -424,46 +424,100 @@ }); // 启动连接 - hubConnection.start() - .then(function () { - console.log("SignalR连接成功"); - // 注册为显示客户端 - hubConnection.invoke("RegisterClient", 3, "Display") - .then(function() { - console.log("注册为显示客户端成功"); - // 获取显示配置 - return hubConnection.invoke("GetDisplayConfig"); - }) - .then(function(configJson) { - if(configJson) { - console.log("已获取显示配置"); - try { - const config = JSON.parse(configJson); - updateConfig(config); - } catch (error) { - console.error("解析显示配置失败:", error); - } - } - // 开始请求第一条显示文本 - requestNextText(); - }) - .catch(function(err) { - console.error("客户端操作失败:", err); - }); - }) - .catch(function (err) { - console.error("SignalR连接失败:", err); - // 5秒后重试 - setTimeout(initializeSignalRConnection, 5000); - }); + startConnection(); // 连接关闭的处理 hubConnection.onclose(function() { console.log("SignalR连接已关闭,尝试重新连接..."); - setTimeout(initializeSignalRConnection, 5000); + reconnectAttempts++; // 增加失败计数 + console.log(`关闭后重连尝试次数: ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`); + + // 检查是否达到最大尝试次数 + if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { + console.error("达到最大重连次数,刷新页面..."); + // 显示刷新提示 + if (!document.getElementById('reconnect-alert')) { + const alert = document.createElement('div'); + alert.id = 'reconnect-alert'; + alert.style.cssText = 'position:fixed;top:10px;left:50%;transform:translateX(-50%);background:rgba(255,0,0,0.7);color:white;padding:10px;border-radius:5px;z-index:9999;'; + alert.innerHTML = '连接服务器失败,3秒后自动刷新页面...'; + document.body.appendChild(alert); + } + // 3秒后刷新页面 + setTimeout(() => { + window.location.reload(); + }, 3000); + return; + } + + setTimeout(startConnection, 5000); }); } + // 启动连接函数 + function startConnection() { + hubConnection.start() + .then(function () { + console.log("SignalR连接成功"); + // 连接成功,重置尝试次数 + reconnectAttempts = 0; + // 注册为显示客户端 + registerAsDisplayClient(); + }) + .catch(function (err) { + console.error("SignalR连接失败:", err); + reconnectAttempts++; // 增加失败计数 + console.log(`连接尝试次数: ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`); + + // 检查是否达到最大尝试次数 + if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { + console.error("达到最大重连次数,刷新页面..."); + // 显示刷新提示 + if (!document.getElementById('reconnect-alert')) { + const alert = document.createElement('div'); + alert.id = 'reconnect-alert'; + alert.style.cssText = 'position:fixed;top:10px;left:50%;transform:translateX(-50%);background:rgba(255,0,0,0.7);color:white;padding:10px;border-radius:5px;z-index:9999;'; + alert.innerHTML = '连接服务器失败,3秒后自动刷新页面...'; + document.body.appendChild(alert); + } + // 3秒后刷新页面 + setTimeout(() => { + window.location.reload(); + }, 3000); + return; + } + + // 5秒后重试 + setTimeout(startConnection, 5000); + }); + } + + // 注册为显示客户端 + function registerAsDisplayClient() { + hubConnection.invoke("RegisterClient", 3, "Display") + .then(function() { + console.log("注册为显示客户端成功"); + // 获取显示配置 + return hubConnection.invoke("GetDisplayConfig"); + }) + .then(function(configJson) { + if(configJson) { + console.log("已获取显示配置"); + try { + const config = JSON.parse(configJson); + updateConfig(config); + } catch (error) { + console.error("解析显示配置失败:", error); + } + } + // 开始请求第一条显示文本 + requestNextText(); + }) + .catch(function(err) { + console.error("客户端操作失败:", err); + }); + } + // 更新配置 function updateConfig(newConfig) { // 更新左侧容器配置 @@ -702,17 +756,35 @@ * 请求下一条文本 */ function requestNextText() { - if (hubConnection && hubConnection.state === signalR.HubConnectionState.Connected) { + if (!hubConnection) { + console.warn("SignalR连接对象不存在,正在初始化连接..."); + initializeSignalRConnection(); + return; + } + + if (hubConnection.state === signalR.HubConnectionState.Connected) { console.log("正在向服务器请求下一条文本..."); hubConnection.invoke("GetNextDisplayText") .catch(function(err) { console.error("请求下一条文本失败:", err); - // 如果请求失败,稍后重试 - setTimeout(requestNextText, 5000); + // 检查是否是因为连接断开导致的错误 + if (hubConnection.state !== signalR.HubConnectionState.Connected) { + console.warn("连接已断开,尝试重新连接后请求文本"); + // 如果请求失败是因为连接已断开,先尝试重连 + startConnection(); + } else { + // 其他错误,稍后重试 + setTimeout(requestNextText, 5000); + } }); + } else if (hubConnection.state === signalR.HubConnectionState.Reconnecting) { + console.warn("SignalR正在重连中,等待重连完成后自动请求文本"); + // 不需额外操作,重连成功后会自动请求文本 } else { - console.warn("SignalR连接未就绪,无法请求下一条文本"); - // 如果连接未就绪,稍后重试 + console.warn("SignalR连接未就绪,状态:", hubConnection.state); + // 尝试重新连接 + startConnection(); + // 3秒后重试 setTimeout(requestNextText, 3000); } } diff --git a/ShengShengBuXi/config/display.json b/ShengShengBuXi/config/display.json index 4c90e81..7590a0b 100644 --- a/ShengShengBuXi/config/display.json +++ b/ShengShengBuXi/config/display.json @@ -8,9 +8,9 @@ "fontSize": "40px", "fontWeight": "bolder", "fontStyle": "italic", - "typewriterSpeed": 6000, + "typewriterSpeed": 18000, "fadeChars": 1, - "fadeStepTime": 1000, + "fadeStepTime": 3000, "fadeDelayFactor": 0.20 }, "waterEffect": {