Skip to content

Commit 78bf2c3

Browse files
committed
fix: use rounding for float-to-integer conversions
Replace truncating casts with proper rounding in float-to-integer sample conversions to eliminate bias and preserve small signals. Changes: - Use f32::round() and f64::round() instead of truncating `as` casts - Eliminates bias towards zero from truncation behavior - Preserves small audio signals that would otherwise be truncated to zero - Removes nonlinear distortion caused by signal values in (-1.0, 1.0) all mapping to zero, creating an interval twice as large as any other Inlines sqrt and round functions for performance. Additional tests verify proper rounding behavior for cases that would fail with truncation.
1 parent d089297 commit 78bf2c3

File tree

4 files changed

+55
-24
lines changed

4 files changed

+55
-24
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
yielding samples when the underlying signal gets exhausted. This is a breaking
66
change. The return type of the `IntoInterleavedSamples#next_sample` method was
77
modified.
8+
- Improved float-to-integer conversions to use proper rounding instead of truncation.
89

910
---
1011

dasp_sample/src/conv.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@ macro_rules! conversion_fns {
126126
macro_rules! conversions {
127127
($T:ident, $mod_name:ident { $($rest:tt)* }) => {
128128
pub mod $mod_name {
129-
use $crate::types::{I24, U24, I48, U48};
129+
#[allow(unused_imports)]
130+
use $crate::ops;
131+
use $crate::{types::{I24, U24, I48, U48}};
130132
conversion_fns!($T, $($rest)*);
131133
}
132134
};
@@ -531,12 +533,12 @@ conversions!(u64, u64 {
531533
// The following conversions assume `-1.0 <= s < 1.0` (note that +1.0 is excluded) and will
532534
// overflow otherwise.
533535
conversions!(f32, f32 {
534-
s to_i8 { (s * 128.0) as i8 }
535-
s to_i16 { (s * 32_768.0) as i16 }
536-
s to_i24 { I24::new_unchecked((s * 8_388_608.0) as i32) }
537-
s to_i32 { (s * 2_147_483_648.0) as i32 }
538-
s to_i48 { I48::new_unchecked((s * 140_737_488_355_328.0) as i64) }
539-
s to_i64 { (s * 9_223_372_036_854_775_808.0) as i64 }
536+
s to_i8 { ops::f32::round(s * 128.0) as i8 }
537+
s to_i16 { ops::f32::round(s * 32_768.0) as i16 }
538+
s to_i24 { I24::new_unchecked(ops::f32::round(s * 8_388_608.0) as i32) }
539+
s to_i32 { ops::f32::round(s * 2_147_483_648.0) as i32 }
540+
s to_i48 { I48::new_unchecked(ops::f32::round(s * 140_737_488_355_328.0) as i64) }
541+
s to_i64 { ops::f32::round(s * 9_223_372_036_854_775_808.0) as i64 }
540542
s to_u8 { super::i8::to_u8(to_i8(s)) }
541543
s to_u16 { super::i16::to_u16(to_i16(s)) }
542544
s to_u24 { super::i24::to_u24(to_i24(s)) }
@@ -549,12 +551,12 @@ conversions!(f32, f32 {
549551
// The following conversions assume `-1.0 <= s < 1.0` (note that +1.0 is excluded) and will
550552
// overflow otherwise.
551553
conversions!(f64, f64 {
552-
s to_i8 { (s * 128.0) as i8 }
553-
s to_i16 { (s * 32_768.0) as i16 }
554-
s to_i24 { I24::new_unchecked((s * 8_388_608.0) as i32) }
555-
s to_i32 { (s * 2_147_483_648.0) as i32 }
556-
s to_i48 { I48::new_unchecked((s * 140_737_488_355_328.0) as i64) }
557-
s to_i64 { (s * 9_223_372_036_854_775_808.0) as i64 }
554+
s to_i8 { ops::f64::round(s * 128.0) as i8 }
555+
s to_i16 { ops::f64::round(s * 32_768.0) as i16 }
556+
s to_i24 { I24::new_unchecked(ops::f64::round(s * 8_388_608.0) as i32) }
557+
s to_i32 { ops::f64::round(s * 2_147_483_648.0) as i32 }
558+
s to_i48 { I48::new_unchecked(ops::f64::round(s * 140_737_488_355_328.0) as i64) }
559+
s to_i64 { ops::f64::round(s * 9_223_372_036_854_775_808.0) as i64 }
558560
s to_u8 { super::i8::to_u8(to_i8(s)) }
559561
s to_u16 { super::i16::to_u16(to_i16(s)) }
560562
s to_u24 { super::i24::to_u24(to_i24(s)) }

dasp_sample/src/ops.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub mod f32 {
33
/// Uses bit manipulation for initial guess, then 3 iterations for ~6-7 decimal places.
44
/// Accuracy: ~6-7 decimal places
55
#[cfg(not(feature = "std"))]
6+
#[inline]
67
pub fn sqrt(x: f32) -> f32 {
78
if x < 0.0 {
89
return f32::NAN;
@@ -31,13 +32,27 @@ pub mod f32 {
3132
pub fn sqrt(x: f32) -> f32 {
3233
x.sqrt()
3334
}
35+
36+
#[cfg(not(feature = "std"))]
37+
#[inline]
38+
pub fn round(x: f32) -> f32 {
39+
// Branchless rounding: copysign gives +0.5 for positive x, -0.5 for negative x
40+
// This shifts the value toward zero before truncation, achieving proper rounding
41+
(x + 0.5_f32.copysign(x)) as i64 as f32
42+
}
43+
#[cfg(feature = "std")]
44+
#[inline]
45+
pub fn round(x: f32) -> f32 {
46+
x.round()
47+
}
3448
}
3549

3650
pub mod f64 {
3751
/// Newton-Raphson square root implementation for f64.
3852
/// Uses bit manipulation for initial guess, then 4 iterations for ~14-15 decimal places.
3953
/// Accuracy: ~14-15 decimal places
4054
#[cfg(not(feature = "std"))]
55+
#[inline]
4156
pub fn sqrt(x: f64) -> f64 {
4257
if x < 0.0 {
4358
return f64::NAN;
@@ -66,4 +81,17 @@ pub mod f64 {
6681
pub fn sqrt(x: f64) -> f64 {
6782
x.sqrt()
6883
}
84+
85+
#[cfg(not(feature = "std"))]
86+
#[inline]
87+
pub fn round(x: f64) -> f64 {
88+
// Branchless rounding: copysign gives +0.5 for positive x, -0.5 for negative x
89+
// This shifts the value toward zero before truncation, achieving proper rounding
90+
(x + 0.5_f64.copysign(x)) as i64 as f64
91+
}
92+
#[cfg(feature = "std")]
93+
#[inline]
94+
pub fn round(x: f64) -> f64 {
95+
x.round()
96+
}
6997
}

dasp_sample/tests/conv.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -479,11 +479,11 @@ tests!(u64 {
479479
});
480480

481481
tests!(f32 {
482-
to_i8 { -1.0, -128; 0.0, 0; }
483-
to_i16 { -1.0, -32_768; 0.0, 0; }
484-
to_i24 { -1.0, -8_388_608; 0.0, 0; }
485-
to_i32 { -1.0, -2_147_483_648; 0.0, 0; }
486-
to_i48 { -1.0, -140_737_488_355_328; 0.0, 0; }
482+
to_i8 { -1.0, -128; 0.0, 0; 0.1, 13; 0.004, 1; -0.004, -1; 0.003, 0; }
483+
to_i16 { -1.0, -32_768; 0.0, 0; 0.1, 3277; 0.00002, 1; 0.00001, 0; }
484+
to_i24 { -1.0, -8_388_608; 0.0, 0; 0.1, 838861; 0.0000001, 1; -0.0000001, -1; 0.00000005, 0; }
485+
to_i32 { -1.0, -2_147_483_648; 0.0, 0; 0.0000000004, 1; -0.0000000004, -1; 0.0000000002, 0; }
486+
to_i48 { -1.0, -140_737_488_355_328; 0.0, 0; 0.000000000000006, 1; -0.000000000000006, -1; 0.000000000000003, 0; }
487487
to_i64 { -1.0, -9_223_372_036_854_775_808; 0.0, 0; }
488488
to_u8 { -1.0, 0; 0.0, 128; }
489489
to_u16 { -1.0, 0; 0.0, 32_768; }
@@ -495,12 +495,12 @@ tests!(f32 {
495495
});
496496

497497
tests!(f64 {
498-
to_i8 { -1.0, -128; 0.0, 0; }
499-
to_i16 { -1.0, -32_768; 0.0, 0; }
500-
to_i24 { -1.0, -8_388_608; 0.0, 0; }
501-
to_i32 { -1.0, -2_147_483_648; 0.0, 0; }
502-
to_i48 { -1.0, -140_737_488_355_328; 0.0, 0; }
503-
to_i64 { -1.0, -9_223_372_036_854_775_808; 0.0, 0; }
498+
to_i8 { -1.0, -128; 0.0, 0; 0.1, 13; 0.007, 1; -0.004, -1; 0.003, 0; }
499+
to_i16 { -1.0, -32_768; 0.0, 0; 0.1, 3277; 0.00002, 1; -0.00002, -1; 0.00001, 0; }
500+
to_i24 { -1.0, -8_388_608; 0.0, 0; 0.1, 838861; 0.0000001, 1; -0.0000001, -1; 0.00000005, 0; }
501+
to_i32 { -1.0, -2_147_483_648; 0.0, 0; 0.1, 214748365; 0.0000000004, 1; -0.0000000004, -1; 0.0000000002, 0; }
502+
to_i48 { -1.0, -140_737_488_355_328; 0.0, 0; 0.1, 14073748835533; 0.000000000000006, 1; -0.000000000000006, -1; 0.000000000000003, 0; }
503+
to_i64 { -1.0, -9_223_372_036_854_775_808; 0.0, 0; 0.1, 922337203685477632; }
504504
to_u8 { -1.0, 0; 0.0, 128; }
505505
to_u16 { -1.0, 0; 0.0, 32_768; }
506506
to_u24 { -1.0, 0; 0.0, 8_388_608; }

0 commit comments

Comments
 (0)