add audio

This commit is contained in:
bQUARKz 2026-01-14 17:55:06 +00:00 committed by Nilton Constantino
parent 4906a53e65
commit 356f70a435
No known key found for this signature in database
11 changed files with 861 additions and 49 deletions

324
Cargo.lock generated
View File

@ -31,12 +31,43 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "alsa"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
dependencies = [
"alsa-sys",
"bitflags 2.10.0",
"cfg-if",
"libc",
]
[[package]]
name = "alsa-sys"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "android-activity"
version = "0.6.0"
@ -51,7 +82,7 @@ dependencies = [
"jni-sys",
"libc",
"log",
"ndk",
"ndk 0.9.0",
"ndk-context",
"ndk-sys 0.6.0+11769913",
"num_enum",
@ -112,6 +143,24 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags 2.10.0",
"cexpr",
"clang-sys",
"itertools",
"proc-macro2",
"quote",
"regex",
"rustc-hash 2.1.1",
"shlex",
"syn 2.0.114",
]
[[package]]
name = "bit-set"
version = "0.5.3"
@ -220,6 +269,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
@ -238,6 +296,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading 0.8.9",
]
[[package]]
name = "codespan-reporting"
version = "0.11.1"
@ -338,6 +407,49 @@ dependencies = [
"libc",
]
[[package]]
name = "coreaudio-rs"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace"
dependencies = [
"bitflags 1.3.2",
"core-foundation-sys",
"coreaudio-sys",
]
[[package]]
name = "coreaudio-sys"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6"
dependencies = [
"bindgen",
]
[[package]]
name = "cpal"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
dependencies = [
"alsa",
"core-foundation-sys",
"coreaudio-rs",
"dasp_sample",
"jni",
"js-sys",
"libc",
"mach2",
"ndk 0.8.0",
"ndk-context",
"oboe",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows 0.54.0",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@ -361,6 +473,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "dasp_sample"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
[[package]]
name = "dispatch"
version = "0.2.0"
@ -388,6 +506,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "equivalent"
version = "1.0.2"
@ -401,7 +525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -470,6 +594,12 @@ dependencies = [
"xml-rs",
]
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "glow"
version = "0.13.1"
@ -520,7 +650,7 @@ dependencies = [
"presser",
"thiserror",
"winapi",
"windows",
"windows 0.52.0",
]
[[package]]
@ -590,8 +720,10 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
name = "host_desktop"
version = "0.1.0"
dependencies = [
"cpal",
"pixels",
"prometeu-core",
"ringbuf",
"winit",
]
@ -605,6 +737,15 @@ dependencies = [
"hashbrown 0.16.1",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "jni"
version = "0.21.1"
@ -728,6 +869,15 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "mach2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
dependencies = [
"libc",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@ -767,6 +917,12 @@ dependencies = [
"paste",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "naga"
version = "0.19.2"
@ -780,13 +936,27 @@ dependencies = [
"indexmap",
"log",
"num-traits",
"rustc-hash",
"rustc-hash 1.1.0",
"spirv",
"termcolor",
"thiserror",
"unicode-xid",
]
[[package]]
name = "ndk"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
dependencies = [
"bitflags 2.10.0",
"jni-sys",
"log",
"ndk-sys 0.5.0+25.2.9519653",
"num_enum",
"thiserror",
]
[[package]]
name = "ndk"
version = "0.9.0"
@ -826,6 +996,27 @@ dependencies = [
"jni-sys",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1079,6 +1270,29 @@ dependencies = [
"cc",
]
[[package]]
name = "oboe"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
dependencies = [
"jni",
"ndk 0.8.0",
"ndk-context",
"num-derive",
"num-traits",
"oboe-sys",
]
[[package]]
name = "oboe-sys"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d"
dependencies = [
"cc",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@ -1205,6 +1419,21 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2"
[[package]]
name = "portable-atomic"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "presser"
version = "0.3.1"
@ -1302,18 +1531,64 @@ dependencies = [
"bitflags 2.10.0",
]
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "renderdoc-sys"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "ringbuf"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c"
dependencies = [
"crossbeam-utils",
"portable-atomic",
"portable-atomic-util",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustix"
version = "0.38.44"
@ -1337,7 +1612,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -1915,7 +2190,7 @@ dependencies = [
"parking_lot",
"profiling",
"raw-window-handle",
"rustc-hash",
"rustc-hash 1.1.0",
"smallvec",
"thiserror",
"web-sys",
@ -1959,7 +2234,7 @@ dependencies = [
"range-alloc",
"raw-window-handle",
"renderdoc-sys",
"rustc-hash",
"rustc-hash 1.1.0",
"smallvec",
"thiserror",
"wasm-bindgen",
@ -2017,7 +2292,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -2032,7 +2307,17 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
dependencies = [
"windows-core",
"windows-core 0.52.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
dependencies = [
"windows-core 0.54.0",
"windows-targets 0.52.6",
]
@ -2045,12 +2330,31 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
dependencies = [
"windows-result",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
@ -2230,7 +2534,7 @@ dependencies = [
"js-sys",
"libc",
"memmap2",
"ndk",
"ndk 0.9.0",
"objc2",
"objc2-app-kit",
"objc2-foundation",

View File

@ -1,14 +1,22 @@
use crate::model::Color;
use crate::peripherals::{Gfx, InputSignals, Pad, Touch};
use crate::peripherals::{Gfx, InputSignals, Pad, Touch, Audio, LoopMode};
use std::sync::Arc;
use crate::model::Sample;
/// PROMETEU "hardware lógico" (v0).
/// O Host alimenta INPUT SIGNALS e chama `step_frame()` em 60Hz.
pub struct Machine {
pub gfx: Gfx,
pub audio: Audio,
pub pad: Pad,
pub touch: Touch,
pub frame_index: u64,
pub last_frame_cpu_time_us: u64,
// Assets de exemplo
pub sample_square: Option<Arc<Sample>>,
pub sample_kick: Option<Arc<Sample>>,
pub sample_snare: Option<Arc<Sample>>,
}
impl Machine {
@ -18,10 +26,14 @@ impl Machine {
pub fn new() -> Self {
Self {
gfx: Gfx::new(Self::W, Self::H),
audio: Audio::new(),
pad: Pad::default(),
touch: Touch::default(),
frame_index: 0,
last_frame_cpu_time_us: 0,
sample_square: None,
sample_kick: None,
sample_snare: None,
}
}
@ -40,6 +52,11 @@ impl Machine {
pub fn begin_frame(&mut self, signals: &InputSignals) {
self.frame_index += 1;
// Limpa comandos de áudio do frame anterior.
// Nota: O Host deve consumir esses comandos ANTES ou DURANTE o step_frame se quiser processá-los.
// Como o Host atual consome logo após o step_frame, limpar aqui no início do PRÓXIMO frame está correto.
self.audio.clear_commands();
// Flags transitórias do TOUCH devem durar 1 frame.
self.touch.begin_frame(signals);
@ -178,6 +195,91 @@ impl Machine {
// Se soltar o Start, podemos pausar algo futuramente
}
// --- DEMO DE ÁUDIO ---
// Inicializa assets de áudio se não existirem
if self.sample_square.is_none() {
// 1. Onda Quadrada com Loop (Instrumento)
let freq = 440.0;
let sample_rate = 44100;
let period = (sample_rate as f32 / freq) as usize;
let mut data = Vec::with_capacity(period);
for i in 0..period {
data.push(if i < period / 2 { 4000 } else { -4000 });
}
self.sample_square = Some(Arc::new(Sample::new(sample_rate, data).with_loop(0, period as u32)));
// 2. "Kick" simples (Pitch descent)
let mut kick_data = Vec::new();
for i in 0..4000 {
let t = i as f32 / 4000.0;
let f = 100.0 * (1.0 - t * 0.9);
let val = (i as f32 * f * 2.0 * std::f32::consts::PI / 44100.0).sin();
kick_data.push((val * 12000.0 * (1.0 - t)) as i16);
}
self.sample_kick = Some(Arc::new(Sample::new(44100, kick_data)));
// 3. "Snare" simples (White noise)
let mut snare_data = Vec::new();
for i in 0..3000 {
let t = i as f32 / 3000.0;
let noise = Self::rand_f32(i as u32) * 2.0 - 1.0;
snare_data.push((noise * 8000.0 * (1.0 - t)) as i16);
}
self.sample_snare = Some(Arc::new(Sample::new(44100, snare_data)));
}
// --- SEQUENCER SIMPLES (60Hz) ---
let bpm = 125;
let frames_per_beat = (60 * 60) / bpm;
let frames_per_step = frames_per_beat / 4; // 16th notes
let step = (self.frame_index / frames_per_step) % 16;
let is_new_step = self.frame_index % frames_per_step == 0;
if is_new_step {
// Bassline (Square)
// Notas: C-2, C-2, Eb2, F-2, C-2, C-2, G-2, Bb2...
let melody = [
36, 36, 39, 41,
36, 36, 43, 46,
36, 36, 39, 41,
34, 35, 36, 0,
];
let note = melody[step as usize];
if note > 0 {
// Cálculo de pitch otimizado (tabela simples ou aproximação rápida se necessário,
// mas powf uma vez por step está ok. O problema é se fosse todo frame)
let freq = 440.0 * 2.0f64.powf((note as f64 - 69.0) / 12.0);
let pitch = freq / 440.0;
if let Some(s) = &self.sample_square {
self.audio.play(s.clone(), 0, 160, 100, pitch, 0, LoopMode::On);
}
}
// Drums
let kick_pattern = 0b0101100100011001u16; // Invertido para facilitar bitshift
let snare_pattern = 0b0001000000001000u16;
if (kick_pattern >> step) & 1 == 1 {
if let Some(s) = &self.sample_kick {
self.audio.play(s.clone(), 1, 255, 127, 1.0, 1, LoopMode::Off);
}
}
if (snare_pattern >> step) & 1 == 1 {
if let Some(s) = &self.sample_snare {
self.audio.play(s.clone(), 2, 140, 150, 1.0, 1, LoopMode::Off);
}
}
}
// Toca um "beep" extra se apertar A (mantendo interatividade)
if self.pad.a.pressed {
if let Some(s) = &self.sample_square {
self.audio.play(s.clone(), 3, 255, 60, 2.0, 10, LoopMode::Off);
}
}
// Post-FX Fade Pulsante
let pulse = (self.frame_index / 4) % 64;
let level = if pulse > 31 { 63 - pulse } else { pulse };
@ -190,4 +292,15 @@ impl Machine {
pub fn end_frame(&mut self) {
self.gfx.present();
}
/// Helper para noise determinístico
fn rand_f32(seed: u32) -> f32 {
let mut x = seed.wrapping_add(0x9E3779B9);
x = x ^ (x >> 16);
x = x.wrapping_mul(0x85EBCA6B);
x = x ^ (x >> 13);
x = x.wrapping_mul(0xC2B2AE35);
x = x ^ (x >> 16);
(x as f32) / (u32::MAX as f32)
}
}

View File

@ -4,6 +4,7 @@ mod tile;
mod tile_layer;
mod tile_bank;
mod sprite;
mod sample;
pub use button::Button;
pub use color::Color;
@ -11,3 +12,4 @@ pub use tile::Tile;
pub use tile_bank::{TileBank, TileSize};
pub use tile_layer::{HudTileLayer, ScrollableTileLayer, TileMap};
pub use sprite::Sprite;
pub use sample::Sample;

View File

@ -0,0 +1,27 @@
pub struct Sample {
pub sample_rate: u32,
pub data: Vec<i16>,
pub loop_start: Option<u32>,
pub loop_end: Option<u32>,
}
impl Sample {
pub fn new(sample_rate: u32, data: Vec<i16>) -> Self {
Self {
sample_rate,
data,
loop_start: None,
loop_end: None,
}
}
pub fn with_loop(mut self, start: u32, end: u32) -> Self {
self.loop_start = Some(start);
self.loop_end = Some(end);
self
}
pub fn frames_len(&self) -> usize {
self.data.len()
}
}

View File

@ -0,0 +1,127 @@
use crate::model::Sample;
use std::sync::Arc;
pub const MAX_CHANNELS: usize = 16;
pub const OUTPUT_SAMPLE_RATE: u32 = 48000;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum LoopMode {
Off,
On,
}
pub struct Channel {
pub sample: Option<Arc<Sample>>,
pub pos: f64,
pub pitch: f64,
pub volume: u8, // 0..255
pub pan: u8, // 0..255
pub loop_mode: LoopMode,
pub priority: u8,
}
impl Default for Channel {
fn default() -> Self {
Self {
sample: None,
pos: 0.0,
pitch: 1.0,
volume: 255,
pan: 127,
loop_mode: LoopMode::Off,
priority: 0,
}
}
}
pub enum AudioCommand {
Play {
sample: Arc<Sample>,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: LoopMode,
},
Stop {
voice_id: usize,
},
SetVolume {
voice_id: usize,
volume: u8,
},
SetPan {
voice_id: usize,
pan: u8,
},
SetPitch {
voice_id: usize,
pitch: f64,
},
}
pub struct Audio {
pub voices: [Channel; MAX_CHANNELS],
pub commands: Vec<AudioCommand>,
}
impl Audio {
pub fn new() -> Self {
Self {
voices: Default::default(),
commands: Vec::new(),
}
}
pub fn play(&mut self, sample: Arc<Sample>, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) {
if voice_id < MAX_CHANNELS {
self.commands.push(AudioCommand::Play {
sample,
voice_id,
volume,
pan,
pitch,
priority,
loop_mode,
});
}
}
pub fn stop(&mut self, voice_id: usize) {
if voice_id < MAX_CHANNELS {
self.commands.push(AudioCommand::Stop { voice_id });
}
}
pub fn set_volume(&mut self, voice_id: usize, volume: u8) {
if voice_id < MAX_CHANNELS {
self.commands.push(AudioCommand::SetVolume { voice_id, volume });
}
}
pub fn set_pan(&mut self, voice_id: usize, pan: u8) {
if voice_id < MAX_CHANNELS {
self.commands.push(AudioCommand::SetPan { voice_id, pan });
}
}
pub fn set_pitch(&mut self, voice_id: usize, pitch: f64) {
if voice_id < MAX_CHANNELS {
self.commands.push(AudioCommand::SetPitch { voice_id, pitch });
}
}
pub fn is_playing(&self, voice_id: usize) -> bool {
if voice_id < MAX_CHANNELS {
self.voices[voice_id].sample.is_some()
} else {
false
}
}
/// Limpa a fila de comandos. O Host deve consumir isso a cada frame.
pub fn clear_commands(&mut self) {
self.commands.clear();
}
}

View File

@ -30,6 +30,9 @@ pub struct Gfx {
pub scene_fade_color: Color,
pub hud_fade_level: u8, // 0..31
pub hud_fade_color: Color,
// Cache de prioridades para evitar alocações por frame
priority_buckets: [Vec<usize>; 5],
}
impl Gfx {
@ -65,6 +68,13 @@ impl Gfx {
scene_fade_color: Color::BLACK,
hud_fade_level: 31,
hud_fade_color: Color::BLACK,
priority_buckets: [
Vec::with_capacity(128),
Vec::with_capacity(128),
Vec::with_capacity(128),
Vec::with_capacity(128),
Vec::with_capacity(128),
],
}
}
@ -125,33 +135,29 @@ impl Gfx {
/// Segue a ordem de prioridade do manual (Capítulo 4.11).
pub fn render_all(&mut self) {
// 0. Fase de Preparação: Organiza quem deve ser desenhado em cada camada
// Criamos listas de índices para evitar percorrer os 512 sprites 5 vezes
let mut priority_buckets: [Vec<usize>; 5] = [
Vec::with_capacity(128),
Vec::with_capacity(128),
Vec::with_capacity(128),
Vec::with_capacity(128),
Vec::with_capacity(128),
];
// Limpa os buckets sem desalocar memória
for bucket in self.priority_buckets.iter_mut() {
bucket.clear();
}
for (idx, sprite) in self.sprites.iter().enumerate() {
if sprite.active && sprite.priority < 5 {
priority_buckets[sprite.priority as usize].push(idx);
self.priority_buckets[sprite.priority as usize].push(idx);
}
}
// 1. Sprites de prioridade 0 (Atrás da Layer 0 - o fundo do fundo)
self.draw_bucket(&priority_buckets[0]);
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[0], &self.sprites, &self.banks);
for i in 0..self.layers.len() {
// 2. Layers de jogo (0 a 3)
self.render_layer(i);
let bank_id = self.layers[i].bank_id as usize;
if let Some(Some(bank)) = self.banks.get(bank_id) {
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[i].map, bank, self.layers[i].scroll_x, self.layers[i].scroll_y);
}
// 3. Sprites de acordo com prioridade
// i=0 desenha sprites priority 1 (sobre layer 0)
// i=1 desenha sprites priority 2 (sobre layer 1)
// i=2 desenha sprites priority 3 (sobre layer 2)
// i=3 desenha sprites priority 4 (sobre layer 3 - à frente de tudo)
self.draw_bucket(&priority_buckets[i + 1]);
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[i + 1], &self.sprites, &self.banks);
}
// 4. Aplica Scene Fade (Afeta tudo desenhado até agora)
@ -262,15 +268,20 @@ impl Gfx {
}
}
fn draw_bucket(&mut self, bucket: &[usize]) {
fn draw_bucket_on_buffer(
back: &mut [u16],
screen_w: usize,
screen_h: usize,
bucket: &[usize],
sprites: &[Sprite],
banks: &[Option<TileBank>],
) {
for &idx in bucket {
let sprite = &self.sprites[idx];
let bank = match self.banks.get(sprite.bank_id as usize) {
Some(Some(b)) => b,
_ => continue,
};
Self::draw_sprite_pixel_by_pixel(&mut self.back, self.w, self.h, sprite, bank);
let s = &sprites[idx];
let bank_id = s.bank_id as usize;
if let Some(Some(bank)) = banks.get(bank_id) {
Self::draw_sprite_pixel_by_pixel(back, screen_w, screen_h, s, bank);
}
}
}

View File

@ -2,9 +2,11 @@ mod gfx;
mod pad;
mod touch;
mod input_signal;
mod audio;
pub use gfx::Gfx;
pub use gfx::BlendMode;
pub use input_signal::InputSignals;
pub use pad::Pad;
pub use touch::Touch;
pub use audio::{Audio, Channel, AudioCommand, LoopMode, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};

View File

@ -6,4 +6,6 @@ edition = "2021"
[dependencies]
prometeu-core = { path = "../core" }
winit = "0.30.12"
pixels = "0.15.0"
pixels = "0.15.0"
cpal = "0.15.3"
ringbuf = "0.4.7"

View File

@ -0,0 +1,132 @@
use prometeu_core::peripherals::{AudioCommand, Channel, LoopMode, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
use std::time::Duration;
pub struct AudioMixer {
voices: [Channel; MAX_CHANNELS],
pub last_processing_time: Duration,
}
impl AudioMixer {
pub fn new() -> Self {
Self {
voices: Default::default(),
last_processing_time: Duration::ZERO,
}
}
pub fn process_command(&mut self, cmd: AudioCommand) {
match cmd {
AudioCommand::Play {
sample,
voice_id,
volume,
pan,
pitch,
priority,
loop_mode,
} => {
if voice_id < MAX_CHANNELS {
self.voices[voice_id] = Channel {
sample: Some(sample),
pos: 0.0,
pitch,
volume,
pan,
loop_mode,
priority,
};
}
}
AudioCommand::Stop { voice_id } => {
if voice_id < MAX_CHANNELS {
self.voices[voice_id].sample = None;
}
}
AudioCommand::SetVolume { voice_id, volume } => {
if voice_id < MAX_CHANNELS {
self.voices[voice_id].volume = volume;
}
}
AudioCommand::SetPan { voice_id, pan } => {
if voice_id < MAX_CHANNELS {
self.voices[voice_id].pan = pan;
}
}
AudioCommand::SetPitch { voice_id, pitch } => {
if voice_id < MAX_CHANNELS {
self.voices[voice_id].pitch = pitch;
}
}
}
}
pub fn fill_buffer(&mut self, buffer: &mut [f32]) {
let start = std::time::Instant::now();
// Zera o buffer (estéreo)
for sample in buffer.iter_mut() {
*sample = 0.0;
}
for voice in self.voices.iter_mut() {
let sample_data = match &voice.sample {
Some(s) => s,
None => continue,
};
let pitch_ratio = sample_data.sample_rate as f64 / OUTPUT_SAMPLE_RATE as f64;
let step = voice.pitch * pitch_ratio;
let vol_f = voice.volume as f32 / 255.0;
let pan_f = voice.pan as f32 / 255.0;
let vol_l = vol_f * (1.0 - pan_f).sqrt();
let vol_r = vol_f * pan_f.sqrt();
for frame in buffer.chunks_exact_mut(2) {
let pos_int = voice.pos as usize;
let pos_fract = voice.pos - pos_int as f64;
if pos_int >= sample_data.data.len() {
voice.sample = None;
break;
}
// Interpolação Linear
let s1 = sample_data.data[pos_int] as f32 / 32768.0;
let s2 = if pos_int + 1 < sample_data.data.len() {
sample_data.data[pos_int + 1] as f32 / 32768.0
} else if voice.loop_mode == LoopMode::On {
let loop_start = sample_data.loop_start.unwrap_or(0) as usize;
sample_data.data[loop_start] as f32 / 32768.0
} else {
0.0
};
let sample_val = s1 + (s2 - s1) * pos_fract as f32;
frame[0] += sample_val * vol_l;
frame[1] += sample_val * vol_r;
voice.pos += step;
let end_pos = sample_data.loop_end.map(|e| e as f64).unwrap_or(sample_data.data.len() as f64);
if voice.pos >= end_pos {
if voice.loop_mode == LoopMode::On {
let loop_start = sample_data.loop_start.unwrap_or(0) as f64;
voice.pos = loop_start + (voice.pos - end_pos);
} else {
voice.sample = None;
break;
}
}
}
}
// Clamp final para evitar clipping (opcional se usarmos f32, mas bom para fidelidade)
for sample in buffer.iter_mut() {
*sample = sample.clamp(-1.0, 1.0);
}
self.last_processing_time = start.elapsed();
}
}

View File

@ -1,8 +1,15 @@
use pixels::{Pixels, SurfaceTexture};
use std::time::{Duration, Instant};
use pixels::{Pixels, SurfaceTexture};
use prometeu_core::peripherals::InputSignals;
mod audio_mixer;
use audio_mixer::AudioMixer;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use prometeu_core::peripherals::{AudioCommand, InputSignals, OUTPUT_SAMPLE_RATE};
use prometeu_core::Machine;
use ringbuf::traits::{Consumer, Producer, Split};
use ringbuf::HeapRb;
use std::sync::Arc;
use winit::{
application::ApplicationHandler,
dpi::LogicalSize,
@ -35,6 +42,12 @@ struct PrometeuApp {
last_stats_update: Instant,
frames_since_last_update: u64,
audio_load_accum_us: u64,
audio_load_samples: u64,
audio_perf_consumer: Option<ringbuf::wrap::CachingCons<Arc<HeapRb<u64>>>>,
audio_producer: Option<ringbuf::wrap::CachingProd<Arc<HeapRb<AudioCommand>>>>,
_audio_stream: Option<cpal::Stream>,
}
impl PrometeuApp {
@ -55,9 +68,61 @@ impl PrometeuApp {
last_stats_update: Instant::now(),
frames_since_last_update: 0,
audio_load_accum_us: 0,
audio_load_samples: 0,
audio_perf_consumer: None,
audio_producer: None,
_audio_stream: None,
}
}
fn init_audio(&mut self) {
let host = cpal::default_host();
let device = host
.default_output_device()
.expect("no output device available");
let config = cpal::StreamConfig {
channels: 2,
sample_rate: cpal::SampleRate(OUTPUT_SAMPLE_RATE),
buffer_size: cpal::BufferSize::Default,
};
let rb = HeapRb::<AudioCommand>::new(1024);
let (prod, mut cons) = rb.split();
self.audio_producer = Some(prod);
let mut mixer = AudioMixer::new();
// Para passar dados de performance da thread de áudio para a principal
let audio_perf_rb = HeapRb::<u64>::new(64);
let (mut perf_prod, perf_cons) = audio_perf_rb.split();
let stream = device
.build_output_stream(
&config,
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
// Consome comandos da ringbuffer
while let Some(cmd) = cons.try_pop() {
mixer.process_command(cmd);
}
// Mixa áudio
mixer.fill_buffer(data);
// Envia tempo de processamento em microssegundos
let _ = perf_prod.try_push(mixer.last_processing_time.as_micros() as u64);
},
|err| eprintln!("audio stream error: {}", err),
None,
)
.expect("failed to build audio stream");
stream.play().expect("failed to play audio stream");
self._audio_stream = Some(stream);
self.audio_perf_consumer = Some(perf_cons);
}
fn window(&self) -> &'static Window {
self.window.expect("window not created yet")
}
@ -82,7 +147,7 @@ impl PrometeuApp {
impl ApplicationHandler for PrometeuApp {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let attrs = WindowAttributes::default()
.with_title(format!("PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Frame: {}", 0.0, 0.0, 0))
.with_title(format!("PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {}% | Frame: {}", 0.0, 0.0, 0.0, 0))
.with_inner_size(LogicalSize::new(960.0, 540.0))
.with_min_inner_size(LogicalSize::new(320.0, 180.0));
@ -102,6 +167,8 @@ impl ApplicationHandler for PrometeuApp {
self.pixels = Some(pixels);
self.init_audio();
event_loop.set_control_flow(ControlFlow::Poll);
}
@ -202,10 +269,26 @@ impl ApplicationHandler for PrometeuApp {
// 🔥 O coração do determinismo: consome o tempo em fatias exatas de 60Hz
while self.accumulator >= self.frame_target_dt {
self.machine.step_frame(&self.input_signals);
// Envia comandos de áudio gerados neste frame para a thread de áudio
if let Some(producer) = &mut self.audio_producer {
for cmd in self.machine.audio.commands.drain(..) {
let _ = producer.try_push(cmd);
}
}
self.accumulator -= self.frame_target_dt;
self.frames_since_last_update += 1;
}
// Drena tempos de performance do áudio
if let Some(cons) = &mut self.audio_perf_consumer {
while let Some(us) = cons.try_pop() {
self.audio_load_accum_us += us;
self.audio_load_samples += 1;
}
}
// Atualiza estatísticas a cada 1 segundo real
let stats_elapsed = now.duration_since(self.last_stats_update);
if stats_elapsed >= Duration::from_secs(1) {
@ -214,17 +297,26 @@ impl ApplicationHandler for PrometeuApp {
let kb = self.machine.gfx.memory_usage_bytes() as f64 / 1024.0;
let frame_budget_us = 16666.0;
let cpu_load = (self.machine.last_frame_cpu_time_us as f64 / frame_budget_us) * 100.0;
let cpu_load_core = (self.machine.last_frame_cpu_time_us as f64 / frame_budget_us) * 100.0;
let cpu_load_audio = if self.audio_load_samples > 0 {
// O load real é (tempo total processando) / (tempo total de parede).
(self.audio_load_accum_us as f64 / stats_elapsed.as_micros() as f64) * 100.0
} else {
0.0
};
let title = format!(
"PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% | Frame: {}",
kb, fps, cpu_load, self.machine.frame_index
"PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% (C) + {:.1}% (A) | Frame: {}",
kb, fps, cpu_load_core, cpu_load_audio, self.machine.frame_index
);
window.set_title(&title);
}
self.last_stats_update = now;
self.frames_since_last_update = 0;
self.audio_load_accum_us = 0;
self.audio_load_samples = 0;
}
self.request_redraw();

View File

@ -101,9 +101,9 @@ O mundo gráfico é composto por:
---
## 5. Modelo Interno de uma Game Layer
## 5. Modelo Interno de uma Tile Layer
Uma Game Layer **não é um bitmap de pixels**.
Uma Tile Layer **não é um bitmap de pixels**.
Ela é composta por:
- Um **Tilemap lógico** (índices de tiles)
@ -190,7 +190,7 @@ Acesso:
Para cada frame:
1. Para cada Game Layer, em ordem:
1. Para cada Tile Layer, em ordem:
- Rasterizar tiles visíveis do cache
- Aplicar scroll, flip e transparência
- Escrever no back buffer
@ -211,10 +211,10 @@ Para cada frame:
Ordem base:
1. Game Layer 0
2. Game Layer 1
3. Game Layer 2
4. Game Layer 3
1. Tile Layer 0
2. Tile Layer 1
3. Tile Layer 2
4. Tile Layer 3
5. Sprites (por prioridade entre layers)
6. HUD Layer