diff --git a/musicBackend/src/main/java/com/cph/musicbackend/controller/MusicController.java b/musicBackend/src/main/java/com/cph/musicbackend/controller/MusicController.java index a0ef282..b481864 100644 --- a/musicBackend/src/main/java/com/cph/musicbackend/controller/MusicController.java +++ b/musicBackend/src/main/java/com/cph/musicbackend/controller/MusicController.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @RestController @@ -101,6 +102,7 @@ public Object like(@RequestBody Music song) { } @PostMapping("/api/uploadAudio") + @RecognizeAddress public Object recongnizeMusic(@RequestParam("audio") MultipartFile file) { if (file.isEmpty()) { return "{\"error\": \"请选择一个文件上传\"}"; @@ -116,7 +118,9 @@ public Object recongnizeMusic(@RequestParam("audio") MultipartFile file) { // 保存文件 File destFile = new File(dir.getAbsolutePath() + File.separator + fileName); file.transferTo(destFile); - return acrCloudUtil.recongizeByFile(dir.getAbsolutePath() + File.separator + fileName); + Map resultMap = acrCloudUtil.recongizeByFile(dir.getAbsolutePath() + File.separator + fileName); + musicMapper.insert(new Music().setArtist(resultMap.get("artist")).setTitle(resultMap.get("title"))); + return resultMap; // return MusicRecUtil.recongnizeFile(dir.getAbsolutePath() + File.separator + fileName); } catch (IOException e) { diff --git a/musicBackend/src/main/java/com/cph/musicbackend/entity/Music.java b/musicBackend/src/main/java/com/cph/musicbackend/entity/Music.java index 0fe1306..2bad33f 100644 --- a/musicBackend/src/main/java/com/cph/musicbackend/entity/Music.java +++ b/musicBackend/src/main/java/com/cph/musicbackend/entity/Music.java @@ -4,10 +4,12 @@ import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; +import lombok.experimental.Accessors; import java.util.Date; @Data +@Accessors(chain =true) public class Music { @TableId(type = IdType.AUTO, value = "id") diff --git a/musicBackend/src/main/java/com/cph/musicbackend/rd3/AcrCloudUtil.java b/musicBackend/src/main/java/com/cph/musicbackend/rd3/AcrCloudUtil.java index b97b690..741b596 100644 --- a/musicBackend/src/main/java/com/cph/musicbackend/rd3/AcrCloudUtil.java +++ b/musicBackend/src/main/java/com/cph/musicbackend/rd3/AcrCloudUtil.java @@ -22,7 +22,6 @@ public Map recongizeByFile(String filename) { HashMap resultMap = new HashMap<>(); String result = re.recognizeByFile(filename, 1); // 使用示例结果进行解析 -// String result = "{\"status\":{\"msg\":\"Success\",\"version\":\"1.0\",\"code\":0},\"metadata\":{\"timestamp_utc\":\"2024-10-08 12:08:34\",\"music\":[{\"lan\":\"国语\",\"duration_ms\":200000,\"external_ids\":{},\"db_begin_time_offset_ms\":4080,\"artists\":[{\"name\":\"周杰伦\"}],\"db_end_time_offset_ms\":14600,\"sample_begin_time_offset_ms\":0,\"sample_end_time_offset_ms\":10520,\"play_offset_ms\":15540,\"result_from\":3,\"title\":\"前世情人\",\"label\":\"JVR\",\"score\":100,\"acrid\":\"e3c887cf408db8991a68a843f1afd5a0\",\"language\":\"zh\",\"external_metadata\":{},\"release_date\":\"2016-06-24\",\"album\":{\"name\":\"周杰伦的床边故事\"}},{\"duration_ms\":200460,\"external_ids\":{},\"db_begin_time_offset_ms\":4060,\"artists\":[{\"name\":\"周杰伦\"}],\"label\":\"杰威尔音乐有限公司\",\"db_end_time_offset_ms\":14500,\"sample_begin_time_offset_ms\":0,\"sample_end_time_offset_ms\":10440,\"play_offset_ms\":15520,\"result_from\":3,\"title\":\"前世情人\",\"score\":100,\"language\":\"zh\",\"acrid\":\"0eb1c19384ec445e15c6cb3c11c706ba\",\"release_date\":\"2016-06-24\",\"external_metadata\":{},\"album\":{\"name\":\"周杰伦的床边故事\"}}]},\"result_type\":0,\"cost_time\":0.029999971389771}"; System.out.println(result); try { diff --git a/musicBackend/src/main/resources/application.yaml b/musicBackend/src/main/resources/application.yaml index 2f2a78f..dfd9c42 100644 --- a/musicBackend/src/main/resources/application.yaml +++ b/musicBackend/src/main/resources/application.yaml @@ -33,15 +33,15 @@ mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:mybatis/*.xml - -file: - upload: - path: /root/nginx/share/nginx/media/ - url: https://app102.acapp.acwing.com.cn/media/ +# #file: # upload: -# path: D://audio// +# path: /root/nginx/share/nginx/media/ # url: https://app102.acapp.acwing.com.cn/media/ +file: + upload: + path: D://audio// + url: https://app102.acapp.acwing.com.cn/media/ logging: level: diff --git a/package-lock.json b/package-lock.json index 4ade850..df8d362 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.7.7", + "lamejs": "^1.2.1", "react": "^18.3.1", "react-audio-player": "^0.17.0", "react-dom": "^18.3.1", @@ -12445,6 +12446,14 @@ "node": ">= 8" } }, + "node_modules/lamejs": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/lamejs/-/lamejs-1.2.1.tgz", + "integrity": "sha512-s7bxvjvYthw6oPLCm5pFxvA84wUROODB8jEO2+CE1adhKgrIvVOlmMgY8zyugxGrvRaDHNJanOiS21/emty6dQ==", + "dependencies": { + "use-strict": "1.0.1" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmmirror.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -17578,6 +17587,11 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-strict": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/use-strict/-/use-strict-1.0.1.tgz", + "integrity": "sha512-IeiWvvEXfW5ltKVMkxq6FvNf2LojMKvB2OCeja6+ct24S1XOmQw2dGr2JyndwACWAGJva9B7yPHwAmeA9QCqAQ==" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 3f7d555..ea97394 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.7.7", + "lamejs": "^1.2.1", "react": "^18.3.1", "react-audio-player": "^0.17.0", "react-dom": "^18.3.1", diff --git a/src/App.css b/src/App.css index f14cf51..69401c7 100644 --- a/src/App.css +++ b/src/App.css @@ -123,3 +123,22 @@ html, body { width: 150px; height: 150px; } + +@keyframes breathe { + 0% { + transform: scale(1); + opacity: 0.8; + } + 50% { + transform: scale(1.05); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 0.8; + } +} + +.breathing-image { + animation: breathe 2s ease-in-out infinite; +} \ No newline at end of file diff --git a/src/App.js b/src/App.js index 8642ed0..9ae30e7 100644 --- a/src/App.js +++ b/src/App.js @@ -4,9 +4,13 @@ import './App.css'; import PlayList from './components/PlayList.tsx'; import 'react-vant/es/styles'; import MusicPlayer from './components/MusicPlayer.tsx'; -import React, { useState, useEffect, useCallback, useRef } from 'react'; -import { Image, Toast, Search } from 'react-vant' +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { Image, Toast, Search, Loading, Space } from 'react-vant' import { instance } from './utils/api'; +import { Music, MusicO, Close, createFromIconfontCN } from '@react-vant/icons'; +import tinggeshiqu from './assets/images/tinggeshiqu.png'; +import tinggeshiqu40x40 from './assets/images/tinggeshiqu40x40.png'; + // 添加获取 URL 参数的函数 function getParameterByName(name, url = window.location.href) { @@ -18,6 +22,42 @@ function getParameterByName(name, url = window.location.href) { return decodeURIComponent(results[2].replace(/\+/g, ' ')); } +// 添加以下辅助函数 +function writeString(view, offset, string) { + for (let i = 0; i < string.length; i++) { + view.setUint8(offset + i, string.charCodeAt(i)); + } +} + +function floatTo16BitPCM(output, offset, input) { + for (let i = 0; i < input.length; i++, offset += 2) { + let s = Math.max(-1, Math.min(1, input[i])); + output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); + } +} + +function writeAudioBufferToWav(audioBuffer, numChannels, sampleRate) { + const buffer = new ArrayBuffer(44 + audioBuffer.length * 2); + const view = new DataView(buffer); + + writeString(view, 0, 'RIFF'); + view.setUint32(4, 36 + audioBuffer.length * 2, true); + writeString(view, 8, 'WAVE'); + writeString(view, 12, 'fmt '); + view.setUint32(16, 16, true); + view.setUint16(20, 1, true); + view.setUint16(22, numChannels, true); + view.setUint32(24, sampleRate, true); + view.setUint32(28, sampleRate * numChannels * 2, true); + view.setUint16(32, numChannels * 2, true); + view.setUint16(34, 16, true); + writeString(view, 36, 'data'); + view.setUint32(40, audioBuffer.length * 2, true); + + floatTo16BitPCM(view, 44, audioBuffer); + + return new Blob([view], { type: 'audio/wav' }); +} function App() { const [currentSong, setCurrentSong] = useState(null); @@ -27,41 +67,36 @@ function App() { const searchRef = useRef(null); const searchInputRef = useRef(null); const [isShowPlayList, setIsShowPlayList] = useState(false); - // 添加 sourceEnv 状态 const [sourceEnv, setSourceEnv] = useState(null); const playlistRef = useRef(null); + const [isRecording, setIsRecording] = useState(false); + const [mediaRecorder, setMediaRecorder] = useState(null); + const [audioContext, setAudioContext] = useState(null); + const [audioInput, setAudioInput] = useState(null); + const [recorder, setRecorder] = useState(null); useEffect(() => { - // 获取 sourceEnv 参数 const envParam = getParameterByName('sourceEnv'); setSourceEnv(envParam); console.log('sourceEnv:', envParam); - // 根据 sourceEnv 的值执行相应的操作 if (envParam) { switch (envParam) { case 'production': - // 生产环境的逻辑 console.log('Running in production environment'); - // 这里可以添加生产环境特定的逻辑 break; case 'plugin': - // 开发环境的逻辑 console.log('Running in development environment'); - // 这里可以添加开发环境特定的逻辑 break; default: - // 默认逻辑 console.log('Running in default environment'); } } - // 获取播放列表 const fetchPlaylist = async () => { try { const response = await instance.get("/musicList"); setPlaylist(response.data); - // 设置第一首歌为当前歌曲 if (response.data.length > 0) { setCurrentSong(response.data[0]); } @@ -73,6 +108,7 @@ function App() { fetchPlaylist(); }, []); + const handlePrevSong = () => { if (playlist.length === 0) { Toast.info('播放列表为空'); @@ -93,12 +129,12 @@ function App() { setCurrentSong(playlist[nextIndex]); }; + const handleError = (error) => { console.error("Error playing song:", error); Toast.fail(currentSong.title + ' 播放出错,请稍后重试'); }; - // 修改 isInChromeExtension 函数 const isInChromeExtension = () => { return sourceEnv === 'plugin' }; @@ -106,9 +142,9 @@ function App() { const handleKeyDown = useCallback((event) => { if (event.ctrlKey && event.key === 'x') { event.preventDefault(); - setShowSearch(prevState => !prevState); // 切换搜索框的显示状态 + setShowSearch(prevState => !prevState); if (showSearch) { - setSearch(''); // 如果正在隐藏搜索框,清空搜索内容 + setSearch(''); } } }, [showSearch]); @@ -124,11 +160,9 @@ function App() { }, [showSearch, isShowPlayList]); useEffect(() => { - // 添加事件监听器 document.addEventListener('keydown', handleKeyDown); document.addEventListener('mousedown', handleClickOutside); - // 清理函数 return () => { document.removeEventListener('keydown', handleKeyDown); document.removeEventListener('mousedown', handleClickOutside); @@ -142,12 +176,130 @@ function App() { }, [showSearch]); const updateSongInPlaylist = (updatedSong) => { - setPlaylist(prevPlaylist => - prevPlaylist.map(song => + setPlaylist(prevPlaylist => + prevPlaylist.map(song => song.id === updatedSong.id ? updatedSong : song ) ); }; + const stopRecording = async () => { + if (audioContext && audioInput && recorder && mediaRecorder && isRecording) { + setIsRecording(false); // 立即设置为非录音状态 + + if (mediaRecorder.stream) { + mediaRecorder.stream.getTracks().forEach(track => track.stop()); + } + + recorder.disconnect(); + audioInput.disconnect(); + + if (audioContext.state !== 'closed') { + await audioContext.close(); + } + + const audioData = mediaRecorder.audioChunks.reduce((acc, chunk) => { + return new Float32Array([...acc, ...chunk]); + }, new Float32Array()); + + const wavBlob = writeAudioBufferToWav(audioData, 1, audioContext.sampleRate); + + setAudioContext(null); + setAudioInput(null); + setRecorder(null); + setMediaRecorder(null); + + Toast.success('识别中...'); + + await sendAudioToBackend(wavBlob); + } + }; + + const startRecording = useCallback(async () => { + if (isRecording) { + await stopRecording(); + } + + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + const context = new (window.AudioContext || window.webkitAudioContext)(); + const source = context.createMediaStreamSource(stream); + const node = context.createScriptProcessor(4096, 1, 1); + + let audioChunks = []; + + node.onaudioprocess = (e) => { + const inputBuffer = e.inputBuffer; + const channelData = inputBuffer.getChannelData(0); + audioChunks.push(new Float32Array(channelData)); + }; + + source.connect(node); + node.connect(context.destination); + + setAudioContext(context); + setAudioInput(source); + setRecorder(node); + setMediaRecorder({ audioChunks, stream }); + setIsRecording(true); + + // Toast.success('开始录音'); + } catch (error) { + console.error('录音失败:', error); + Toast.fail('无法访问麦克风'); + } + }, [isRecording, stopRecording]); + + const sendAudioToBackend = async (audioBlob) => { + try { + const formData = new FormData(); + const timestamp = Date.now(); + formData.append('audio', audioBlob, `recording_${timestamp}.wav`); + + const response = await instance.post('/uploadAudio', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + Toast.success({ + message: "识别结果:《" + response.data.title + "》," + response.data.artist, + duration: 5000 // 设置持续时间为 5000 毫秒(5 秒) + }); + + } catch (error) { + console.error('音频上传失败:', error); + Toast.fail({ + message: '音频上传失败', + duration: 5000 // 错误消息也设置为 5 秒 + }); + } + }; + + useEffect(() => { + return () => { + if (audioContext && audioContext.state !== 'closed') { + audioContext.close(); + } + if (audioInput) { + audioInput.disconnect(); + } + if (recorder) { + recorder.disconnect(); + } + if (mediaRecorder && mediaRecorder.stream) { + mediaRecorder.stream.getTracks().forEach(track => track.stop()); + } + }; + }, [audioContext, audioInput, recorder, mediaRecorder]); + + // useEffect(() => { + // return () => { + // // 组件卸载时的清理函数 + // if (isRecording) { + // stopRecording(); + // } + // }; + // }, [isRecording, stopRecording]); return (
@@ -173,7 +325,6 @@ function App() { onSearch={async (val) => { const response = await instance.post("/search", { "title": val }); if (response.data instanceof Array) { - // 设置一首歌为当前歌曲 if (response.data.length > 0) { setCurrentSong(response.data[0]); } @@ -212,7 +363,7 @@ function App() { onError={handleError} setIsShowPlayList={setIsShowPlayList} setCurrentSong={setCurrentSong} - updateSongInPlaylist={updateSongInPlaylist} // 新增这一行 + updateSongInPlaylist={updateSongInPlaylist} /> {isShowPlayList && (
@@ -228,9 +379,45 @@ function App() { )}
+
+ {isRecording ? ( + + ) : ( + <> + + 听歌识曲 + + 听歌识曲 + + )} +
); } -export default App; +export default App; \ No newline at end of file diff --git a/src/assets/images/micro.png b/src/assets/images/micro.png new file mode 100644 index 0000000..3b026b4 Binary files /dev/null and b/src/assets/images/micro.png differ diff --git a/src/assets/images/tinggeshiqu.png b/src/assets/images/tinggeshiqu.png new file mode 100644 index 0000000..c0afe53 Binary files /dev/null and b/src/assets/images/tinggeshiqu.png differ diff --git a/src/assets/images/tinggeshiqu40x40.png b/src/assets/images/tinggeshiqu40x40.png new file mode 100644 index 0000000..f63cf18 Binary files /dev/null and b/src/assets/images/tinggeshiqu40x40.png differ diff --git a/src/components/MusicPlayer.tsx b/src/components/MusicPlayer.tsx index 3ca8c11..c0b5529 100644 --- a/src/components/MusicPlayer.tsx +++ b/src/components/MusicPlayer.tsx @@ -1,11 +1,10 @@ import React, { useEffect, useState, useRef } from 'react'; import ReactAudioPlayer from 'react-audio-player'; import './MusicPlayer.css'; -import { Arrow, ArrowLeft, PauseCircle, PlayCircle, Bars, LikeO, Like } from '@react-vant/icons'; +import { Arrow, ArrowLeft, PauseCircle, PlayCircle, Bars, LikeO, Like, Music } from '@react-vant/icons'; import { Slider } from 'react-vant'; import { instance } from '../utils/api'; import { Toast } from 'react-vant/lib'; - // 辅助函数:将秒数转换为 "分:秒" 格式 const formatTime = (seconds: number): string => { const minutes = Math.floor(seconds / 60); @@ -32,7 +31,7 @@ const MusicPlayer = ({ currentSong, onPrevSong, onNextSong, onError, setIsShowPl likeState: status }); Toast.success(response.data); - + // 更新播放列表中的歌曲状态 updateSongInPlaylist(updatedSong); } catch (error) { diff --git a/src/utils/api.js b/src/utils/api.js index 0df6ee1..7a13472 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -1,8 +1,8 @@ import axios from 'axios'; export const instance = axios.create({ - // baseURL: 'https://app102.acapp.acwing.com.cn/api', - baseURL: 'http://localhost:8809/api', + baseURL: 'https://app102.acapp.acwing.com.cn/api', + // baseURL: 'http://localhost:8809/api', headers: { 'Content-Type': 'application/json',