peter288 commited on
Commit
417b727
·
verified ·
1 Parent(s): 83a1998

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +24 -0
  2. app/layout.tsx +19 -0
  3. app/page.tsx +103 -0
  4. next.config.js +6 -0
  5. package.json +22 -0
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用官方輕量級的 Node.js 20 Alpine 映像
2
+ FROM node:20-alpine
3
+
4
+ # 設定工作目錄
5
+ WORKDIR /app
6
+
7
+ # 複製 package.json 和 package-lock.json
8
+ # 使用 * 確保即使 lock 檔案不存在也能運行
9
+ COPY package.json package-lock.json* ./
10
+
11
+ # 安裝專案依賴
12
+ RUN npm install
13
+
14
+ # 複製所有專案檔案到工作目錄
15
+ COPY . .
16
+
17
+ # 執行 Next.js 生產環境建置
18
+ RUN npm run build
19
+
20
+ # Hugging Face Spaces 預設使用 7860 端口
21
+ EXPOSE 7860
22
+
23
+ # 啟動應用程式的命令
24
+ CMD ["npm", "start"]
app/layout.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 位於 app/layout.tsx
2
+
3
+ import type { Metadata } from "next";
4
+ // 修正:直接只匯入 ReactNode 這個「類型」,而不是整個 React 物件
5
+ import type { ReactNode } from 'react';
6
+
7
+ export const metadata: Metadata = {
8
+ title: "MIDI Player Lite",
9
+ description: "A lightweight MIDI player for Hugging Face Spaces.",
10
+ };
11
+
12
+ // 修正:直接使用 ReactNode,不再需要寫 React.ReactNode
13
+ export default function RootLayout({ children }: { children: ReactNode }) {
14
+ return (
15
+ <html lang="en">
16
+ <body>{children}</body>
17
+ </html>
18
+ );
19
+ }
app/page.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import MidiPlayer from 'midi-player-js';
4
+ import Soundfont from 'soundfont-player';
5
+
6
+ // 定義 Soundfont Player 的 instrument 類型,以獲得更好的 TypeScript 支援
7
+ interface Instrument {
8
+ play(noteName: string, when?: number, options?: any): void;
9
+ stop(noteName?: string): void;
10
+ }
11
+
12
+ export default function Home() {
13
+ // 1. 將 useRef 的初始值設為 null,避免在伺服器端存取 window
14
+ const audioCtx = useRef<AudioContext | null>(null);
15
+ const instrument = useRef<Instrument | null>(null);
16
+ const player = useRef<MidiPlayer.Player | null>(null);
17
+ const [isLoaded, setIsLoaded] = useState(false);
18
+ const [nowPlaying, setNowPlaying] = useState("");
19
+
20
+ useEffect(() => {
21
+ // 2. 在 useEffect 中才建立 AudioContext,確保只在瀏覽器環境中執行
22
+ if (!audioCtx.current) {
23
+ audioCtx.current = new (window.AudioContext || (window as any).webkitAudioContext)();
24
+ }
25
+
26
+ // 載入 Soundfont 和 MIDI 播放器
27
+ Soundfont.instrument(audioCtx.current, 'acoustic_grand_piano').then(inst => {
28
+ instrument.current = inst;
29
+ player.current = new MidiPlayer.Player(event => {
30
+ if (!instrument.current) return;
31
+ if (event.name === 'Note on' && event.velocity > 0) {
32
+ instrument.current.play(event.noteName);
33
+ }
34
+ if (event.name === 'Note off' || (event.name === 'Note on' && event.velocity === 0)) {
35
+ instrument.current.stop(event.noteName);
36
+ }
37
+ });
38
+ player.current.on('end', () => setNowPlaying(""));
39
+ setIsLoaded(true);
40
+ });
41
+
42
+ // 3. 元件卸載時的清理函數,關閉 AudioContext
43
+ return () => {
44
+ if (audioCtx.current && audioCtx.current.state !== 'closed') {
45
+ audioCtx.current.close();
46
+ }
47
+ };
48
+ }, []); // 空依賴陣列確保此 effect 只執行一次
49
+
50
+ const loadMidiAndPlay = (e: React.ChangeEvent<HTMLInputElement>) => {
51
+ const file = e.target.files?.[0];
52
+ if (file && player.current) {
53
+ const reader = new FileReader();
54
+ reader.onload = (event) => {
55
+ if (event.target?.result) {
56
+ player.current?.loadArrayBuffer(event.target.result as ArrayBuffer);
57
+ setNowPlaying(`正在播放: ${file.name}`);
58
+ player.current?.play();
59
+ }
60
+ };
61
+ reader.readAsArrayBuffer(file);
62
+ }
63
+ };
64
+
65
+ const handlePlay = () => {
66
+ if (player.current?.isPlaying()) return;
67
+ player.current?.play();
68
+ };
69
+
70
+ const handleStop = () => {
71
+ player.current?.stop();
72
+ setNowPlaying("");
73
+ };
74
+
75
+ return (
76
+ <main style={{ padding: "2rem", fontFamily: "sans-serif", maxWidth: "600px", margin: "auto", color: "#333" }}>
77
+ <h1 style={{ textAlign: "center", color: "#111" }}>🎵 MIDI Player Lite</h1>
78
+ <div style={{ border: "1px solid #ddd", padding: "2rem", borderRadius: "8px", textAlign: "center", background: "#f9f9f9" }}>
79
+ {isLoaded ? (
80
+ <>
81
+ <p>請選擇一個 .mid 或 .midi 檔案來播放。</p>
82
+ <input
83
+ type="file"
84
+ accept=".mid,.midi"
85
+ onChange={loadMidiAndPlay}
86
+ style={{ display: 'block', margin: '1rem auto' }}
87
+ />
88
+ <div style={{ marginTop: "1rem", display: 'flex', gap: '10px', justifyContent: 'center' }}>
89
+ <button onClick={handlePlay} disabled={!player.current?.getSong() || player.current?.isPlaying()}>播放</button>
90
+ <button onClick={handleStop} disabled={!player.current?.getSong()}>停止</button>
91
+ </div>
92
+ {nowPlaying && <p style={{marginTop: "1.5rem", color: "#555", fontStyle: 'italic'}}>{nowPlaying}</p>}
93
+ </>
94
+ ) : (
95
+ <p>正在載入聲音引擎...</p>
96
+ )}
97
+ </div>
98
+ <footer style={{textAlign: 'center', marginTop: '2rem', color: '#888', fontSize: '0.9em'}}>
99
+ <p>Deployed on Hugging Face Spaces</p>
100
+ </footer>
101
+ </main>
102
+ );
103
+ }
next.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ reactStrictMode: true,
4
+ };
5
+
6
+ module.exports = nextConfig;
package.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "midiplayer-lite-hf",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start -p 7860"
9
+ },
10
+ "dependencies": {
11
+ "next": "^14.2.3",
12
+ "react": "^18.2.0",
13
+ "react-dom": "^18.2.0",
14
+ "midi-player-js": "^2.0.5",
15
+ "soundfont-player": "^0.8.5"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "20.14.9",
19
+ "@types/react": "18.3.3",
20
+ "typescript": "5.5.2"
21
+ }
22
+ }