Skip to content

Commit 0d048b3

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 567f1ae commit 0d048b3

File tree

4 files changed

+57
-30
lines changed

4 files changed

+57
-30
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: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
pub mod f32 {
2-
#[allow(unused_imports)]
3-
use core;
4-
52
#[cfg(not(feature = "std"))]
3+
#[inline]
64
pub fn sqrt(x: f32) -> f32 {
75
if x >= 0.0 {
86
f32::from_bits((x.to_bits() + 0x3f80_0000) >> 1)
@@ -11,16 +9,28 @@ pub mod f32 {
119
}
1210
}
1311
#[cfg(feature = "std")]
12+
#[inline]
1413
pub fn sqrt(x: f32) -> f32 {
1514
x.sqrt()
1615
}
16+
17+
#[cfg(not(feature = "std"))]
18+
#[inline]
19+
pub fn round(x: f32) -> f32 {
20+
// Branchless rounding: copysign gives +0.5 for positive x, -0.5 for negative x
21+
// This shifts the value toward zero before truncation, achieving proper rounding
22+
(x + 0.5_f32.copysign(x)) as i64 as f32
23+
}
24+
#[cfg(feature = "std")]
25+
#[inline]
26+
pub fn round(x: f32) -> f32 {
27+
x.round()
28+
}
1729
}
1830

1931
pub mod f64 {
20-
#[allow(unused_imports)]
21-
use core;
22-
2332
#[cfg(not(feature = "std"))]
33+
#[inline]
2434
pub fn sqrt(x: f64) -> f64 {
2535
if x >= 0.0 {
2636
f64::from_bits((x.to_bits() + 0x3f80_0000) >> 1)
@@ -29,7 +39,21 @@ pub mod f64 {
2939
}
3040
}
3141
#[cfg(feature = "std")]
42+
#[inline]
3243
pub fn sqrt(x: f64) -> f64 {
3344
x.sqrt()
3445
}
46+
47+
#[cfg(not(feature = "std"))]
48+
#[inline]
49+
pub fn round(x: f64) -> f64 {
50+
// Branchless rounding: copysign gives +0.5 for positive x, -0.5 for negative x
51+
// This shifts the value toward zero before truncation, achieving proper rounding
52+
(x + 0.5_f64.copysign(x)) as i64 as f64
53+
}
54+
#[cfg(feature = "std")]
55+
#[inline]
56+
pub fn round(x: f64) -> f64 {
57+
x.round()
58+
}
3559
}

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)