Skip to content

Commit 759deb8

Browse files
author
A
committed
Added audio playback but it is very slow as it is in the same thread as the main logic
1 parent 5915028 commit 759deb8

File tree

4 files changed

+182
-17
lines changed

4 files changed

+182
-17
lines changed

Diff for: .gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,8 @@ Cargo.lock
1010
**/*.rs.bk
1111
rustroarch.cfg
1212
.DS_Store
13+
*.dylib
14+
*.zip
15+
*.gb
16+
states/
17+
*.state

Diff for: Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ libloading = "0.7.0"
1313
libc = "0.2"
1414
libretro-sys = "0.1.1"
1515
clap = "2.33.3"
16-
rand = "0.8.4"
16+
rand = "0.8.4"
17+
rodio = "0.17.1"

Diff for: README.md

+130-10
Original file line numberDiff line numberDiff line change
@@ -1463,7 +1463,6 @@ println!("Key for Player 1 A button: {}", config["input_player1_a"]);
14631463

14641464
This is great but the problem with this is the result is a string of the keyboard letter pressed and we need to map it to the correct `minifb` Key enum type in order to use it.
14651465

1466-
14671466
```rust
14681467
let config = setup_config().unwrap();
14691468

@@ -1483,7 +1482,6 @@ let key_device_map = HashMap::from([
14831482
]);
14841483
```
14851484

1486-
14871485
Now we can rewrite our input handling logic to look like this:
14881486

14891487
```rust
@@ -1503,7 +1501,6 @@ Now we can rewrite our input handling logic to look like this:
15031501

15041502
```
15051503

1506-
15071504
This is great but we shouldn't expect the user to have RetroArch installed and have a valid config, and we need to support the case where they might want different settings for out frontend compared to their RetroArch, so lets set up some default values and allow users to override them if they have a file called `rustroarch.cfg`.
15081505

15091506
To do this we can refactor the `setup_config` function like so:
@@ -1551,7 +1548,7 @@ In this code first we setup some default config values, mainly for input handlin
15511548

15521549
Lets start keeping track of the size of the executable, I should have done this from the start but here we go:
15531550

1554-
> Size of executable so far: 1.1MB
1551+
> Size of executable so far: 1.1MB
15551552
15561553
# Step 20 - Saving and Loading state
15571554

@@ -1599,7 +1596,6 @@ But how do we know how large the buffer should be, well libRetro also has us cov
15991596
pub retro_serialize_size: unsafe extern "C" fn() -> libc::size_t,
16001597
```
16011598

1602-
16031599
We can put all this logic in its own function and use the builtin rust library to write it to a file like so:
16041600

16051601
```rust
@@ -1614,10 +1610,8 @@ unsafe fn save_state(core_api: &CoreAPI) {
16141610
}
16151611
```
16161612

1617-
16181613
This will save the state into the current directory with the hardcoded name `save_state.state` but the problem is this same file will be overriden no matter what ROM you load, ideally it would be good to save a different file based on the game you are playing.
16191614

1620-
16211615
After saving a file using RetroArch itself it seems to save with both the ROM name and also a save state index (which can be incremented/decremented by the user) and it also replaces any invalid characters (such as spaces) with the `_` character, this is quite a bit of logic in itself and we will need this logic in both saving and loading of state so lets create a new function for this purpose:
16221616

16231617
```rust
@@ -1688,7 +1682,6 @@ unsafe fn load_state(core_api: &CoreAPI, save_directory: &String) {
16881682
}
16891683
```
16901684

1691-
16921685
And we can call it similar to how we call `save_state`:
16931686

16941687
```rust
@@ -1702,7 +1695,6 @@ And we can call it similar to how we call `save_state`:
17021695
}
17031696
```
17041697

1705-
17061698
# Step 21 - Supporting save slots
17071699

17081700
You will notice that we hard coded the `save_slot_index` to 0, we can now store the current save slot index in our global variable and then allow the user to increment and decrement the current save slot, allowing them to have different save states for the same game.
@@ -1768,5 +1760,133 @@ Now if you run the program you can incfrease and decrease the save slots and it
17681760

17691761
# Step 22 - Audio Support
17701762

1763+
So far the game is playable but rather... quiet, lets change that by adding audio support!
1764+
1765+
We already implemented the two audio callbacks as dummy functions before to prevent the core from causing a segmentation fault but they don't do anything yet:
1766+
1767+
```rust
1768+
unsafe extern "C" fn libretro_set_audio_sample_callback(left: i16, right: i16) {
1769+
println!("libretro_set_audio_sample_callback left channel: {} right: {}", left, right);
1770+
1771+
1772+
unsafe extern "C" fn libretro_set_audio_sample_batch_callback(
1773+
data: *const i16,
1774+
frames: libc::size_t,
1775+
) -> libc::size_t {
1776+
// println!("libretro_set_audio_sample_batch_callback");
1777+
return 1;
1778+
}
1779+
```
1780+
1781+
The first function `libretro_set_audio_sample_callback` doesn't seem to be used by the Gambate core that I am using for testing so we will need to come back to this when we find a core that requires it.
1782+
1783+
So `libretro_set_audio_sample_batch_callback` seems to have two paramerters, one is a data buffer that contains both the left and right audio channel dataper frame and the other is the number of frames that are in the buffer.
1784+
1785+
Before we can use this data we first need to figure out how we can play audio in rust across all the major Operating Systems. So after a quick google search the first resut was the `rodio` crate, so lets add it to our Cargo project:
1786+
1787+
```rust
1788+
cargo add rodio
1789+
```
1790+
1791+
Now lets try to get the main example from the Rodio documentation to work:
1792+
1793+
```rust
1794+
use std::fs::File;
1795+
use std::io::BufReader;
1796+
use std::time::Duration;
1797+
use rodio::{Decoder, OutputStream, Sink};
1798+
use rodio::source::{SineWave, Source};
1799+
1800+
fn play_audio() {
1801+
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
1802+
let sink = Sink::try_new(&stream_handle).unwrap();
1803+
1804+
// Add a dummy source of the sake of the example.
1805+
let source = SineWave::new(440.0).take_duration(Duration::from_secs_f32(0.25)).amplify(0.20);
1806+
sink.append(source);
1807+
1808+
// The sound plays in a separate thread. This call will block the current thread until the sink
1809+
// has finished playing all its queued sounds.
1810+
sink.sleep_until_end();
1811+
}
1812+
```
1813+
1814+
Now call `play_audio` somewhere in the main game loop for example:
1815+
1816+
```rust
1817+
unsafe {
1818+
(core_api.retro_run)();
1819+
}
1820+
play_audio();
1821+
```
1822+
1823+
If all went well you will get an annoying sound while the game is playing, but how do we convert the buffer that the callback gives us into something that rodio can play?
1824+
1825+
First lets add an audio_data buffer to our global variable so that we can pass it between the callback and the play_audio function:
1826+
1827+
```rust
1828+
struct EmulatorState {
1829+
rom_name: String,
1830+
core_name: String,
1831+
frame_buffer: Option<Vec<u32>>,
1832+
audio_data: Option<Vec<i16>>,
1833+
```
1834+
1835+
Now lets update the callback so that it sets the global variables value every time its called:
1836+
1837+
```rust
1838+
const AUDIO_CHANNELS: usize = 2; // left and right
1839+
unsafe extern "C" fn libretro_set_audio_sample_batch_callback(
1840+
audio_data: *const i16,
1841+
frames: libc::size_t,
1842+
) -> libc::size_t {
1843+
let audio_slice = std::slice::from_raw_parts(audio_data, frames * AUDIO_CHANNELS);
1844+
CURRENT_EMULATOR_STATE.audio_data = Some(audio_slice.to_vec());
1845+
return frames;
1846+
}
1847+
```
1848+
1849+
We need to be able to take that audio_data and play it back inside Rodeo, for this Rodeo provides a SamplesBuffer that we can use as a source:
1850+
1851+
```rust
1852+
match &CURRENT_EMULATOR_STATE.audio_data {
1853+
Some(data) => {
1854+
if sink.empty() {
1855+
let audio_slice = std::slice::from_raw_parts(data.as_ptr() as *const i16, data.len());
1856+
let source = SamplesBuffer::new(2, 32768*2, audio_slice);
1857+
sink.append(source);
1858+
sink.play();
1859+
sink.sleep_until_end();
1860+
}
1861+
},
1862+
None => {},
1863+
};
1864+
```
1865+
1866+
If you run the program now you will notice that it starts to play audio, but in a very slow manner, turns out audio processing is very cpu time consuming.
1867+
1868+
You will also notice a massive dip in the frame rate, this is because we are setting up a brand new audio sink every frame, lets move this logic out ibefore the main loop and pass the Sink in to the play_audio function:
1869+
1870+
```rust
1871+
let core_api;
1872+
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
1873+
let sink = Sink::try_new(&stream_handle).unwrap();
1874+
```
1875+
1876+
Now just pass it into the call to `play_audio` like so:
1877+
1878+
```rust
1879+
play_audio(&sink);
1880+
```
1881+
1882+
You will notice that this has helped the frame rate a bit (around 30 fps on my machine) but its still half of what it was before we added audio support, in the next step we can sort this.
1883+
1884+
> Size of executable so far: 1.4MB
1885+
1886+
# Step 23 - Creating an Audio Thread
1887+
1888+
Audio processing is very cpu intensive and so far we have done all our logic in a single thread, this is now affecting the frame rate of games being played in our frontend. One solution for this is to put the audio processing in its own thread and just pass the audio data between the threads.
1889+
1890+
17711891

1772-
# Step 23 - Game Controller support
1892+
# Step ? - Game Controller support

Diff for: src/main.rs

+45-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ extern crate libloading;
33
use clap::{App, Arg};
44

55
use libloading::Library;
6-
use libretro_sys::{CoreAPI, GameInfo, PixelFormat};
6+
use libretro_sys::{CoreAPI, GameInfo, PixelFormat, SystemAvInfo, GameGeometry, SystemTiming};
77
use minifb::{Key, KeyRepeat, Window, WindowOptions};
88
use std::collections::HashMap;
99
use std::ffi::{c_void, CString};
@@ -13,13 +13,17 @@ use std::io::{BufRead, BufReader};
1313
use std::path::{Path, PathBuf};
1414
use std::time::{Duration, Instant};
1515
use std::{env, fs, ptr}; // Add this line to import the Read trait
16+
use rodio::{Sink, OutputStream, OutputStreamHandle};
17+
use rodio::buffer::SamplesBuffer;
18+
1619

1720
const EXPECTED_LIB_RETRO_VERSION: u32 = 1;
1821

1922
struct EmulatorState {
2023
rom_name: String,
2124
core_name: String,
2225
frame_buffer: Option<Vec<u32>>,
26+
audio_data: Option<Vec<i16>>,
2327
pixel_format: PixelFormat,
2428
bytes_per_pixel: u8, // its only either 2 or 4 bytes per pixel in libretro
2529
screen_pitch: u32,
@@ -33,6 +37,7 @@ static mut CURRENT_EMULATOR_STATE: EmulatorState = EmulatorState {
3337
rom_name: String::new(),
3438
core_name: String::new(),
3539
frame_buffer: None,
40+
audio_data: None,
3641
pixel_format: PixelFormat::ARGB8888,
3742
bytes_per_pixel: 4,
3843
screen_pitch: 0,
@@ -150,15 +155,17 @@ unsafe extern "C" fn libretro_set_input_state_callback(
150155
}
151156

152157
unsafe extern "C" fn libretro_set_audio_sample_callback(left: i16, right: i16) {
153-
// println!("libretro_set_audio_sample_callback");
158+
println!("libretro_set_audio_sample_callback left channel: {} right: {}", left, right);
154159
}
155160

161+
const AUDIO_CHANNELS: usize = 2; // left and right
156162
unsafe extern "C" fn libretro_set_audio_sample_batch_callback(
157-
data: *const i16,
163+
audio_data: *const i16,
158164
frames: libc::size_t,
159165
) -> libc::size_t {
160-
// println!("libretro_set_audio_sample_batch_callback");
161-
return 1;
166+
let audio_slice = std::slice::from_raw_parts(audio_data, frames * AUDIO_CHANNELS);
167+
CURRENT_EMULATOR_STATE.audio_data = Some(audio_slice.to_vec());
168+
return frames;
162169
}
163170

164171
unsafe extern "C" fn libretro_environment_callback(command: u32, return_data: *mut c_void) -> bool {
@@ -366,6 +373,21 @@ unsafe fn load_rom_file(core_api: &CoreAPI, rom_name: &String) -> bool {
366373
return was_load_successful;
367374
}
368375

376+
unsafe fn play_audio( sink: &Sink) {
377+
match &CURRENT_EMULATOR_STATE.audio_data {
378+
Some(data) => {
379+
if sink.empty() {
380+
let audio_slice = std::slice::from_raw_parts(data.as_ptr() as *const i16, data.len());
381+
let source = SamplesBuffer::new(2, 32768*2, audio_slice);
382+
sink.append(source);
383+
sink.play();
384+
sink.sleep_until_end();
385+
}
386+
},
387+
None => {},
388+
};
389+
}
390+
369391
fn get_save_state_path(
370392
save_directory: &String,
371393
game_file_name: &str,
@@ -513,16 +535,32 @@ fn main() {
513535
let mut fps_timer = Instant::now();
514536
let mut fps_counter = 0;
515537
let core_api;
538+
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
539+
let sink = Sink::try_new(&stream_handle).unwrap();
516540

517541
unsafe {
518542
core_api = load_core(&CURRENT_EMULATOR_STATE.core_name);
519543
(core_api.retro_init)();
544+
let mut av_info = SystemAvInfo {
545+
geometry: GameGeometry {
546+
base_width: 0,
547+
base_height: 0,
548+
max_width: 0,
549+
max_height: 0,
550+
aspect_ratio: 0.0,
551+
},
552+
timing: SystemTiming {
553+
fps: 0.0,
554+
sample_rate: 0.0,
555+
},
556+
};
557+
(core_api.retro_get_system_av_info)(&mut av_info);
558+
println!("AV Info: {:?}", &av_info);
520559
println!("About to load ROM: {}", CURRENT_EMULATOR_STATE.rom_name);
521560
load_rom_file(&core_api, &CURRENT_EMULATOR_STATE.rom_name);
522561
}
523562

524563
window.limit_update_rate(Some(std::time::Duration::from_micros(16600))); // Limit to ~60fps
525-
526564
while window.is_open() && !window.is_key_down(Key::Escape) {
527565
// Call the libRetro core every frame
528566
unsafe {
@@ -584,6 +622,7 @@ fn main() {
584622
}
585623

586624
CURRENT_EMULATOR_STATE.buttons_pressed = Some(this_frames_pressed_buttons);
625+
play_audio(&sink);
587626

588627
match &CURRENT_EMULATOR_STATE.frame_buffer {
589628
Some(buffer) => {

0 commit comments

Comments
 (0)