Rust-SDL2 / rust-sdl2

SDL2 bindings for Rust

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`AudioQueue::queue_audio()` unelegant cast for correct audio.

trip-flip opened this issue · comments

Hello, I have a question concerning the usage of the AudioQueue::queue_audio() method in tandem with another Rust library, ffmpeg_next.

Background

I decided recently to start on working on understanding how to use the libav* libraries, and what better way to start than working with the abstracting layer of ffmpeg_next. Despite diving head first, I was able to get video decoding working with an example provided by that project, and was working on getting audio set up as well (with some help from this tutorial).

To cut it short, I decided to use the queuing feature of SDL2 instead of the callback feature, and I was able to get audio from what is considered the AUDIO_S16SYS/AUDIO_S16LSB format, albeit garbled and quiet. I decided to rewrite the portion of audio code in C++ (also using libav and SDL2) to get a more grounded understanding on what was happening, and after some time, was able to get the correct audio playing as intended!

Technical

So I had to track down the discrepancy between the C++ code and the Rust code and its abstracted libraries. I had a funny feeling about the AudioQueue struct, specifically with Channel generic type that was attached to it. In order to instantiate an AudioQueue, you need to provide a Channel type via a superfish. From my code:

let audio_device = audio.open_queue::<i16, _>(None, &desired_format).unwrap();

i16 correlates to AUDIO_S16SYS/AUDIO_S16LSB, and the format field for the SDL_AudioSpec struct. This generates a method something like this:

pub fn queue_audio(&self, data: &[i16]) -> Result<(), String>

...where i16 would be called Channel, but this is functionally the same.

I decode my packets from into a frame from the ffmpeg_next side of the code. My frame contains a method called Frame::data(), which requires an index argument, since there could be multiple data arrays, but since my audio data in my tests is packed, it should be only in the first (0) index. Basically,

let data: &[u8] = frame.data(0);

This data...has to go into a queue...that can only take &[i16]. Casting each element into i16 doesn't work, and is actually the source of my problem, since doing an as messes with how byte-length of the array is passed over. My solution was this:

let data = unsafe {
	std::slice::from_raw_parts(data.as_ptr() as *const i16, data.len() / 2)
};

which forces the slice into another type, preserving the byte-length of the pointer/array. On that last point, AudioQueue::queue_audio() uses the slice's length, and multiplies it by the size of the Channel type to get the byte-length of the data that it's receiving. data.len() / 2 is there to preserve that length that I received on the first definition of data. And in the end, it still sends the data (effectively) as u8!

Conclusion

The effort I put into this is so I can provide enough context for what I want to do, what I'm expecting, and what I experienced. I wouldn't be surprised if my understanding of Rust is lacking in some areas, and that there may be an easier solution that dipping into unsafe code, but I was curious if this is something that anyone has ran into when trying to queue audio, and if there was a more elegant solution beyond creating an unsafe block. The squarewave example doesn't seem to have a problem with the current implementation from what I have seen by the way.

Thanks for your time and help!

If you want to convert a &[u8] (bytes) into a &[i16] (array of integers) slice, I'd suggest you use the crate bytemuck: https://docs.rs/bytemuck/latest/bytemuck/fn.try_cast_slice.html

Keep in mind that if you look at the implementation, it uses from_raw_parts under the hood, so it's basically the same as your solution, but at least the unsafe is not in your code. https://docs.rs/bytemuck/latest/src/bytemuck/internal.rs.html#353

Haha, I actually found this exact crate after looking on type casting slices. Nonetheless, thanks for the recommendation! I think it might be worthwhile to add a note in the documentation about casting slices in the case someone has incompatible data. I could go about doing that if you think a PR will be merged.