Browse Source

Merge branch 'master' into feature/ir-user

# Conflicts:
#	ctru-rs/src/services/mod.rs
pull/129/head
Mark Drobnak 2 years ago
parent
commit
a832cb9b36
No known key found for this signature in database
GPG Key ID: 47A133F3BF9D03D3
  1. 3
      ctru-rs/Cargo.toml
  2. 166
      ctru-rs/examples/audio-filters.rs
  3. 4
      ctru-rs/examples/camera-image.rs
  4. 5
      ctru-rs/examples/file-explorer.rs
  5. 70
      ctru-rs/examples/gfx-3d-mode.rs
  6. 6
      ctru-rs/src/error.rs
  7. 183
      ctru-rs/src/gfx.rs
  8. 1
      ctru-rs/src/linear.rs
  9. 4
      ctru-rs/src/romfs.rs
  10. 9
      ctru-rs/src/services/mod.rs
  11. 342
      ctru-rs/src/services/ndsp/mod.rs
  12. 180
      ctru-rs/src/services/ndsp/wave.rs
  13. 4
      ctru-rs/src/services/soc.rs

3
ctru-rs/Cargo.toml

@ -20,7 +20,6 @@ pthread-3ds = { git = "https://github.com/rust3ds/pthread-3ds.git" } @@ -20,7 +20,6 @@ pthread-3ds = { git = "https://github.com/rust3ds/pthread-3ds.git" }
libc = "0.2.121"
bitflags = "1.0.0"
widestring = "0.2.2"
once_cell = "1.10.0"
[build-dependencies]
toml = "0.5"
@ -31,6 +30,8 @@ futures = "0.3" @@ -31,6 +30,8 @@ futures = "0.3"
time = "0.3.7"
tokio = { version = "1.16", features = ["rt", "time", "sync", "macros"] }
cfg-if = "1.0.0"
bytemuck = "1.12.3"
lewton = "0.10.2"
[features]
default = ["romfs", "big-stack"]

166
ctru-rs/examples/audio-filters.rs

@ -0,0 +1,166 @@ @@ -0,0 +1,166 @@
#![feature(allocator_api)]
use std::f32::consts::PI;
use ctru::linear::LinearAllocator;
use ctru::prelude::*;
use ctru::services::ndsp::{
wave::{WaveInfo, WaveStatus},
AudioFormat, InterpolationType, Ndsp, OutputMode,
};
const SAMPLE_RATE: usize = 22050;
const SAMPLES_PER_BUF: usize = SAMPLE_RATE / 10; // 2205
const BYTES_PER_SAMPLE: usize = 4;
const AUDIO_WAVE_LENGTH: usize = SAMPLES_PER_BUF * BYTES_PER_SAMPLE;
// Note Frequencies
const NOTEFREQ: [f32; 7] = [220., 440., 880., 1760., 3520., 7040., 14080.];
// The audio format is Stereo PCM16
// As such, a sample is made up of 2 "Mono" samples (2 * i16 = u32), one for each channel (left and right)
fn fill_buffer(audio_data: &mut [u8], frequency: f32) {
let formatted_data = bytemuck::cast_slice_mut::<_, [i16; 2]>(audio_data);
for (i, chunk) in formatted_data.iter_mut().enumerate() {
// This is a simple sine wave, with a frequency of `frequency` Hz, and an amplitude 30% of maximum.
let sample: f32 = (frequency * (i as f32 / SAMPLE_RATE as f32) * 2. * PI).sin();
let amplitude = 0.3 * i16::MAX as f32;
let result = (sample * amplitude) as i16;
// Stereo samples are interleaved: left and right channels.
*chunk = [result, result];
}
}
fn main() {
ctru::init();
let gfx = Gfx::init().expect("Couldn't obtain GFX controller");
let hid = Hid::init().expect("Couldn't obtain HID controller");
let apt = Apt::init().expect("Couldn't obtain APT controller");
let _console = Console::init(gfx.top_screen.borrow_mut());
let mut note: usize = 4;
// Filters
let filter_names = [
"None",
"Low-Pass",
"High-Pass",
"Band-Pass",
"Notch",
"Peaking",
];
let mut filter: i32 = 0;
// We set up two wave buffers and alternate between the two,
// effectively streaming an infinitely long sine wave.
let mut audio_data1 = Box::new_in([0u8; AUDIO_WAVE_LENGTH], LinearAllocator);
fill_buffer(audio_data1.as_mut_slice(), NOTEFREQ[4]);
let audio_data2 = audio_data1.clone();
let mut wave_info1 = WaveInfo::new(audio_data1, AudioFormat::PCM16Stereo, false);
let mut wave_info2 = WaveInfo::new(audio_data2, AudioFormat::PCM16Stereo, false);
let mut ndsp = Ndsp::init().expect("Couldn't obtain NDSP controller");
// This line isn't needed since the default NDSP configuration already sets the output mode to `Stereo`
ndsp.set_output_mode(OutputMode::Stereo);
let channel_zero = ndsp.channel(0).unwrap();
channel_zero.set_interpolation(InterpolationType::Linear);
channel_zero.set_sample_rate(SAMPLE_RATE as f32);
channel_zero.set_format(AudioFormat::PCM16Stereo);
// Output at 100% on the first pair of left and right channels.
let mut mix: [f32; 12] = [0f32; 12];
mix[0] = 1.0;
mix[1] = 1.0;
channel_zero.set_mix(&mix);
channel_zero.queue_wave(&mut wave_info1).unwrap();
channel_zero.queue_wave(&mut wave_info2).unwrap();
println!("\x1b[1;1HPress up/down to change tone frequency");
println!("\x1b[2;1HPress left/right to change filter");
println!("\x1b[4;1Hnote = {} Hz ", NOTEFREQ[note]);
println!(
"\x1b[5;1Hfilter = {} ",
filter_names[filter as usize]
);
let mut altern = true; // true is wave_info1, false is wave_info2
while apt.main_loop() {
hid.scan_input();
let keys_down = hid.keys_down();
if keys_down.contains(KeyPad::KEY_START) {
break;
} // break in order to return to hbmenu
if keys_down.intersects(KeyPad::KEY_DOWN) {
note = note.saturating_sub(1);
} else if keys_down.intersects(KeyPad::KEY_UP) {
note = std::cmp::min(note + 1, NOTEFREQ.len() - 1);
}
let mut update_params = false;
if keys_down.intersects(KeyPad::KEY_LEFT) {
filter -= 1;
filter = filter.rem_euclid(filter_names.len() as _);
update_params = true;
} else if keys_down.intersects(KeyPad::KEY_RIGHT) {
filter += 1;
filter = filter.rem_euclid(filter_names.len() as _);
update_params = true;
}
println!("\x1b[4;1Hnote = {} Hz ", NOTEFREQ[note]);
println!(
"\x1b[5;1Hfilter = {} ",
filter_names[filter as usize]
);
if update_params {
match filter {
1 => channel_zero.iir_biquad_set_params_low_pass_filter(1760., 0.707),
2 => channel_zero.iir_biquad_set_params_high_pass_filter(1760., 0.707),
3 => channel_zero.iir_biquad_set_params_band_pass_filter(1760., 0.707),
4 => channel_zero.iir_biquad_set_params_notch_filter(1760., 0.707),
5 => channel_zero.iir_biquad_set_params_peaking_equalizer(1760., 0.707, 3.),
_ => channel_zero.iir_biquad_set_enabled(false),
}
}
let current: &mut WaveInfo = if altern {
&mut wave_info1
} else {
&mut wave_info2
};
let status = current.get_status();
if let WaveStatus::Done = status {
fill_buffer(current.get_buffer_mut().unwrap(), NOTEFREQ[note]);
channel_zero.queue_wave(current).unwrap();
altern = !altern;
}
// Flush and swap framebuffers
gfx.flush_buffers();
gfx.swap_buffers();
//Wait for VBlank
gfx.wait_for_vblank();
}
}

4
ctru-rs/examples/camera-image.rs

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
use ctru::console::Console;
use ctru::gfx::{Gfx, Screen, Side};
use ctru::gfx::{Gfx, Screen};
use ctru::services::cam::{Cam, CamOutputFormat, CamShutterSoundType, CamSize, Camera};
use ctru::services::hid::KeyPad;
use ctru::services::{Apt, Hid};
@ -88,7 +88,7 @@ fn main() { @@ -88,7 +88,7 @@ fn main() {
unsafe {
gfx.top_screen
.borrow_mut()
.get_raw_framebuffer(Side::Left)
.get_raw_framebuffer()
.ptr
.copy_from(img.as_ptr(), img.len());
}

5
ctru-rs/examples/file-explorer.rs

@ -32,8 +32,9 @@ struct FileExplorer<'a> { @@ -32,8 +32,9 @@ struct FileExplorer<'a> {
impl<'a> FileExplorer<'a> {
fn init(apt: &'a Apt, hid: &'a Hid, gfx: &'a Gfx) -> Self {
gfx.top_screen.borrow_mut().set_wide_mode(true);
let console = Console::init(gfx.top_screen.borrow_mut());
let mut top_screen = gfx.top_screen.borrow_mut();
top_screen.set_wide_mode(true);
let console = Console::init(top_screen);
FileExplorer {
apt,

70
ctru-rs/examples/gfx-3d-mode.rs

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
use ctru::gfx::{Screen, Side, TopScreen3D};
use ctru::prelude::*;
/// See `graphics-bitmap.rs` for details on how the image is generated.
///
/// WARNING: this example uses 3D mode in a rather unnatural way, and should
/// probably not be viewed for too long or at all if you are photosensitive.
const IMAGE: &[u8] = include_bytes!("assets/ferris.rgb");
static ZERO: &[u8] = &[0; IMAGE.len()];
fn main() {
ctru::init();
let gfx = Gfx::init().expect("Couldn't obtain GFX controller");
let hid = Hid::init().expect("Couldn't obtain HID controller");
let apt = Apt::init().expect("Couldn't obtain APT controller");
let _console = Console::init(gfx.bottom_screen.borrow_mut());
println!("Press Start to exit.\nPress A to switch sides (be sure to have 3D mode enabled).");
gfx.top_screen.borrow_mut().set_double_buffering(true);
let top_screen = TopScreen3D::from(&gfx.top_screen);
let (mut left, mut right) = top_screen.split_mut();
let mut current_side = Side::Left;
// Main loop
while apt.main_loop() {
//Scan all the inputs. This should be done once for each frame
hid.scan_input();
if hid.keys_down().contains(KeyPad::KEY_START) {
break;
}
let left_buf = left.get_raw_framebuffer();
let right_buf = right.get_raw_framebuffer();
// Clear both buffers every time, in case the user switches sides this loop
unsafe {
left_buf.ptr.copy_from(ZERO.as_ptr(), ZERO.len());
right_buf.ptr.copy_from(ZERO.as_ptr(), ZERO.len());
}
if hid.keys_down().contains(KeyPad::KEY_A) {
// flip which buffer we're writing to
current_side = match current_side {
Side::Left => Side::Right,
Side::Right => Side::Left,
};
}
let buf = match current_side {
Side::Left => left_buf.ptr,
Side::Right => right_buf.ptr,
};
unsafe {
buf.copy_from(IMAGE.as_ptr(), IMAGE.len());
}
// Flush and swap framebuffers
gfx.flush_buffers();
gfx.swap_buffers();
//Wait for VBlank
gfx.wait_for_vblank();
}
}

6
ctru-rs/src/error.rs

@ -124,11 +124,7 @@ impl fmt::Display for Error { @@ -124,11 +124,7 @@ impl fmt::Display for Error {
}
}
impl error::Error for Error {
fn description(&self) -> &str {
"error originating from a libctru function"
}
}
impl error::Error for Error {}
fn result_code_level_str(result: ctru_sys::Result) -> Cow<'static, str> {
use ctru_sys::{

183
ctru-rs/src/gfx.rs

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
//! LCD screens manipulation helper
use once_cell::sync::Lazy;
use std::cell::RefCell;
use std::cell::{Ref, RefCell, RefMut};
use std::marker::PhantomData;
use std::sync::Mutex;
@ -9,11 +8,45 @@ use crate::error::Result; @@ -9,11 +8,45 @@ use crate::error::Result;
use crate::services::gspgpu::{self, FramebufferFormat};
use crate::services::ServiceReference;
/// Trait implemented by TopScreen and BottomScreen for common methods
pub trait Screen {
/// Returns the libctru value for the Screen kind
mod private {
use super::{BottomScreen, TopScreen, TopScreenLeft, TopScreenRight};
pub trait Sealed {}
impl Sealed for TopScreen {}
impl Sealed for TopScreenLeft {}
impl Sealed for TopScreenRight {}
impl Sealed for BottomScreen {}
}
/// This trait is implemented by the screen structs for working with frame buffers and
/// drawing to the screens. Graphics-related code can be made generic over this
/// trait to work with any of the given screens.
pub trait Screen: private::Sealed {
/// Returns the `libctru` value for the Screen kind.
fn as_raw(&self) -> ctru_sys::gfxScreen_t;
/// Returns the Screen side (left or right).
fn side(&self) -> Side;
/// Returns a [`RawFrameBuffer`] for the screen.
///
/// Note that the pointer of the framebuffer returned by this function can
/// change after each call to this function if double buffering is enabled.
fn get_raw_framebuffer(&mut self) -> RawFrameBuffer {
let mut width = 0;
let mut height = 0;
let ptr = unsafe {
ctru_sys::gfxGetFramebuffer(self.as_raw(), self.side().into(), &mut width, &mut height)
};
RawFrameBuffer {
ptr,
width,
height,
screen: PhantomData,
}
}
/// Sets whether to use double buffering. Enabled by default.
///
/// Note that even when double buffering is disabled, one should still use the `swap_buffers`
@ -33,10 +66,26 @@ pub trait Screen { @@ -33,10 +66,26 @@ pub trait Screen {
}
}
#[non_exhaustive]
pub struct TopScreen;
/// The top screen. Mutable access to this struct is required to write to the top
/// screen's frame buffer. To enable 3D mode, it can be converted into a [`TopScreen3D`].
pub struct TopScreen {
left: TopScreenLeft,
right: TopScreenRight,
}
/// A helper container for both sides of the top screen. Once the [`TopScreen`] is
/// converted into this, 3D mode will be enabled until this struct is dropped.
pub struct TopScreen3D<'top_screen> {
screen: &'top_screen RefCell<TopScreen>,
}
struct TopScreenLeft;
struct TopScreenRight;
#[non_exhaustive]
/// The bottom screen. Mutable access to this struct is required to write to the
/// bottom screen's frame buffer.
pub struct BottomScreen;
/// Representation of a framebuffer for one [`Side`] of the top screen, or the
@ -69,14 +118,13 @@ pub enum Side { @@ -69,14 +118,13 @@ pub enum Side {
/// provides helper functions and utilities for software rendering.
///
/// The service exits when this struct is dropped.
#[non_exhaustive]
pub struct Gfx {
pub top_screen: RefCell<TopScreen>,
pub bottom_screen: RefCell<BottomScreen>,
_service_handler: ServiceReference,
}
static GFX_ACTIVE: Lazy<Mutex<usize>> = Lazy::new(|| Mutex::new(0));
static GFX_ACTIVE: Mutex<usize> = Mutex::new(0);
impl Gfx {
/// Initialize the Gfx module with the chosen framebuffer formats for the top and bottom
@ -88,7 +136,7 @@ impl Gfx { @@ -88,7 +136,7 @@ impl Gfx {
bottom_fb_fmt: FramebufferFormat,
use_vram_buffers: bool,
) -> Result<Self> {
let _service_handler = ServiceReference::new(
let handler = ServiceReference::new(
&GFX_ACTIVE,
false,
|| unsafe {
@ -100,14 +148,15 @@ impl Gfx { @@ -100,14 +148,15 @@ impl Gfx {
)?;
Ok(Self {
top_screen: RefCell::new(TopScreen),
top_screen: RefCell::new(TopScreen::new()),
bottom_screen: RefCell::new(BottomScreen),
_service_handler,
_service_handler: handler,
})
}
/// Creates a new Gfx instance with default init values
/// It's the same as calling: `Gfx::with_formats(FramebufferFormat::Bgr8, FramebufferFormat::Bgr8, false)
/// Creates a new [Gfx] instance with default init values
/// It's the same as calling:
/// `Gfx::with_formats(FramebufferFormat::Bgr8, FramebufferFormat::Bgr8, false)`
pub fn init() -> Result<Self> {
Gfx::with_formats(FramebufferFormat::Bgr8, FramebufferFormat::Bgr8, false)
}
@ -139,80 +188,106 @@ impl Gfx { @@ -139,80 +188,106 @@ impl Gfx {
}
}
impl TopScreen {
/// Enable or disable the 3D stereoscopic effect
pub fn set_3d_enabled(&mut self, enabled: bool) {
impl TopScreen3D<'_> {
/// Immutably borrow the two sides of the screen as `(left, right)`.
pub fn split(&self) -> (Ref<dyn Screen>, Ref<dyn Screen>) {
Ref::map_split(self.screen.borrow(), |screen| {
(&screen.left as _, &screen.right as _)
})
}
/// Mutably borrow the two sides of the screen as `(left, right)`.
pub fn split_mut(&self) -> (RefMut<dyn Screen>, RefMut<dyn Screen>) {
RefMut::map_split(self.screen.borrow_mut(), |screen| {
(&mut screen.left as _, &mut screen.right as _)
})
}
}
impl<'top_screen> From<&'top_screen RefCell<TopScreen>> for TopScreen3D<'top_screen> {
fn from(top_screen: &'top_screen RefCell<TopScreen>) -> Self {
unsafe {
ctru_sys::gfxSet3D(enabled);
ctru_sys::gfxSet3D(true);
}
TopScreen3D { screen: top_screen }
}
}
/// Enable or disable the wide screen mode (top screen).
/// This only works when 3D is disabled.
pub fn set_wide_mode(&mut self, enabled: bool) {
impl Drop for TopScreen3D<'_> {
fn drop(&mut self) {
unsafe {
ctru_sys::gfxSetWide(enabled);
ctru_sys::gfxSet3D(false);
}
}
}
/// Get the status of wide screen mode.
impl TopScreen {
fn new() -> Self {
Self {
left: TopScreenLeft,
right: TopScreenRight,
}
}
/// Enable or disable wide mode on the top screen.
pub fn set_wide_mode(&mut self, enable: bool) {
unsafe {
ctru_sys::gfxSetWide(enable);
}
}
/// Returns whether or not wide mode is enabled on the top screen.
pub fn get_wide_mode(&self) -> bool {
unsafe { ctru_sys::gfxIsWide() }
}
/// Returns a [`RawFrameBuffer`] for the given [`Side`] of the top screen.
///
/// Note that the pointer of the framebuffer returned by this function can
/// change after each call to this function if double buffering is enabled.
pub fn get_raw_framebuffer(&mut self, side: Side) -> RawFrameBuffer {
RawFrameBuffer::for_screen_side(self, side)
}
impl Screen for TopScreen {
fn as_raw(&self) -> ctru_sys::gfxScreen_t {
self.left.as_raw()
}
impl BottomScreen {
/// Returns a [`RawFrameBuffer`] for the bottom screen.
///
/// Note that the pointer of the framebuffer returned by this function can
/// change after each call to this function if double buffering is enabled.
pub fn get_raw_framebuffer(&mut self) -> RawFrameBuffer {
RawFrameBuffer::for_screen_side(self, Side::Left)
fn side(&self) -> Side {
self.left.side()
}
}
impl<'screen> RawFrameBuffer<'screen> {
fn for_screen_side(screen: &'screen mut dyn Screen, side: Side) -> Self {
let mut width = 0;
let mut height = 0;
let ptr = unsafe {
ctru_sys::gfxGetFramebuffer(screen.as_raw(), side.into(), &mut width, &mut height)
};
Self {
ptr,
width,
height,
screen: PhantomData,
impl Screen for TopScreenLeft {
fn as_raw(&self) -> ctru_sys::gfxScreen_t {
ctru_sys::GFX_TOP
}
fn side(&self) -> Side {
Side::Left
}
}
impl Screen for TopScreen {
impl Screen for TopScreenRight {
fn as_raw(&self) -> ctru_sys::gfxScreen_t {
ctru_sys::GFX_TOP
}
fn side(&self) -> Side {
Side::Right
}
}
impl Screen for BottomScreen {
fn as_raw(&self) -> ctru_sys::gfxScreen_t {
ctru_sys::GFX_BOTTOM
}
fn side(&self) -> Side {
Side::Left
}
}
impl From<Side> for ctru_sys::gfx3dSide_t {
fn from(s: Side) -> ctru_sys::gfx3dSide_t {
use self::Side::*;
match s {
Left => ctru_sys::GFX_LEFT,
Right => ctru_sys::GFX_RIGHT,
Side::Left => ctru_sys::GFX_LEFT,
Side::Right => ctru_sys::GFX_RIGHT,
}
}
}
@ -225,6 +300,6 @@ mod tests { @@ -225,6 +300,6 @@ mod tests {
#[test]
fn gfx_duplicate() {
// We don't need to build a `Gfx` because the test runner has one already
assert!(matches!(Gfx::init(), Err(Error::ServiceAlreadyActive)))
assert!(matches!(Gfx::init(), Err(Error::ServiceAlreadyActive)));
}
}

1
ctru-rs/src/linear.rs

@ -17,6 +17,7 @@ use std::ptr::NonNull; @@ -17,6 +17,7 @@ use std::ptr::NonNull;
/// [`std::alloc::Allocator`] struct for LINEAR memory
/// To use this struct the main crate must activate the `allocator_api` unstable feature.
#[derive(Copy, Clone, Default, Debug)]
pub struct LinearAllocator;
impl LinearAllocator {

4
ctru-rs/src/romfs.rs

@ -11,18 +11,16 @@ @@ -11,18 +11,16 @@
//! ```
use crate::error::ResultCode;
use once_cell::sync::Lazy;
use std::ffi::CStr;
use std::sync::Mutex;
use crate::services::ServiceReference;
#[non_exhaustive]
pub struct RomFS {
_service_handler: ServiceReference,
}
static ROMFS_ACTIVE: Lazy<Mutex<usize>> = Lazy::new(|| Mutex::new(0));
static ROMFS_ACTIVE: Mutex<usize> = Mutex::new(0);
impl RomFS {
pub fn init() -> crate::Result<Self> {

9
ctru-rs/src/services/mod.rs

@ -1,3 +1,10 @@ @@ -1,3 +1,10 @@
//! System services used to handle system-specific functionalities.
//!
//! Most of the 3DS console's functionalities (when writing homebrew) are locked behind services,
//! which need to be initialized before accessing any particular feature.
//!
//! Some include: button input, audio playback, graphics rendering, built-in cameras, etc.
pub mod apt;
pub mod cam;
pub mod cfgu;
@ -5,6 +12,7 @@ pub mod fs; @@ -5,6 +12,7 @@ pub mod fs;
pub mod gspgpu;
pub mod hid;
pub mod ir_user;
pub mod ndsp;
pub mod ps;
mod reference;
pub mod soc;
@ -13,6 +21,5 @@ pub mod sslc; @@ -13,6 +21,5 @@ pub mod sslc;
pub use self::apt::Apt;
pub use self::hid::Hid;
pub use self::sslc::SslC;
pub(crate) use self::reference::ServiceReference;

342
ctru-rs/src/services/ndsp/mod.rs

@ -0,0 +1,342 @@ @@ -0,0 +1,342 @@
//! NDSP (Audio) service
pub mod wave;
use wave::{WaveInfo, WaveStatus};
use crate::error::ResultCode;
use crate::services::ServiceReference;
use std::cell::{RefCell, RefMut};
use std::error;
use std::fmt;
use std::sync::Mutex;
const NUMBER_OF_CHANNELS: u8 = 24;
#[derive(Copy, Clone, Debug)]
#[repr(u32)]
pub enum OutputMode {
Mono = ctru_sys::NDSP_OUTPUT_MONO,
Stereo = ctru_sys::NDSP_OUTPUT_STEREO,
Surround = ctru_sys::NDSP_OUTPUT_SURROUND,
}
#[derive(Copy, Clone, Debug)]
#[repr(u32)]
pub enum AudioFormat {
PCM8Mono = ctru_sys::NDSP_FORMAT_MONO_PCM8,
PCM16Mono = ctru_sys::NDSP_FORMAT_MONO_PCM16,
PCM8Stereo = ctru_sys::NDSP_FORMAT_STEREO_PCM8,
PCM16Stereo = ctru_sys::NDSP_FORMAT_STEREO_PCM16,
}
#[derive(Copy, Clone, Debug)]
#[repr(u32)]
pub enum InterpolationType {
Polyphase = ctru_sys::NDSP_INTERP_POLYPHASE,
Linear = ctru_sys::NDSP_INTERP_LINEAR,
None = ctru_sys::NDSP_INTERP_NONE,
}
#[derive(Copy, Clone, Debug)]
pub enum NdspError {
/// Channel ID
InvalidChannel(u8),
/// Channel ID
ChannelAlreadyInUse(u8),
/// Channel ID
WaveBusy(u8),
/// Sample amount requested, Max sample amount
SampleCountOutOfBounds(u32, u32),
}
pub struct Channel<'ndsp> {
id: u8,
_rf: RefMut<'ndsp, ()>, // we don't need to hold any data
}
static NDSP_ACTIVE: Mutex<usize> = Mutex::new(0);
/// Handler of the DSP service and DSP processor.
///
/// This is the main struct to handle audio playback using the 3DS' speakers and headphone jack.
/// Only one "instance" of this struct can exist at a time.
pub struct Ndsp {
_service_handler: ServiceReference,
channel_flags: [RefCell<()>; NUMBER_OF_CHANNELS as usize],
}
impl Ndsp {
/// Initialize the DSP service and audio units.
///
/// # Errors
///
/// This function will return an error if an instance of the `Ndsp` struct already exists
/// or if there are any issues during initialization.
pub fn init() -> crate::Result<Self> {
let _service_handler = ServiceReference::new(
&NDSP_ACTIVE,
false,
|| {
ResultCode(unsafe { ctru_sys::ndspInit() })?;
Ok(())
},
|| unsafe {
ctru_sys::ndspExit();
},
)?;
Ok(Self {
_service_handler,
channel_flags: Default::default(),
})
}
/// Return a representation of the specified channel.
///
/// # Errors
///
/// An error will be returned if the channel ID is not between 0 and 23 or if the specified channel is already being used.
pub fn channel(&self, id: u8) -> std::result::Result<Channel, NdspError> {
let in_bounds = self.channel_flags.get(id as usize);
match in_bounds {
Some(ref_cell) => {
let flag = ref_cell.try_borrow_mut();
match flag {
Ok(_rf) => Ok(Channel { id, _rf }),
Err(_) => Err(NdspError::ChannelAlreadyInUse(id)),
}
}
None => Err(NdspError::InvalidChannel(id)),
}
}
/// Set the audio output mode. Defaults to `OutputMode::Stereo`.
pub fn set_output_mode(&mut self, mode: OutputMode) {
unsafe { ctru_sys::ndspSetOutputMode(mode as u32) };
}
}
impl Channel<'_> {
/// Reset the channel
pub fn reset(&self) {
unsafe { ctru_sys::ndspChnReset(self.id.into()) };
}
/// Initialize the channel's parameters
pub fn init_parameters(&self) {
unsafe { ctru_sys::ndspChnInitParams(self.id.into()) };
}
/// Returns whether the channel is playing any audio.
pub fn is_playing(&self) -> bool {
unsafe { ctru_sys::ndspChnIsPlaying(self.id.into()) }
}
/// Returns whether the channel's playback is currently paused.
pub fn is_paused(&self) -> bool {
unsafe { ctru_sys::ndspChnIsPaused(self.id.into()) }
}
// Returns the channel's id
pub fn get_id(&self) -> u8 {
self.id
}
/// Returns the channel's current sample's position.
pub fn get_sample_position(&self) -> u32 {
unsafe { ctru_sys::ndspChnGetSamplePos(self.id.into()) }
}
/// Returns the channel's current wave sequence's id.
pub fn get_wave_sequence_id(&self) -> u16 {
unsafe { ctru_sys::ndspChnGetWaveBufSeq(self.id.into()) }
}
/// Pause or un-pause the channel's playback.
pub fn set_paused(&self, state: bool) {
unsafe { ctru_sys::ndspChnSetPaused(self.id.into(), state) };
}
/// Set the channel's output format.
/// Change this setting based on the used sample's format.
pub fn set_format(&self, format: AudioFormat) {
unsafe { ctru_sys::ndspChnSetFormat(self.id.into(), format as u16) };
}
/// Set the channel's interpolation mode.
pub fn set_interpolation(&self, interp_type: InterpolationType) {
unsafe { ctru_sys::ndspChnSetInterp(self.id.into(), interp_type as u32) };
}
/// Set the channel's volume mix.
///
/// # Notes
///
/// The buffer's format is read as:
///
/// Index 0: Front left volume <br>
/// Index 1: Front right volume <br>
/// Index 2: Back left volume <br>
/// Index 3: Back right volume <br>
/// Index 4..7: Same as 0..3 but for auxiliary output 0 <br>
/// Index 8..11: Same as 0..3 but for auxiliary output 1 <br>
pub fn set_mix(&self, mix: &[f32; 12]) {
unsafe { ctru_sys::ndspChnSetMix(self.id.into(), mix.as_ptr().cast_mut()) }
}
/// Set the channel's rate of sampling.
pub fn set_sample_rate(&self, rate: f32) {
unsafe { ctru_sys::ndspChnSetRate(self.id.into(), rate) };
}
// `ndspChnSetAdpcmCoefs` isn't wrapped on purpose.
// DSPADPCM is a proprietary format used by Nintendo, unavailable by "normal" means.
// We suggest using other wave formats when developing homebrew applications.
/// Clear the wave buffer queue and stop playback.
pub fn clear_queue(&self) {
unsafe { ctru_sys::ndspChnWaveBufClear(self.id.into()) };
}
/// Add a wave buffer to the channel's queue.
/// If there are no other buffers in queue, playback for this buffer will start.
///
/// # Warning
///
/// `libctru` expects the user to manually keep the info data (in this case [WaveInfo]) alive during playback.
/// To ensure safety, checks within [WaveInfo] will clear the whole channel queue if any queued [WaveInfo] is dropped prematurely.
pub fn queue_wave(&self, wave: &mut WaveInfo) -> std::result::Result<(), NdspError> {
match wave.get_status() {
WaveStatus::Playing | WaveStatus::Queued => return Err(NdspError::WaveBusy(self.id)),
_ => (),
}
wave.set_channel(self.id);
unsafe { ctru_sys::ndspChnWaveBufAdd(self.id.into(), &mut wave.raw_data) };
Ok(())
}
}
/// Functions to handle audio filtering.
///
/// Refer to [libctru](https://libctru.devkitpro.org/channel_8h.html#a1da3b363c2edfd318c92276b527daae6) for more info.
impl Channel<'_> {
/// Enables/disables monopole filters.
pub fn iir_mono_set_enabled(&self, enable: bool) {
unsafe { ctru_sys::ndspChnIirMonoSetEnable(self.id.into(), enable) };
}
/// Sets the monopole to be a high pass filter.
///
/// # Notes
///
/// This is a lower quality filter than the Biquad alternative.
pub fn iir_mono_set_params_high_pass_filter(&self, cut_off_freq: f32) {
unsafe { ctru_sys::ndspChnIirMonoSetParamsHighPassFilter(self.id.into(), cut_off_freq) };
}
/// Sets the monopole to be a low pass filter.
///
/// # Notes
///
/// This is a lower quality filter than the Biquad alternative.
pub fn iir_mono_set_params_low_pass_filter(&self, cut_off_freq: f32) {
unsafe { ctru_sys::ndspChnIirMonoSetParamsLowPassFilter(self.id.into(), cut_off_freq) };
}
/// Enables/disables biquad filters.
pub fn iir_biquad_set_enabled(&self, enable: bool) {
unsafe { ctru_sys::ndspChnIirBiquadSetEnable(self.id.into(), enable) };
}
/// Sets the biquad to be a high pass filter.
pub fn iir_biquad_set_params_high_pass_filter(&self, cut_off_freq: f32, quality: f32) {
unsafe {
ctru_sys::ndspChnIirBiquadSetParamsHighPassFilter(self.id.into(), cut_off_freq, quality)
};
}
/// Sets the biquad to be a low pass filter.
pub fn iir_biquad_set_params_low_pass_filter(&self, cut_off_freq: f32, quality: f32) {
unsafe {
ctru_sys::ndspChnIirBiquadSetParamsLowPassFilter(self.id.into(), cut_off_freq, quality)
};
}
/// Sets the biquad to be a notch filter.
pub fn iir_biquad_set_params_notch_filter(&self, notch_freq: f32, quality: f32) {
unsafe {
ctru_sys::ndspChnIirBiquadSetParamsNotchFilter(self.id.into(), notch_freq, quality)
};
}
/// Sets the biquad to be a band pass filter.
pub fn iir_biquad_set_params_band_pass_filter(&self, mid_freq: f32, quality: f32) {
unsafe {
ctru_sys::ndspChnIirBiquadSetParamsBandPassFilter(self.id.into(), mid_freq, quality)
};
}
/// Sets the biquad to be a peaking equalizer.
pub fn iir_biquad_set_params_peaking_equalizer(
&self,
central_freq: f32,
quality: f32,
gain: f32,
) {
unsafe {
ctru_sys::ndspChnIirBiquadSetParamsPeakingEqualizer(
self.id.into(),
central_freq,
quality,
gain,
)
};
}
}
impl AudioFormat {
/// Returns the amount of bytes needed to store one sample
/// Eg.
/// 8 bit formats return 1 (byte)
/// 16 bit formats return 2 (bytes)
pub fn sample_size(self) -> u8 {
match self {
AudioFormat::PCM8Mono => 1,
AudioFormat::PCM16Mono | AudioFormat::PCM8Stereo => 2,
AudioFormat::PCM16Stereo => 4,
}
}
}
impl fmt::Display for NdspError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::InvalidChannel(id) => write!(f, "Audio Channel with ID {id} doesn't exist. Valid channels have an ID between 0 and 23."),
Self::ChannelAlreadyInUse(id) => write!(f, "Audio Channel with ID {id} is already being used. Drop the other instance if you want to use it here."),
Self::WaveBusy(id) => write!(f, "The selected WaveInfo is busy playing on channel {id}."),
Self::SampleCountOutOfBounds(samples_requested, max_samples) => write!(f, "The sample count requested is too big. Requested amount was {samples_requested} while the maximum sample count is {max_samples}."),
}
}
}
impl error::Error for NdspError {}
impl<'ndsp> Drop for Channel<'ndsp> {
fn drop(&mut self) {
self.reset();
}
}
impl Drop for Ndsp {
fn drop(&mut self) {
for i in 0..NUMBER_OF_CHANNELS {
self.channel(i).unwrap().clear_queue();
}
}
}

180
ctru-rs/src/services/ndsp/wave.rs

@ -0,0 +1,180 @@ @@ -0,0 +1,180 @@
use super::{AudioFormat, NdspError};
use crate::linear::LinearAllocator;
/// Informational struct holding the raw audio data and playback info. This corresponds to [ctru_sys::ndspWaveBuf]
pub struct WaveInfo {
/// Data block of the audio wave (and its format information).
buffer: Box<[u8], LinearAllocator>,
audio_format: AudioFormat,
// Holding the data with the raw format is necessary since `libctru` will access it.
pub(crate) raw_data: ctru_sys::ndspWaveBuf,
played_on_channel: Option<u8>,
}
#[derive(Copy, Clone, Debug)]
#[repr(u8)]
/// Enum representing the playback status of a [WaveInfo].
pub enum WaveStatus {
Free = ctru_sys::NDSP_WBUF_FREE as u8,
Queued = ctru_sys::NDSP_WBUF_QUEUED as u8,
Playing = ctru_sys::NDSP_WBUF_PLAYING as u8,
Done = ctru_sys::NDSP_WBUF_DONE as u8,
}
impl WaveInfo {
/// Build a new playable wave object from a raw buffer on LINEAR memory and a some info.
pub fn new(
buffer: Box<[u8], LinearAllocator>,
audio_format: AudioFormat,
looping: bool,
) -> Self {
let sample_count: usize = buffer.len() / (audio_format.sample_size() as usize);
// Signal to the DSP processor the buffer's RAM sector.
// This step may seem delicate, but testing reports failure most of the time, while still having no repercussions on the resulting audio.
unsafe {
let _r = ctru_sys::DSP_FlushDataCache(buffer.as_ptr().cast(), buffer.len() as u32);
}
let address = ctru_sys::tag_ndspWaveBuf__bindgen_ty_1 {
data_vaddr: buffer.as_ptr().cast(),
};
let raw_data = ctru_sys::ndspWaveBuf {
__bindgen_anon_1: address, // Buffer data virtual address
nsamples: sample_count as u32,
adpcm_data: std::ptr::null_mut(),
offset: 0,
looping,
// The ones after this point aren't supposed to be setup by the user
status: 0,
sequence_id: 0,
next: std::ptr::null_mut(),
};
Self {
buffer,
audio_format,
raw_data,
played_on_channel: None,
}
}
/// Return a slice to the audio data (on the LINEAR memory).
pub fn get_buffer(&self) -> &[u8] {
&self.buffer
}
/// Return a mutable slice to the audio data (on the LINEAR memory).
///
/// # Errors
///
/// This function will return an error if the [WaveInfo] is currently busy,
/// with the id to the channel in which it's queued.
pub fn get_buffer_mut(&mut self) -> Result<&mut [u8], NdspError> {
match self.get_status() {
WaveStatus::Playing | WaveStatus::Queued => {
Err(NdspError::WaveBusy(self.played_on_channel.unwrap()))
}
_ => Ok(&mut self.buffer),
}
}
/// Return this wave's playback status.
pub fn get_status(&self) -> WaveStatus {
self.raw_data.status.try_into().unwrap()
}
/// Get the amounts of samples *read* by the NDSP process.
///
/// # Notes
///
/// This value varies depending on [Self::set_sample_count].
pub fn get_sample_count(&self) -> u32 {
self.raw_data.nsamples
}
/// Get the format of the audio data.
pub fn get_format(&self) -> AudioFormat {
self.audio_format
}
// Set the internal flag for the id of the channel playing this wave.
//
// Internal Use Only.
pub(crate) fn set_channel(&mut self, id: u8) {
self.played_on_channel = Some(id)
}
/// Set the amount of samples to be read.
/// This function doesn't resize the internal buffer.
///
/// # Note
///
/// Operations of this kind are particularly useful to allocate memory pools
/// for VBR (Variable BitRate) Formats, like OGG Vorbis.
///
/// # Errors
///
/// This function will return an error if the sample size exceeds the buffer's capacity
/// or if the WaveInfo is currently queued.
pub fn set_sample_count(&mut self, sample_count: u32) -> Result<(), NdspError> {
match self.get_status() {
WaveStatus::Playing | WaveStatus::Queued => {
return Err(NdspError::WaveBusy(self.played_on_channel.unwrap()));
}
_ => (),
}
let max_count: usize = self.buffer.len() / (self.audio_format.sample_size() as usize);
if sample_count > max_count as u32 {
return Err(NdspError::SampleCountOutOfBounds(
sample_count,
max_count as u32,
));
}
self.raw_data.nsamples = sample_count;
Ok(())
}
}
impl TryFrom<u8> for WaveStatus {
type Error = &'static str;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Free),
1 => Ok(Self::Queued),
2 => Ok(Self::Playing),
3 => Ok(Self::Done),
_ => Err("Invalid WaveInfo Status code"),
}
}
}
impl Drop for WaveInfo {
fn drop(&mut self) {
// This was the only way I found I could check for improper drops of `WaveInfos`.
// A panic was considered, but it would cause issues with drop order against `Ndsp`.
match self.get_status() {
WaveStatus::Free | WaveStatus::Done => (),
// If the status flag is "unfinished"
_ => {
// The unwrap is safe, since it must have a value in the case the status is "unfinished".
unsafe { ctru_sys::ndspChnWaveBufClear(self.played_on_channel.unwrap().into()) };
}
}
unsafe {
// Flag the buffer's RAM sector as unused
// This step has no real effect in normal applications and is skipped even by devkitPRO's own examples.
let _r = ctru_sys::DSP_InvalidateDataCache(
self.buffer.as_ptr().cast(),
self.buffer.len().try_into().unwrap(),
);
}
}
}

4
ctru-rs/src/services/soc.rs

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
use libc::memalign;
use once_cell::sync::Lazy;
use std::net::Ipv4Addr;
use std::sync::Mutex;
@ -9,13 +8,12 @@ use crate::Error; @@ -9,13 +8,12 @@ use crate::Error;
/// Soc service. Initializing this service will enable the use of network sockets and utilities
/// such as those found in `std::net`. The service will be closed when this struct is is dropped.
#[non_exhaustive]
pub struct Soc {
_service_handler: ServiceReference,
sock_3dslink: libc::c_int,
}
static SOC_ACTIVE: Lazy<Mutex<usize>> = Lazy::new(|| Mutex::new(0));
static SOC_ACTIVE: Mutex<usize> = Mutex::new(0);
impl Soc {
/// Initialize the Soc service with a default buffer size of 0x100000 bytes

Loading…
Cancel
Save