njin
Engine 2D gọn nhẹ để tự tay làm game theo ý mình. Vibe coding cũng ổn luôn
Loading...
Searching...
No Matches
Âm thanh

njin có hai loại âm thanh, cho hai nhu cầu khác nhau:

Sound Music
Dùng cho Hiệu ứng ngắn: tiếng bắn, nhấp, va chạm Nhạc nền, âm thanh môi trường dài
Lưu ở đâu Nằm hết trong bộ nhớ Stream từ đĩa, giải mã từng đoạn
Phát chồng nhiều bản Có (tối đa 8) Không, một bài một luồng
Lặp Có, nhưng chỗ nối có quãng lặng cỡ một frame Có, không có quãng lặng
Hàm sound_* music_*

Cả hai dùng handle giống texture và shader: xem Vẽ ảnh và shader.

Ví dụ

#include <njin.h>
namespace {
njin::sound_handle engine_hum;
njin::action_handle pause_music;
bool paused = false;
void load(njin::context &ctx) {
// Sound: ngắn, nằm hết trong bộ nhớ. Music: dài, stream từ đĩa.
click = njin::sound_load(ctx, "assets/click.wav");
hit = njin::sound_load(ctx, "assets/hit.wav");
engine_hum = njin::sound_load(ctx, "assets/hum.ogg");
theme = njin::music_load(ctx, "assets/theme.ogg");
// Nạp thất bại (file thiếu, không có loa) trả về handle id 0 và ghi log.
// Mọi hàm nhận handle đó đều không làm gì, nên không cần kiểm tra ở mọi chỗ.
njin::sound_set_volume(ctx, engine_hum, 0.4f);
njin::music_set_volume(ctx, theme, 0.7f);
njin::music_play(ctx, theme); // lặp mặc định
njin::sound_play_loop(ctx, engine_hum);
fire = njin::action_register(ctx, "fire");
pause_music = njin::action_register(ctx, "pause_music");
njin::action_bind_key(ctx, pause_music, njin::key_p);
}
void update(njin::context &ctx) {
// Bấm dồn cũng nghe rõ từng tiếng nhấp: mỗi lần cắt bản trước rồi phát lại.
// Tiếng va chạm: nhiều thứ cùng kích hoạt thì chồng lên nhau (tối đa 8 bản).
// Vật lớn hơn thì trầm hơn và to hơn: cùng một bản ghi cho mọi kích cỡ.
if (njin::action_pressed(ctx, fire)) {
const njin::f32 size = 2.0f; // ví dụ
njin::sound_play_once_at(ctx, hit, 1.0f / size, 0.5f + 0.25f * size);
}
// Tạm dừng giữ nguyên vị trí; phát tiếp từ chỗ đó.
if (njin::action_pressed(ctx, pause_music)) {
paused = !paused;
if (paused)
njin::music_pause(ctx, theme);
else
njin::music_resume(ctx, theme);
}
}
void setup(njin::context &ctx) {
}
} // namespace
njin::mod_desc audio_demo_module() {
return {.name = "audio_demo", .setup = setup};
}
bool key_pressed(const context &ctx, key_code key)
Đúng ở frame mà phím vừa được nhấn xuống (chỉ một frame).
void action_bind_key(context &ctx, action_handle handle, key_code key)
Gắn một phím vào action.
bool action_pressed(const context &ctx, action_handle handle)
Đúng ở frame mà một trong các nguồn của action (phím, nút chuột, nút tay cầm) vừa được nhấn.
action_handle action_register(context &ctx, const char *name)
Đăng ký một action có tên.
@ key_p
Phím P.
Definition _types.h:359
@ key_space
Phím cách.
Definition _types.h:411
@ key_enter
Enter.
Definition _types.h:412
void ecs_register(context &ctx, sys_phase phase, sys_fnc fnc, const char *name=nullptr)
Thêm một system vào lịch chạy của một phase.
@ phase_update
Mỗi frame, logic chính của game.
Definition _mod.h:35
@ phase_startup
Một lần, trước frame đầu tiên.
Definition _mod.h:32
void music_play(context &ctx, music_handle handle)
Phát từ đầu, kể cả khi đang tạm dừng hoặc đang phát.
void music_resume(context &ctx, music_handle handle)
Phát tiếp từ chỗ music_pause() đã dừng.
music_handle music_load(context &ctx, const char *path)
Nạp một bản nhạc để stream từ đĩa.
void music_set_volume(context &ctx, music_handle handle, f32 volume)
Đặt âm lượng của music, từ 0 trở lên (1 là âm lượng gốc).
void music_pause(context &ctx, music_handle handle)
Tạm dừng nhưng giữ nguyên vị trí.
void sound_play_once_at(context &ctx, sound_handle handle, f32 pitch, f32 gain)
Giống sound_play_once() nhưng chỉnh cao độ và độ lớn cho riêng bản này.
sound_handle sound_load(context &ctx, const char *path)
Nạp một âm thanh ngắn vào bộ nhớ.
void sound_play_restart(context &ctx, sound_handle handle)
Cắt mọi bản đang phát rồi phát lại từ đầu, nên lúc nào cũng chỉ nghe một bản.
void sound_set_volume(context &ctx, sound_handle handle, f32 volume)
Đặt âm lượng của sound, từ 0 trở lên (1 là âm lượng gốc).
void sound_play_loop(context &ctx, sound_handle handle)
Đánh dấu sound là lặp lại và phát nếu nó chưa phát.
float f32
Số thực 32 bit.
Definition _types.h:28
Định danh của một action nhập liệu, tạo bởi action_register().
Definition _types.h:85
Mô tả một module: một nhóm system có tên.
Definition _mod.h:67
Định danh của một bản nhạc stream (music), tạo bởi music_load().
Definition _types.h:123
Định danh của một âm thanh ngắn (sound), tạo bởi sound_load().
Definition _types.h:116

Nạp và thất bại

njin::sound_load() và njin::music_load() trả về handle có id == 0 khi:

  • Máy không có thiết bị âm thanh (không loa, lỗi driver). Game vẫn chạy bình thường, chỉ là không có tiếng.
  • File không tồn tại.
  • File không có đuôi (như Doxyfile). raylib chọn bộ giải mã theo đuôi file nên không đoán được định dạng.
  • File không giải mã được.

Mọi trường hợp đều ghi log lý do kèm đường dẫn. Mọi hàm nhận handle không hợp lệ hoặc đã unload thì không làm gì, nên bạn không cần kiểm tra ở mỗi chỗ gọi.

Định dạng hỗ trợ là những gì raylib giải mã được, gồm wav, ogg, mp3, flac. Đường dẫn tính từ thư mục làm việc khi chạy game (xem Bắt đầu).

Sound

Phát

Bốn cách phát, cho bốn nhu cầu:

Hàm Hành vi Hợp với
njin::sound_play_once() Chồng lên các bản đang phát, tối đa 8. Quá số đó thì bản chạy lâu nhất bị cắt Âm thanh nhiều thứ cùng kích hoạt: đám đông được nghe như đám đông
njin::sound_play_once_at() Như trên, nhưng chỉnh cao độ và độ lớn cho riêng bản này Cùng một bản ghi cho nhiều vật kích cỡ khác nhau
njin::sound_play_restart() Cắt mọi bản đang phát rồi phát lại từ đầu Tiếng nhấp giao diện, tiếng cảnh báo: bấm dồn thì nghe rõ từng lần
njin::sound_play_loop() Phát lặp cho đến khi njin::sound_stop() Tiếng động cơ, tiếng nền ngắn

Với njin::sound_play_once_at(), pitch 1 là như bản ghi và 2 là cao gấp đôi, gain nhân vào âm lượng của sound. Cả hai chỉ áp dụng cho bản đó: lần phát thường sau đó trở về 1 và 1.

Âm thanh theo vị trí

njin::sound_play_at() phát như njin::sound_play_once() kèm một vị trí trong thế giới:

  • Âm lượng giảm dần theo khoảng cách tới điểm camera đang nhìn: đủ to trong khoảng gần, nhỏ dần đều, im hẳn ở khoảng xa. Chỉnh bằng njin::audio_set_range() (mặc định 200 và 1200).
  • Trái phải: tiếng lệch sang loa trái hoặc phải theo vị trí trên màn hình.

Âm lượng và tắt tiếng

  • njin::sound_set_volume(): 1 là âm lượng gốc, 0 là im lặng. Áp dụng ngay cả cho những bản đang phát.
  • njin::sound_set_muted(): đưa âm lượng đang phát về 0 mà không đụng đến âm lượng đã lưu, nên bật lại thì khôi phục đúng như cũ.

Âm thanh tự sinh

Không phải âm thanh nào cũng đọc từ file. njin::sound_load_samples() tạo sound từ các mẫu trong bộ nhớ:

#include <cmath>
#include <njin.h>
#include <vector>
namespace {
// Âm thanh do game tự sinh ra lúc khởi động, không đọc từ file.
void make_beep(njin::context &ctx) {
const njin::i32 sample_rate = 44100;
const njin::i32 count = sample_rate / 5; // 0,2 giây
std::vector<njin::f32> samples((std::size_t)count);
for (njin::i32 i = 0; i < count; i++) {
const njin::f32 t = (njin::f32)i / (njin::f32)sample_rate;
const njin::f32 fade = 1.0f - (njin::f32)i / (njin::f32)count; // tắt dần
samples[(std::size_t)i] = 0.4f * fade * std::sin(2.0f * 3.14159265f * 880.0f * t);
}
// Mono, số thực 32 bit. Dữ liệu được chép đi, nên `samples` bị hủy ngay sau
// đó cũng không sao.
beep = njin::sound_load_samples(ctx, samples.data(), count, sample_rate);
}
void setup(njin::context &ctx) {
}
} // namespace
njin::mod_desc procedural_audio_module() {
return {.name = "procedural_audio", .setup = setup};
}
sound_handle sound_load_samples(context &ctx, const f32 *samples, i32 count, i32 sample_rate)
Tạo một âm thanh từ các mẫu đã có trong bộ nhớ.
std::int32_t i32
Số nguyên có dấu 32 bit.
Definition _types.h:14

Music

njin::music_load() không nạp cả file mà mở một stream. Vòng lặp và bài hát nhạc nền thường dùng cách này.

Hàm Việc làm
njin::music_play() Phát từ đầu, kể cả khi đang tạm dừng hoặc đang phát
njin::music_stop() Dừng và đưa về đầu bài
njin::music_pause() / njin::music_resume() Tạm dừng và phát tiếp, giữ nguyên vị trí
njin::music_set_looping() Bật hoặc tắt lặp. Mặc định là bật, có hiệu lực ngay giữa bài
njin::music_set_volume() / njin::music_set_muted() Giống sound

Âm thanh 3D

Trong game 3D, tiếng phát ra từ một chỗ trong thế giới và được nghe từ một tai nghe (njin::audio_listener3d). Engine tính lại cho từng tiếng, mỗi frame:

  • Âm lượng theo khoảng cách từ tai tới nguồn, theo hướng loa (nếu có nón) và theo vật che giữa hai bên.
  • Trái phải theo hướng của nguồn so với hướng tai đang nhìn.
  • Cao độ theo hiệu ứng Doppler: nguồn và tai đi lại gần nhau thì tiếng cao lên, đi xa nhau thì trầm xuống (tiếng xe lao qua).
#include <njin.h>
namespace {
using namespace njin;
sound_handle engine;
voice3d_handle engine_voice;
void load(context &ctx) {
engine = sound_load(ctx, "assets/engine.ogg");
fire = sound_load(ctx, "assets/fire.ogg");
boom = sound_load(ctx, "assets/boom.wav");
body3d_create(ctx, {.position = {0.0f, -0.5f, 0.0f}, .size = {200.0f, 1.0f, 200.0f}});
// Một bức tường giữa camera và đống lửa: tiếng lửa nhỏ đi khi bị che.
body3d_create(ctx, {.position = {0.0f, 1.5f, -6.0f}, .size = {6.0f, 3.0f, 0.3f}});
car = vehicle3d_create(ctx, {.position = {4.0f, 1.0f, 0.0f}});
// Lửa tí tách đứng yên một chỗ: nghe trong 25 m, bị tường che thì còn 30%.
sound_loop3d(ctx, fire, vec3{0.0f, 0.5f, -10.0f},
{.max_distance = 25.0f, .occlusion = true, .occlusion_volume = 0.3f});
// Tiếng máy: lặp, vị trí và vận tốc lấy từ thân xe mỗi frame (ở update).
// Thân xe là body của chính nó nên tia che bỏ qua 2,5 m quanh nguồn.
engine_voice = sound_loop3d(ctx, engine, vec3{4.0f, 1.0f, 0.0f},
{.volume = 0.6f, .max_distance = 60.0f, .occlusion = true, .occlusion_margin = 2.5f});
}
void update(context &ctx) {
const body3d_handle body = vehicle3d_body(ctx, car);
voice3d_set_position(ctx, engine_voice, body3d_transform(ctx, body).position);
// Vận tốc thật của body cho Doppler, thay cho vận tốc engine đo từ quãng đi.
voice3d_set_velocity(ctx, engine_voice, body3d_velocity(ctx, body));
// Cao độ theo vòng tua máy: 1 ở 1000 vòng, 2 ở 6000 vòng.
sound3d_desc d = voice3d_desc(ctx, engine_voice);
d.pitch = 0.8f + vehicle3d_rpm(ctx, car) / 5000.0f;
voice3d_set_desc(ctx, engine_voice, d);
// Một tiếng nổ một lần ở chỗ cách xe 10 m phía trước.
if (key_pressed(ctx, key_b))
sound_play3d(ctx, boom, body3d_transform(ctx, body).position + vec3{0.0f, 0.0f, -10.0f},
{.min_distance = 3.0f, .max_distance = 120.0f});
}
void render(context &ctx) {
// Tai nghe đi theo camera này: không cần gọi gì thêm.
begin_3d(ctx, {.position = {0.0f, 6.0f, 14.0f}, .target = {0.0f, 0.0f, 0.0f}});
draw_plane3d(ctx, {0.0f, 0.0f, 0.0f}, {200.0f, 200.0f}, colors::gray);
end_3d(ctx);
}
void setup(context &ctx) {
ecs_register(ctx, phase_update, update);
ecs_register(ctx, phase_render, render);
}
} // namespace
njin::mod_desc audio3d_demo_module() { return {.name = "audio3d_demo", .setup = setup}; }
void begin_3d(context &ctx, const camera3d &camera)
Bắt đầu vẽ 3D qua camera.
void draw_plane3d(const context &ctx, vec3 center, vec2 size, rgba color)
Vẽ mặt phẳng nằm ngang (song song mặt xz), mặt trên hướng +y.
void end_3d(context &ctx)
Kết thúc vẽ 3D và quay về vẽ 2D trong không gian thế giới.
@ key_b
Phím B.
Definition _types.h:345
@ phase_render
Mỗi frame, vẽ trong không gian thế giới.
Definition _mod.h:38
f32 vehicle3d_rpm(const context &ctx, vehicle3d_handle handle)
Vòng tua động cơ lúc này, cho tiếng máy và đồng hồ.
vec3 body3d_velocity(const context &ctx, body3d_handle handle)
Vận tốc thẳng của body, đơn vị mỗi giây.
body3d_handle vehicle3d_body(const context &ctx, vehicle3d_handle handle)
Thân xe, để đọc vị trí (body3d_transform()), vận tốc, hay đẩy nó.
body3d_handle body3d_create(context &ctx, const body3d_desc &desc)
Tạo một body vật lý 3D.
transform3d body3d_transform(const context &ctx, body3d_handle handle)
Vị trí và góc xoay hiện tại của body, để vẽ nó (scale luôn là 1).
vehicle3d_handle vehicle3d_create(context &ctx, const vehicle3d_desc &desc)
Tạo một xe có bánh: thân xe là body động, mỗi bánh là một giảm xóc dò mặt đất (không phải body),...
void voice3d_set_desc(context &ctx, voice3d_handle voice, const sound3d_desc &desc)
Đổi cách nghe của tiếng đang phát: âm lượng theo ga của xe, cao độ theo vòng tua máy (vehicle3d_rpm()...
void voice3d_set_position(context &ctx, voice3d_handle voice, vec3 position)
Dời tiếng tới position, và thôi đi theo entity nếu đang đi theo.
void voice3d_set_velocity(context &ctx, voice3d_handle voice, vec3 velocity)
Đặt vận tốc của nguồn cho Doppler.
sound3d_desc voice3d_desc(const context &ctx, voice3d_handle voice)
Cách nghe đang dùng của tiếng.
voice3d_handle sound_loop3d(context &ctx, sound_handle handle, vec3 position, const sound3d_desc &desc={})
Phát lặp handle tại position cho đến khi voice3d_stop() (hoặc sound_stop() cho sound đó): tiếng máy n...
voice3d_handle sound_play3d(context &ctx, sound_handle handle, vec3 position, const sound3d_desc &desc={})
Phát một lần handle tại position, nghe từ tai nghe 3D theo desc.
constexpr rgba gray
Xám.
Definition _math.h:240
Định danh của một body vật lý 3D, tạo bởi body3d_create().
Definition _types.h:160
Cách nghe một tiếng 3D.
Definition njin_audio.h:307
f32 pitch
Cao độ trước Doppler. 1 là như bản ghi.
Definition njin_audio.h:309
Vector 3 chiều.
Definition _types.h:47
Định danh của một xe có bánh, tạo bởi vehicle3d_create().
Definition _types.h:222
Định danh của một tiếng đang phát trong không gian 3D, tạo bởi sound_play3d() hoặc sound_loop3d().
Definition _types.h:270

Tai nghe

Mặc định tai đi theo camera của lần njin::begin_3d() vẽ ra màn hình gần nhất: cùng vị trí, cùng hướng nhìn, và vận tốc engine đo từ quãng camera đi mỗi frame. Game 3D thường không phải gọi gì.

Khi tai không ở camera (góc nhìn thứ ba mà muốn nghe từ nhân vật), đặt tay bằng njin::audio_set_listener3d(); từ lúc đó tai thôi theo camera, cho đến khi gọi njin::audio_listener3d_follow_camera(). Lần begin_3d() vẽ vào render texture (model xoay trong menu) không đổi tai.

Phát

Hàm Hành vi
njin::sound_play3d() Phát một lần tại một điểm, hoặc đi theo một entity có njin::transform3d
njin::sound_loop3d() Phát lặp cho đến khi njin::voice3d_stop(): máy nổ, lửa, thác nước

Cả hai trả về một njin::voice3d_handle cho riêng tiếng đó. Dùng nó để dời tiếng (njin::voice3d_set_position(), njin::voice3d_attach()), đặt vận tốc cho Doppler (njin::voice3d_set_velocity()), đổi cách nghe lúc đang phát (njin::voice3d_set_desc(): cao độ theo vòng tua máy, âm lượng theo ga) và dừng nó. Tiếng phát xong thì handle tự hết hiệu lực; ô của nó được dùng lại nhưng handle cũ không bao giờ trỏ nhầm sang tiếng mới.

Tiếng đi theo entity: entity bị hủy thì tiếng một lần phát nốt ở chỗ cuối cùng, còn tiếng lặp dừng luôn. njin::sound_stop() cho một sound cũng dừng mọi tiếng 3D của sound đó.

Tối đa 64 tiếng 3D cùng lúc. Quá số đó thì tiếng một lần đã chạy lâu nhất bị cắt; tiếng lặp không bao giờ bị cắt.

Khoảng cách

njin::sound3d_desc cho biết tiếng nghe được tới đâu:

Trường Ý nghĩa
min_distance Gần hơn thì nghe đủ to. Mặc định 1
max_distance Xa hơn thì im. Mặc định 40
rolloff Cách giảm ở giữa (bảng dưới)
rolloff_factor Độ dốc: 2 là tắt nhanh hơn, 0.5 là vang xa hơn
rolloff Âm lượng ở khoảng cách d Hợp với
njin::rolloff_inverse min / (min + factor * (d - min)): như ngoài đời Mặc định, hầu hết mọi tiếng
njin::rolloff_linear Giảm đều từ 1 đến 0 giữa min_distance và max_distance Khi cần biết chắc tiếng nghe được tới đâu
njin::rolloff_exponential (d / min)^-factor: tắt nhanh hơn Tiếng nhỏ, chỉ nghe khi đứng sát

Inverse và exponential tự chúng không bao giờ về 0, nên ở 10% cuối trước max_distance engine cho chúng nhỏ dần về 0: tiếng đi ra khỏi tầm không bị tắt đột ngột.

Trái phải, Doppler, nón

  • spread (0..1) là độ lệch trái phải: 1 là tiếng bên phải nghe hẳn ở loa phải, 0 là luôn ở giữa. Gần tai hơn min_distance thì tiếng tự về giữa dần, để tiếng ngay trên đầu không nhảy từ loa này sang loa kia.
  • doppler (0 là tắt, 1 là như ngoài đời). Vận tốc của tiếng mặc định do engine đo từ quãng nó đi mỗi frame, làm mượt trong khoảng 0,1 giây; có vận tốc thật (njin::body3d_velocity()) thì đưa vào bằng njin::voice3d_set_velocity(). Tốc độ âm thanh mặc định 343 đơn vị mỗi giây (một đơn vị là một mét), đổi bằng njin::audio_set_speed_of_sound().
  • Nón: loa có hướng (còi xe, loa phóng thanh) đặt cone_direction. Trong góc cone_inner quanh hướng đó thì đủ to, ngoài cone_outer thì còn cone_outer_volume, ở giữa thì chuyển dần. cone_direction bằng 0 (mặc định) là phát đều mọi hướng.

Vật che

Bật occlusion thì mỗi frame engine bắn một tia vật lý (njin::physics3d_raycast()) từ tai tới nguồn. Tia chạm một body ở trước nguồn thì tiếng nhỏ lại còn occlusion_volume, chuyển mượt trong khoảng 0,15 giây. Chỗ chạm cách nguồn trong occlusion_margin thì không tính: thân xe không che tiếng máy của chính nó.

Ghi chú
Vật che chỉ làm tiếng nhỏ đi, không làm tiếng đục đi (không lọc âm cao). raylib trộn từng tiếng bằng âm lượng, trái phải và cao độ, không có bộ lọc riêng cho mỗi tiếng.

Âm lượng cuối cùng

Âm lượng của một tiếng 3D là tích của: âm lượng riêng của sound (njin::sound_set_volume()), kênh của nó và bus_master (njin::audio_set_bus_volume(), xem Cài đặt của người chơi), volume trong njin::sound3d_desc, khoảng cách, nón và vật che. Đổi âm lượng kênh trong menu cài đặt thì tiếng 3D đang phát nhận ngay ở frame sau.

njin::voice3d_state() trả về những gì engine vừa tính cho một tiếng (âm lượng, trái phải, cao độ, khoảng cách, có bị che không), để debug hoặc vẽ chỉ báo tiếng động trên màn hình.

Module âm thanh của engine

Có một module lõi tên njin.audio luôn được đăng ký sẵn. Mỗi frame, ở phase_post_update, nó làm ba việc:

  • Cấp dữ liệu cho các stream nhạc. Nhạc giải mã từng đoạn nhỏ và sẽ im lặng ngay sau đoạn đầu nếu không có việc này.
  • Phát lại các sound lặp đã kết thúc. Đó là lý do njin::sound_play_loop() chỉ cần gọi một lần.
  • Cập nhật các tiếng 3D: dời tai theo camera, dời tiếng theo entity, tính lại âm lượng, trái phải và cao độ, phát lại tiếng 3D lặp và giải phóng tiếng một lần đã phát xong.

Bạn không cần gọi gì để bật nó. Xem Kiến trúc bên trong để biết module lõi hoạt động thế nào.

Ghi chú
Vì sound lặp được phát lại sau khi nó kết thúc, chỗ nối có một quãng lặng cỡ một frame. Với tiếng nền ngắn thì không đáng kể. Với nhạc nền dài, dùng njin::music_load().

Giải phóng

Tài nguyên được giải phóng tự động khi gọi njin::destroy(). Chỉ gọi njin::sound_unload() hoặc njin::music_unload() khi muốn giải phóng sớm. Thiết bị âm thanh được mở cùng cửa sổ và đóng sau khi mọi tài nguyên đã được giải phóng.