Add new audio file: nhac-phat-giao.mp3 to media directory

This commit is contained in:
tuyenio
2026-06-25 14:28:46 +07:00
parent 8ad9c7113b
commit 6fa8a6016a
16 changed files with 179 additions and 18 deletions
@@ -0,0 +1,95 @@
# Lồng tiếng thuyết minh + nhạc nền — Tour Chùa Thái Lạc
Ngày: 2026-06-25
## Mục tiêu
Gắn âm thanh thuyết minh (lồng tiếng) cho từng cảnh trong tour VR360 Chùa Thái Lạc,
và thêm nhạc nền chạy liên tục trên toàn bộ tour với âm lượng nhỏ nhẹ để không lấn lời thuyết minh.
## Bối cảnh kỹ thuật
Engine `src/app/components/VirtualTour.tsx` đã hỗ trợ đầy đủ, **không cần sửa logic**:
- `backgroundAudio: string` — một track nhạc nền phát lặp (`loop`). Âm lượng nền
`AUDIO_TARGET_VOLUME = 0.16`, tự động hạ xuống `BACKGROUND_DUCK_VOLUME = 0.045`
khi đang phát thuyết minh (ducking). Chuỗi rỗng `""` → engine bỏ qua, không render audio.
- `sceneNarration: Map<SceneId, string[]>` — mỗi cảnh có một MẢNG nguồn âm thanh,
phát tuần tự (hàng đợi). Âm lượng thuyết minh `NARRATION_TARGET_VOLUME = 0.7`.
- `continuousNarrationGroups: SceneId[][]` — các cảnh trong cùng nhóm dùng chung file
sẽ phát LIỀN MẠCH (không tua lại) khi khách di chuyển giữa chúng.
Đây là việc cấu hình DỮ LIỆU trong `src/app/tours/chua-thai-lac.ts` + đổi tên file âm thanh.
## Yêu cầu
### 1. Đổi tên 13 file âm thanh sang ASCII
Theo convention dự án (tránh lỗi URL-encode + phân biệt hoa/thường khi deploy lên Linux/Vercel).
Hai file `.MP3` (chữ hoa) được chuẩn hóa thành `.mp3`.
| File hiện tại | Tên mới |
|---|---|
| `public/media/chua-thai-lac/Lời chào.mp3` | `loi-chao.mp3` |
| `public/media/chua-thai-lac/Tam quan.mp3` | `tam-quan.mp3` |
| `public/media/chua-thai-lac/Sân Tịnh An.mp3` | `san-tinh-an.mp3` |
| `public/media/chua-thai-lac/Hồ Di Lặc.MP3` | `ho-di-lac.mp3` |
| `public/media/chua-thai-lac/Tiền sảnh.mp3` | `tien-sanh.mp3` |
| `public/media/chua-thai-lac/Tôn Đài Đức Phật Thích Ca.mp3` | `ton-dai-duc-phat.mp3` |
| `public/media/chua-thai-lac/Vườn tháp cổ.mp3` | `vuon-thap-co.mp3` |
| `public/media/chua-thai-lac/Khách xá.mp3` | `khach-xa.mp3` |
| `public/media/chua-thai-lac/Khách đường.mp3` | `khach-duong.mp3` |
| `public/media/chua-thai-lac/Hữu Vu La Hán- Tả Vu La Hán.MP3` | `huu-vu-ta-vu-la-han.mp3` |
| `public/media/chua-thai-lac/Nhà Tổ.mp3` | `nha-to.mp3` |
| `public/media/chua-thai-lac/Tiền Đường.mp3` | `tien-duong.mp3` |
| `public/media/nhac-nen/Nhạc Phật Giáo sâu lắng.mp3` | `nhac-phat-giao.mp3` |
### 2. Cấu hình `src/app/tours/chua-thai-lac.ts`
**a) Nhạc nền:** `backgroundAudio: ""``"/media/nhac-nen/nhac-phat-giao.mp3"`.
Giữ nguyên âm lượng mặc định của engine (nền 0.16, ducking 0.045 khi thuyết minh).
**b) Bảng thuyết minh `narrationGroups`** — mở rộng kiểu trường `audio` thành `string | string[]`
để cảnh 1-3 phát nối tiếp hai file (lời chào rồi Tam quan):
```
[loi-chao, tam-quan] → cảnh 1, 2, 3
san-tinh-an → cảnh 4, 5
ho-di-lac → cảnh 6
tien-sanh → cảnh 7, 8 (MỚI)
ton-dai-duc-phat → cảnh 9 (MỚI)
vuon-thap-co → cảnh 10 (MỚI)
(không thuyết minh) → cảnh 11
khach-xa → cảnh 12, 13, 14
khach-duong → cảnh 15
huu-vu-ta-vu-la-han → cảnh 16, 18
nha-to → cảnh 17
tien-duong → cảnh 19, 20, 21
```
Vòng lặp build `sceneNarration` phải chuẩn hóa `audio` (string hoặc string[]) thành mảng nguồn
qua `audioForName` cho mỗi cảnh.
**c) `continuousNarrationGroups`** — thêm `["scene-7", "scene-8"]` (hai cảnh kề nhau dùng chung
`tien-sanh`). Các nhóm hiện có giữ nguyên. Cảnh 16 & 18 vẫn KHÔNG gom (cách nhau bởi cảnh 17)
nên sẽ phát lại — đúng hành vi mong muốn.
## Hành vi đã thống nhất
- Cảnh 1: phát `loi-chao` rồi tự động phát `tam-quan`; di chuyển 1→2→3 không tua lại.
- Rời cảnh 1-3 rồi quay lại → "lời chào" phát lại từ đầu (hành vi mặc định, chấp nhận được).
- Nhạc nền nhỏ nhẹ và tự hạ thấp hơn nữa khi có thuyết minh.
## Kiểm chứng
1. `npm run lint` / `npm run build` không lỗi type.
2. `npm run dev`, vào tour và xác nhận:
- Nhạc nền chạy nhẹ liên tục mọi cảnh.
- Mỗi cảnh phát đúng file thuyết minh theo bảng trên.
- Cảnh 1 nghe "lời chào → tam quan" nối tiếp.
- Nhạc nền tự nhỏ lại khi đang có lời thuyết minh.
## Ngoài phạm vi
- Không sửa logic engine `VirtualTour.tsx`.
- Không thay đổi nội dung popup info markers, hướng mũi tên, hay sơ đồ mini-map.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+31 -8
View File
@@ -1043,6 +1043,8 @@ function TourExperience({
const narrationTokenRef = useRef(0);
const activeNarrationSrcRef = useRef<string | null>(null);
const lastNarrationSceneRef = useRef<SceneId | null>(null);
// Listener "ended" của bài đang phát — giữ lại để gỡ sạch khi dừng/đổi hàng đợi.
const narrationEndedRef = useRef<(() => void) | null>(null);
const { orientation, isSupported, requestPermission, startListening } = useDeviceOrientation();
@@ -1053,6 +1055,10 @@ function TourExperience({
activeNarrationSrcRef.current = null;
if (audio) {
if (narrationEndedRef.current) {
audio.removeEventListener("ended", narrationEndedRef.current);
narrationEndedRef.current = null;
}
audio.pause();
audio.currentTime = 0;
audio.onended = null;
@@ -1074,6 +1080,12 @@ function TourExperience({
const token = narrationTokenRef.current;
narrationQueueRef.current = [...sources];
// Gỡ listener "ended" của lần phát trước (nếu còn) để không bị gọi chồng.
if (narrationEndedRef.current) {
audio.removeEventListener("ended", narrationEndedRef.current);
narrationEndedRef.current = null;
}
const playNext = async () => {
if (token !== narrationTokenRef.current) {
return;
@@ -1087,17 +1099,28 @@ function TourExperience({
return;
}
if (audio.src !== nextSource) {
// Đổi nguồn rồi load() để play() không bị huỷ giữa chừng khi chuyển bài
// trong hàng đợi — đảm bảo cảnh 1-3 phát "lời chào" xong NỐI TIẾP "Tam quan".
audio.src = nextSource;
}
audio.currentTime = 0;
audio.playbackRate = NARRATION_PLAYBACK_RATE;
audio.volume = NARRATION_TARGET_VOLUME;
audio.load();
activeNarrationSrcRef.current = nextSource;
// Mỗi bài gắn một listener "ended" riêng (once) → khi bài này hết thì
// tự phát bài kế tiếp trong hàng đợi. Không phụ thuộc audio.onended (dễ bị null).
const handleEnded = () => {
narrationEndedRef.current = null;
void playNext();
};
narrationEndedRef.current = handleEnded;
audio.addEventListener("ended", handleEnded, { once: true });
try {
await audio.play();
if (token !== narrationTokenRef.current) {
return;
}
onNarrationStateChange?.(true);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
@@ -1108,10 +1131,6 @@ function TourExperience({
}
};
audio.onended = () => {
void playNext();
};
void playNext();
},
[onNarrationStateChange],
@@ -1205,6 +1224,10 @@ function TourExperience({
window.clearTimeout(closingInfoTimerRef.current);
}
if (narrationAudio) {
if (narrationEndedRef.current) {
narrationAudio.removeEventListener("ended", narrationEndedRef.current);
narrationEndedRef.current = null;
}
narrationAudio.onended = null;
}
};
+52 -9
View File
@@ -102,7 +102,25 @@ const seeds: SceneSeed[] = [
],
},
// 7 — ảnh 08: sân lớn lát đá trước gác chuông tam quan đồ sộ
{ title: "Tiền sảnh", location: "Sân chùa & Tam quan", imageFile: 8 },
{
title: "Tiền sảnh",
location: "Sân chùa & Tam quan",
imageFile: 8,
infoMarkers: [
{
id: "tien-sanh",
title: "Tiền sảnh",
eyebrow: EYEBROW,
yaw: 90,
pitch: 0,
iconType: "info",
content: [
"Khu vực sân trong là một khoảng sân rộng lớn, thông thoáng, được lát phối hợp giữa gạch xám và các dải gạch đỏ tạo thành những đường vòng cung định hướng không gian. Khoảng sân được bao bọc bởi dãy nhà cổ lợp mái ngói mũi hài rêu phong, tường sơn vàng và hệ thống cửa gỗ lá bàng khép kín. Tại vị trí chính diện tòa nhà trung tâm, một đỉnh hương đá lớn được tôn trí giữa hai cột đèn đá nhỏ.",
"Bao quanh sân là hệ thống cây cảnh và các cây duối cổ thụ được cắt tỉa, uốn thế dạng tầng tán mâm xôi bề thế, đặt trong các bồn đá tròn. Phía góc sân hướng ra bên ngoài là khoảng không gian trống có treo cờ Phật giáo, kết hợp với các dãy chậu cây cảnh xếp dọc tường bao. Phía đối diện là một lễ đài lớn ngoài trời có dựng bức bạt tạo hình đóa hoa sen tôn trí hình ảnh Đức Phật, hoàn thiện kết cấu cảnh quan xung quanh sân chính.",
],
},
],
},
// 8 — ảnh 09: sân gạch, tam quan bên trái, đài Quan Âm bên phải
{ title: "Sân chùa", location: "Sân chùa & Tam quan", imageFile: 9 },
// 9 — ảnh 10: đài Quan Âm lộ thiên, sân lát gạch đỏ
@@ -126,7 +144,25 @@ const seeds: SceneSeed[] = [
],
},
// 10 — ảnh 11: vườn tháp, tháp mộ gạch cổ
{ title: "Vườn tháp cổ", location: "Sân chùa & Tam quan", imageFile: 11 },
{
title: "Vườn tháp cổ",
location: "Sân chùa & Tam quan",
imageFile: 11,
infoMarkers: [
{
id: "vuon-thap-co",
title: "Vườn tháp cổ",
eyebrow: EYEBROW,
yaw: 115,
pitch: 0,
iconType: "info",
content: [
"Khu vực Vườn tháp cổ được xây dựng trên một đài nền xi măng cao hơn mặt sân, có hệ thống bậc tam cấp thấp trải dài làm lối dẫn lên. Trung tâm khuôn viên là một lối đi lát đá xám phẳng phiu, hai bên bố trí đối xứng các bảo tháp ba tầng được xây bằng gạch trần màu đỏ cam kiên cố.",
"Mỗi bảo tháp có phần chân đế đắp nổi phù điêu đá xám, thân tháp trổ các vòm cửa nhỏ hướng ra ngoài và phần mái tầng trên cùng được tạo tác cong vút với các họa tiết mây đao bằng đá xám, đỉnh tháp nhọn hướng lên nền trời. Bao bọc xung quanh các ngôi bảo tháp là hệ thống cây xanh cổ thụ tán rộng và các chậu cây cảnh tầm trung đặt trong chum gốm nung màu nâu mộc mạc.",
],
},
],
},
// 11 — ảnh 12: lối đi hẹp giữa các dãy nhà, chậu cảnh
{ title: "Tả đạo", location: "Khu nhà Tổ & nhà khách", imageFile: 12 },
// 12 — ảnh 14: hiên nhà có hàng cột, giàn cây xanh che mát (Khách xá)
@@ -570,14 +606,19 @@ const mapRoutes = (() => {
// ─────────────────────────────────────────────────────────────────────────────
// LỒNG TIẾNG (thuyết minh) — gán file âm thanh cho từng nhóm cảnh.
// Mỗi nhóm dùng CHUNG một file; khi khách đi giữa các cảnh trong cùng nhóm thì
// Mỗi nhóm dùng CHUNG (các) file; khi khách đi giữa các cảnh trong cùng nhóm thì
// audio phát LIỀN MẠCH (không tua lại) nhờ `continuousNarrationGroups` bên dưới.
// Cảnh không có trong bảng (7, 8, 9, 10, 11) sẽ không phát thuyết minh.
// `audio` có thể là một tên hoặc MẢNG tên — mảng sẽ phát NỐI TIẾP nhau (hàng đợi),
// vd cảnh 1-3 phát "lời chào" xong tự phát tiếp "Tam quan".
// Cảnh không có trong bảng (cảnh 11) sẽ không phát thuyết minh.
// ─────────────────────────────────────────────────────────────────────────────
const narrationGroups: { audio: string; scenes: number[] }[] = [
{ audio: "tam-quan", scenes: [1, 2, 3] },
const narrationGroups: { audio: string | string[]; scenes: number[] }[] = [
{ audio: ["loi-chao", "tam-quan"], scenes: [1, 2, 3] },
{ audio: "san-tinh-an", scenes: [4, 5] },
{ audio: "ho-di-lac", scenes: [6] },
{ audio: "tien-sanh", scenes: [7, 8] },
{ audio: "ton-dai-duc-phat", scenes: [9] },
{ audio: "vuon-thap-co", scenes: [10] },
{ audio: "khach-xa", scenes: [12, 13, 14] },
{ audio: "khach-duong", scenes: [15] },
{ audio: "huu-vu-ta-vu-la-han", scenes: [16, 18] },
@@ -587,9 +628,10 @@ const narrationGroups: { audio: string; scenes: number[] }[] = [
const sceneNarration = new Map<string, string[]>();
for (const group of narrationGroups) {
const src = audioForName(group.audio);
const names = Array.isArray(group.audio) ? group.audio : [group.audio];
const sources = names.map(audioForName);
for (const scene of group.scenes) {
sceneNarration.set(sceneId(scene), [src]);
sceneNarration.set(sceneId(scene), sources);
}
}
@@ -598,6 +640,7 @@ for (const group of narrationGroups) {
const continuousNarrationGroups: string[][] = [
["scene-1", "scene-2", "scene-3"],
["scene-4", "scene-5"],
["scene-7", "scene-8"],
["scene-12", "scene-13", "scene-14"],
["scene-19", "scene-20", "scene-21"],
];
@@ -620,7 +663,7 @@ export const chuaThaiLacTour: Omit<TourConfig, "sceneById" | "locationGroups"> =
sceneId: "scene-9",
},
],
backgroundAudio: "", // chưa có nhạc nền; engine bỏ qua chuỗi rỗng (không render thẻ audio)
backgroundAudio: "/media/nhac-nen/nhac-phat-giao.mp3", // nhạc nền nhẹ, tự hạ thấp khi có thuyết minh
practicalInfo: {
intro: "Thông tin phục vụ tham quan và lễ chùa tại Chùa Thái Lạc (Pháp Vân tự).",
items: [