-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
989 additions
and
10 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
[package] | ||
name = "mania-codec" | ||
version.workspace = true | ||
edition.workspace = true | ||
license-file.workspace = true | ||
|
||
[dependencies] | ||
num-traits = "0.2.19" | ||
rubato = "0.16.1" | ||
silk-sys = "0.1.0" | ||
symphonia = { version = "0.5.4", features = ["all"] } | ||
thiserror.workspace = true | ||
num_enum.workspace = true | ||
bytes.workspace = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
## mania-codec | ||
|
||
Audio codec library for Mania | ||
|
||
### Special Thanks | ||
|
||
- [Konata.Codec](https://github.com/KonataDev/Konata.Codec) |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
use crate::audio::decoder::{AudioCodecDecoderError, AudioDecoder}; | ||
use crate::audio::encoder::{AudioCodecEncoderError, AudioEncoder}; | ||
use crate::audio::resampler::{AudioCodecResamplerError, AudioResampler}; | ||
use num_traits::FromPrimitive; | ||
use std::io::{Read, Seek}; | ||
use std::marker::PhantomData; | ||
use symphonia::core::conv::{FromSample, IntoSample}; | ||
use symphonia::core::sample::{i24, u24}; | ||
use thiserror::Error; | ||
|
||
pub mod decoder; | ||
pub mod encoder; | ||
pub mod resampler; | ||
|
||
#[derive(Debug, Error)] | ||
pub enum AudioCodecError { | ||
#[error("Decode error {0}")] | ||
DecodeError(#[from] AudioCodecDecoderError), | ||
#[error("Resample error {0}")] | ||
ResampleError(#[from] AudioCodecResamplerError), | ||
#[error("Encode error {0}")] | ||
EncodeError(#[from] AudioCodecEncoderError), | ||
} | ||
|
||
pub trait Sample: Copy + Send + Sync {} | ||
impl Sample for u8 {} | ||
impl Sample for i16 {} | ||
impl Sample for i32 {} | ||
impl Sample for f32 {} | ||
|
||
pub trait ResampleSample: Sample + IntoSample<f32> {} | ||
impl ResampleSample for i16 {} | ||
impl ResampleSample for f32 {} | ||
|
||
pub trait DecodeSample: | ||
FromSample<u8> | ||
+ FromSample<u16> | ||
+ FromSample<u24> | ||
+ FromSample<u32> | ||
+ FromSample<i8> | ||
+ FromSample<i16> | ||
+ FromSample<i24> | ||
+ FromSample<i32> | ||
+ FromSample<f32> | ||
+ FromSample<f64> | ||
+ FromPrimitive | ||
+ ResampleSample | ||
{ | ||
} | ||
impl DecodeSample for f32 {} | ||
|
||
pub trait EncodeSample: Sample {} | ||
impl EncodeSample for i16 {} | ||
impl EncodeSample for u8 {} | ||
|
||
pub trait RSStream: Read + Seek + Send + Sync { | ||
fn is_seekable(&self) -> bool; | ||
fn byte_len(&self) -> Option<u64>; | ||
} | ||
|
||
impl RSStream for std::fs::File { | ||
fn is_seekable(&self) -> bool { | ||
true | ||
} | ||
|
||
fn byte_len(&self) -> Option<u64> { | ||
self.metadata().ok().map(|m| m.len()) | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
pub struct AudioInfo<T: Sample> { | ||
pub sample_rate: u32, | ||
pub channels: u16, | ||
_phantom: PhantomData<T>, | ||
} | ||
|
||
impl<T: Sample> AudioInfo<T> { | ||
pub fn new(sample_rate: u32, channels: u16) -> Self { | ||
Self { | ||
sample_rate, | ||
channels, | ||
_phantom: PhantomData, | ||
} | ||
} | ||
} | ||
|
||
pub struct AudioRwStream<T: ResampleSample> { | ||
pub stream: Box<dyn RSStream>, | ||
pub info: Option<AudioInfo<T>>, | ||
} | ||
|
||
impl<T: ResampleSample> AudioRwStream<T> { | ||
pub fn new(stream: Box<dyn RSStream>) -> Self { | ||
Self { | ||
stream, | ||
info: None, // cannot get info now | ||
} | ||
} | ||
|
||
pub fn decode<D: AudioDecoder<T>>( | ||
self, | ||
decoder: D, | ||
) -> Result<AudioResampleStream<T>, AudioCodecDecoderError> | ||
where | ||
T: DecodeSample, | ||
{ | ||
decoder.decode(self) | ||
} | ||
} | ||
|
||
pub struct AudioResampleStream<T: ResampleSample> { | ||
pub stream: Vec<T>, | ||
pub info: AudioInfo<T>, | ||
} | ||
|
||
impl<T: ResampleSample> AudioResampleStream<T> { | ||
pub fn resample<U: ResampleSample, R: AudioResampler<T, U>>( | ||
self, | ||
resampler: R, | ||
) -> Result<AudioResampleStream<U>, AudioCodecResamplerError> { | ||
resampler.resample(&self) | ||
} | ||
|
||
pub fn encode<E: AudioEncoder<T>>( | ||
self, | ||
encoder: E, | ||
) -> Result<AudioEncodeStream<T>, AudioCodecEncoderError> | ||
where | ||
T: EncodeSample, | ||
{ | ||
encoder.encode(&self) | ||
} | ||
} | ||
|
||
pub struct AudioEncodeStream<T: EncodeSample> { | ||
pub stream: Vec<u8>, | ||
pub info: AudioInfo<T>, | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
use crate::audio::decoder::symphonia_decoder::SymphoniaDecoder; | ||
use crate::audio::encoder::silk_encoder::SilkEncoder; | ||
use crate::audio::resampler::rubato_resampler::RubatoResampler; | ||
use std::fs::File; | ||
use std::io::Write; | ||
|
||
#[test] | ||
fn test_pipeline() -> Result<(), AudioCodecError> { | ||
let pipeline = AudioRwStream::new(Box::new(File::open("resource/test.mp3").unwrap())) | ||
.decode(SymphoniaDecoder::<f32>::new())? | ||
.resample(RubatoResampler::<i16>::new(24000))? | ||
.encode(SilkEncoder::new(30000))?; | ||
let mut file = File::create("resource/test.silk").unwrap(); | ||
file.write_all(&pipeline.stream).unwrap(); | ||
Ok(()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
pub mod symphonia_decoder; | ||
|
||
use crate::audio::{AudioResampleStream, AudioRwStream, DecodeSample}; | ||
use thiserror::Error; | ||
|
||
#[derive(Debug, Error)] | ||
pub enum AudioCodecDecoderError {} | ||
|
||
pub trait AudioDecoder<T: DecodeSample> { | ||
fn decode( | ||
&self, | ||
input: AudioRwStream<T>, | ||
) -> Result<AudioResampleStream<T>, AudioCodecDecoderError>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
use crate::audio::decoder::{AudioCodecDecoderError, AudioDecoder}; | ||
use crate::audio::{AudioInfo, AudioResampleStream, AudioRwStream, DecodeSample, RSStream}; | ||
use std::io::{Read, Seek, SeekFrom}; | ||
use std::marker::PhantomData; | ||
use symphonia::core::audio::{AudioBufferRef, Signal}; | ||
use symphonia::core::codecs::CODEC_TYPE_NULL; | ||
use symphonia::core::formats::FormatOptions; | ||
use symphonia::core::io::{MediaSource, MediaSourceStream}; | ||
use symphonia::core::meta::MetadataOptions; | ||
use symphonia::core::probe::Hint; | ||
|
||
struct RSStreamAdapter { | ||
inner: Box<dyn RSStream>, | ||
is_seekable: bool, | ||
byte_len: Option<u64>, | ||
} | ||
|
||
impl Read for RSStreamAdapter { | ||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { | ||
self.inner.read(buf) | ||
} | ||
} | ||
|
||
impl Seek for RSStreamAdapter { | ||
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> { | ||
self.inner.seek(pos) | ||
} | ||
} | ||
|
||
impl RSStream for RSStreamAdapter { | ||
fn is_seekable(&self) -> bool { | ||
self.is_seekable | ||
} | ||
|
||
fn byte_len(&self) -> Option<u64> { | ||
self.byte_len | ||
} | ||
} | ||
|
||
impl MediaSource for RSStreamAdapter { | ||
fn is_seekable(&self) -> bool { | ||
self.is_seekable | ||
} | ||
|
||
fn byte_len(&self) -> Option<u64> { | ||
self.byte_len | ||
} | ||
} | ||
|
||
pub struct SymphoniaDecoder<T: DecodeSample> { | ||
_phantom: PhantomData<T>, | ||
} | ||
|
||
impl<T: DecodeSample> SymphoniaDecoder<T> { | ||
pub fn new() -> Self { | ||
Self { | ||
_phantom: PhantomData, | ||
} | ||
} | ||
} | ||
|
||
impl<T: DecodeSample> Default for SymphoniaDecoder<T> { | ||
fn default() -> Self { | ||
Self::new() | ||
} | ||
} | ||
|
||
// FIXME: It is now just a rigid conversion to mono (in the form of [0])! | ||
fn conv<S, T>(samples: &mut Vec<S>, data: std::borrow::Cow<symphonia::core::audio::AudioBuffer<T>>) | ||
where | ||
T: symphonia::core::sample::Sample, | ||
S: symphonia::core::conv::FromSample<T>, | ||
{ | ||
samples.extend(data.chan(0).iter().map(|v| S::from_sample(*v))); | ||
} | ||
|
||
// ref: https://github.com/kyutai-labs/hibiki/blob/main/hibiki-rs/src/audio_io.rs | ||
impl<T: DecodeSample> AudioDecoder<T> for SymphoniaDecoder<T> { | ||
fn decode( | ||
&self, | ||
input: AudioRwStream<T>, | ||
) -> Result<AudioResampleStream<T>, AudioCodecDecoderError> { | ||
let (stream, _) = (input.stream, input.info); | ||
let is_seekable = stream.is_seekable(); | ||
let byte_len = stream.byte_len(); | ||
let adapter = RSStreamAdapter { | ||
inner: stream, | ||
is_seekable, | ||
byte_len, | ||
}; | ||
let mss = MediaSourceStream::new(Box::new(adapter), Default::default()); | ||
let hint = Hint::new(); | ||
let meta_opts: MetadataOptions = Default::default(); | ||
let fmt_opts: FormatOptions = Default::default(); | ||
let probed = symphonia::default::get_probe() | ||
.format(&hint, mss, &fmt_opts, &meta_opts) | ||
.unwrap(); | ||
let mut format = probed.format; | ||
let track = format | ||
.tracks() | ||
.iter() | ||
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL) | ||
.expect("no supported audio tracks"); | ||
let mut decoder = symphonia::default::get_codecs() | ||
.make(&track.codec_params, &Default::default()) | ||
.expect("unsupported codec"); | ||
let track_id = track.id; | ||
let sample_rate = track.codec_params.sample_rate.unwrap_or(0); | ||
let mut pcm_data = Vec::new(); | ||
while let Ok(packet) = format.next_packet() { | ||
while !format.metadata().is_latest() { | ||
format.metadata().pop(); | ||
} | ||
if packet.track_id() != track_id { | ||
continue; | ||
} | ||
match decoder.decode(&packet).unwrap() { | ||
AudioBufferRef::F32(data) => conv(&mut pcm_data, data), | ||
AudioBufferRef::U8(data) => conv(&mut pcm_data, data), | ||
AudioBufferRef::U16(data) => conv(&mut pcm_data, data), | ||
AudioBufferRef::U24(data) => conv(&mut pcm_data, data), | ||
AudioBufferRef::U32(data) => conv(&mut pcm_data, data), | ||
AudioBufferRef::S8(data) => conv(&mut pcm_data, data), | ||
AudioBufferRef::S16(data) => conv(&mut pcm_data, data), | ||
AudioBufferRef::S24(data) => conv(&mut pcm_data, data), | ||
AudioBufferRef::S32(data) => conv(&mut pcm_data, data), | ||
AudioBufferRef::F64(data) => conv(&mut pcm_data, data), | ||
} | ||
} | ||
Ok(AudioResampleStream { | ||
stream: pcm_data, | ||
info: AudioInfo::new(sample_rate, 1), // TODO: channel | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
pub mod silk_encoder; | ||
|
||
use crate::audio::{AudioEncodeStream, AudioResampleStream, EncodeSample, ResampleSample}; | ||
use thiserror::Error; | ||
|
||
#[derive(Debug, Error)] | ||
pub enum AudioCodecEncoderError { | ||
#[error("Silk encoder error: {0}")] | ||
SilkEncoderKnownError(#[from] silk_encoder::SilkError), | ||
#[error("Silk encoder error: {0}")] | ||
SilkEncoderUnknownError(i32), | ||
} | ||
|
||
pub trait AudioEncoder<T: EncodeSample + ResampleSample> { | ||
fn encode( | ||
&self, | ||
input: &AudioResampleStream<T>, | ||
) -> Result<AudioEncodeStream<T>, AudioCodecEncoderError>; | ||
} |
Oops, something went wrong.