Skip to content

Commit 47a70e4

Browse files
committed
io(rtl),dsp,tests: fix FS/4 rotation phase handling
1 parent 23d9e5f commit 47a70e4

9 files changed

Lines changed: 1122 additions & 223 deletions

File tree

include/dsd-neo/dsp/simd_widen.h

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,49 @@ void widen_u8_to_f32_bias127(const unsigned char* src, float* dst, uint32_t len)
6060
*/
6161
void widen_rotate90_u8_to_f32_bias127(const unsigned char* src, float* dst, uint32_t len);
6262

63+
/**
64+
* @brief Rotate 90° (IQ) and widen u8→float centered at 127.5 with explicit phase.
65+
*
66+
* Applies the `j^n` sequence starting at `phase & 3`, where phase 0 leaves the
67+
* first I/Q pair unchanged. Processes `floor(len/2)` pairs and returns the phase
68+
* to use for the next pair-aligned chunk.
69+
*
70+
* Callers must preserve I/Q byte alignment themselves. If a transport split can
71+
* leave one dangling byte, buffer that byte externally and resume with a
72+
* pair-aligned span on the next call.
73+
*
74+
* @param src Source buffer of unsigned bytes (I/Q interleaved).
75+
* @param dst Destination float buffer.
76+
* @param len Number of bytes in src to process.
77+
* @param phase Starting rotation phase in [0, 3]; other bits are ignored.
78+
* @return Next rotation phase after processing the available I/Q pairs.
79+
*/
80+
uint32_t widen_rotate90_u8_to_f32_bias127_phase(const unsigned char* src, float* dst, uint32_t len, uint32_t phase);
81+
82+
/**
83+
* @brief Legacy in-place byte-domain 90° IQ rotation with explicit phase.
84+
*
85+
* Rotates interleaved u8 I/Q samples using byte-domain negation (`255 - x`).
86+
* Intended to pair with `widen_u8_to_f32_bias128_scalar()` for the legacy
87+
* two-pass RTL path. Processes `floor(len/2)` pairs and returns the phase to
88+
* use for the next pair-aligned chunk.
89+
*
90+
* Callers must preserve I/Q byte alignment themselves. If a transport split can
91+
* leave one dangling byte, buffer that byte externally and resume with a
92+
* pair-aligned span on the next call.
93+
*
94+
* @param buf Buffer of interleaved I/Q bytes to rotate in place.
95+
* @param len Number of bytes in buf to process.
96+
* @param phase Starting rotation phase in [0, 3]; other bits are ignored.
97+
* @return Next rotation phase after processing the available I/Q pairs.
98+
*/
99+
uint32_t rotate90_u8_inplace_phase(unsigned char* buf, uint32_t len, uint32_t phase);
100+
63101
/**
64102
* @brief Widen u8 to float centered at 128 (for legacy pre-rotation negation).
65103
*
66104
* Scalar widening that subtracts 128 instead of 127. Intended to pair with
67-
* legacy byte-wise rotate_90(u8) which performs 255-x negation so that overall
68-
* effect equals correct centered negation (127-x).
105+
* legacy byte-wise `rotate90_u8_inplace_phase()` which performs 255-x negation.
69106
*
70107
* @param src Source buffer of unsigned bytes.
71108
* @param dst Destination float buffer.

include/dsd-neo/io/rtl_device.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,11 +201,15 @@ int rtl_device_stop_async(struct rtl_device* dev);
201201
* @brief Mute the incoming raw USB/TCP byte stream for a short duration.
202202
*
203203
* Note: The argument is in raw input BYTES (u8 I/Q interleaved), not int16
204-
* samples. This matches how the underlying callback consumes the value
205-
* (clamping and subtracting from the remaining byte count per callback).
204+
* samples. The requested mute span is rounded up to whole I/Q pairs, but the
205+
* internal remaining byte count may still go odd between rtl_tcp callbacks
206+
* because TCP read boundaries are arbitrary. If a prior chunk ended on an odd
207+
* byte boundary, the implementation may discard one additional future byte
208+
* internally so unmuted processing resumes on an I/Q boundary.
206209
*
207210
* @param dev RTL-SDR device handle.
208-
* @param bytes Number of input bytes to overwrite with 0x7F (mute).
211+
* @param bytes Number of input bytes to discard while muted. Odd values are
212+
* rounded up so the muted span always covers whole I/Q pairs.
209213
*/
210214
void rtl_device_mute(struct rtl_device* dev, int bytes);
211215

src/dsp/simd_widen.cpp

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,51 @@
1616

1717
#include <stdint.h>
1818

19+
static inline void
20+
apply_j4_rotation_f32(float in_i, float in_q, uint32_t phase, float* out_i, float* out_q) {
21+
switch (phase & 3U) {
22+
case 0:
23+
*out_i = in_i;
24+
*out_q = in_q;
25+
break;
26+
case 1:
27+
*out_i = -in_q;
28+
*out_q = in_i;
29+
break;
30+
case 2:
31+
*out_i = -in_i;
32+
*out_q = -in_q;
33+
break;
34+
default:
35+
*out_i = in_q;
36+
*out_q = -in_i;
37+
break;
38+
}
39+
}
40+
41+
static inline void
42+
apply_j4_rotation_u8(unsigned char in_i, unsigned char in_q, uint32_t phase, unsigned char* out_i,
43+
unsigned char* out_q) {
44+
switch (phase & 3U) {
45+
case 0:
46+
*out_i = in_i;
47+
*out_q = in_q;
48+
break;
49+
case 1:
50+
*out_i = (unsigned char)(255U - (uint32_t)in_q);
51+
*out_q = in_i;
52+
break;
53+
case 2:
54+
*out_i = (unsigned char)(255U - (uint32_t)in_i);
55+
*out_q = (unsigned char)(255U - (uint32_t)in_q);
56+
break;
57+
default:
58+
*out_i = in_q;
59+
*out_q = (unsigned char)(255U - (uint32_t)in_i);
60+
break;
61+
}
62+
}
63+
1964
void
2065
widen_u8_to_f32_bias127(const unsigned char* src, float* dst, uint32_t len) {
2166
if (!src || !dst || len == 0) {
@@ -40,35 +85,48 @@ widen_u8_to_f32_bias128_scalar(const unsigned char* src, float* dst, uint32_t le
4085
}
4186
}
4287

43-
void
44-
widen_rotate90_u8_to_f32_bias127(const unsigned char* src, float* dst, uint32_t len) {
88+
uint32_t
89+
widen_rotate90_u8_to_f32_bias127_phase(const unsigned char* src, float* dst, uint32_t len, uint32_t phase) {
90+
uint32_t cur_phase = phase & 3U;
4591
if (!src || !dst || len < 2) {
46-
return;
92+
return cur_phase;
4793
}
94+
4895
const float inv = 1.0f / 127.5f;
4996
uint32_t pairs = len >> 1;
5097
for (uint32_t n = 0; n < pairs; n++) {
5198
uint32_t idx = n << 1;
5299
float i_raw = ((float)src[idx + 0] - 127.5f) * inv;
53100
float q_raw = ((float)src[idx + 1] - 127.5f) * inv;
54-
float ri = i_raw;
55-
float rq = q_raw;
56-
switch (n & 3U) {
57-
case 1:
58-
ri = -q_raw;
59-
rq = i_raw;
60-
break; /* +90° */
61-
case 2:
62-
ri = -i_raw;
63-
rq = -q_raw;
64-
break; /* 180° */
65-
case 3:
66-
ri = q_raw;
67-
rq = -i_raw;
68-
break; /* -90° */
69-
default: break; /**/
70-
}
71-
dst[idx + 0] = ri;
72-
dst[idx + 1] = rq;
101+
apply_j4_rotation_f32(i_raw, q_raw, cur_phase, &dst[idx + 0], &dst[idx + 1]);
102+
cur_phase = (cur_phase + 1U) & 3U;
103+
}
104+
105+
return cur_phase;
106+
}
107+
108+
void
109+
widen_rotate90_u8_to_f32_bias127(const unsigned char* src, float* dst, uint32_t len) {
110+
(void)widen_rotate90_u8_to_f32_bias127_phase(src, dst, len, 0U);
111+
}
112+
113+
uint32_t
114+
rotate90_u8_inplace_phase(unsigned char* buf, uint32_t len, uint32_t phase) {
115+
uint32_t cur_phase = phase & 3U;
116+
if (!buf || len < 2) {
117+
return cur_phase;
73118
}
119+
120+
uint32_t pairs = len >> 1;
121+
for (uint32_t n = 0; n < pairs; n++) {
122+
uint32_t idx = n << 1;
123+
unsigned char out_i = 0;
124+
unsigned char out_q = 0;
125+
apply_j4_rotation_u8(buf[idx + 0], buf[idx + 1], cur_phase, &out_i, &out_q);
126+
buf[idx + 0] = out_i;
127+
buf[idx + 1] = out_q;
128+
cur_phase = (cur_phase + 1U) & 3U;
129+
}
130+
131+
return cur_phase;
74132
}

src/io/radio/rtl_capture_phase.h

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
/*
3+
* Copyright (C) 2026 by arancormonk <180709949+arancormonk@users.noreply.github.com>
4+
*/
5+
6+
/**
7+
* @file
8+
* @brief Helpers for preserving capture-side FS/4 rotation phase across chunks and drops.
9+
*/
10+
11+
#ifndef DSD_NEO_IO_RADIO_RTL_CAPTURE_PHASE_H
12+
#define DSD_NEO_IO_RADIO_RTL_CAPTURE_PHASE_H
13+
14+
#include <limits.h>
15+
#include <stddef.h>
16+
17+
struct rtl_capture_u8_byte_carry {
18+
unsigned char byte;
19+
unsigned int valid;
20+
};
21+
22+
static inline void
23+
rtl_capture_u8_byte_carry_clear(struct rtl_capture_u8_byte_carry* carry) {
24+
if (!carry) {
25+
return;
26+
}
27+
carry->byte = 0;
28+
carry->valid = 0;
29+
}
30+
31+
static inline void
32+
rtl_capture_u8_byte_carry_save(struct rtl_capture_u8_byte_carry* carry, unsigned char byte) {
33+
if (!carry) {
34+
return;
35+
}
36+
carry->byte = byte;
37+
carry->valid = 1U;
38+
}
39+
40+
static inline size_t
41+
rtl_capture_u8_byte_carry_ready_bytes(size_t byte_count, const struct rtl_capture_u8_byte_carry* carry) {
42+
size_t total = byte_count + (size_t)((carry && carry->valid) ? 1U : 0U);
43+
return total & ~((size_t)1U);
44+
}
45+
46+
static inline size_t
47+
rtl_capture_u8_byte_carry_consume_prefix(const unsigned char* src, size_t byte_count,
48+
struct rtl_capture_u8_byte_carry* carry, unsigned char out_pair[2]) {
49+
if (!src || byte_count == 0 || !carry || !carry->valid || !out_pair) {
50+
return 0;
51+
}
52+
out_pair[0] = carry->byte;
53+
out_pair[1] = src[0];
54+
rtl_capture_u8_byte_carry_clear(carry);
55+
return 1;
56+
}
57+
58+
static inline size_t
59+
rtl_capture_u8_byte_carry_drop_aligned(const unsigned char* src, size_t byte_count,
60+
struct rtl_capture_u8_byte_carry* carry) {
61+
size_t total = byte_count + (size_t)((carry && carry->valid) ? 1U : 0U);
62+
if ((total & 1U) == 0U) {
63+
rtl_capture_u8_byte_carry_clear(carry);
64+
return total;
65+
}
66+
if (!src || byte_count == 0) {
67+
return 0;
68+
}
69+
rtl_capture_u8_byte_carry_save(carry, src[byte_count - 1]);
70+
return total - 1U;
71+
}
72+
73+
static inline int
74+
rtl_capture_phase_advance_pairs(int phase, size_t pair_count) {
75+
return (phase + (int)(pair_count & 3U)) & 3;
76+
}
77+
78+
static inline int
79+
rtl_capture_phase_advance_u8_bytes(int phase, size_t byte_count) {
80+
/* Raw u8 input is interleaved I/Q, so only full byte pairs advance the j^n state. */
81+
return rtl_capture_phase_advance_pairs(phase, byte_count >> 1);
82+
}
83+
84+
static inline int
85+
rtl_capture_phase_advance_u8_bytes_fragmented(int phase, size_t byte_count, unsigned int* partial_byte_count) {
86+
if (!partial_byte_count) {
87+
return rtl_capture_phase_advance_u8_bytes(phase, byte_count);
88+
}
89+
size_t total = byte_count + (size_t)(*partial_byte_count & 1U);
90+
*partial_byte_count = (unsigned int)(total & 1U);
91+
return rtl_capture_phase_advance_pairs(phase, total >> 1);
92+
}
93+
94+
static inline int
95+
rtl_capture_align_u8_iq_bytes(int byte_count) {
96+
if (byte_count <= 0) {
97+
return 0;
98+
}
99+
if ((byte_count & 1) == 0) {
100+
return byte_count;
101+
}
102+
if (byte_count >= (INT_MAX - 1)) {
103+
return INT_MAX - 1;
104+
}
105+
return byte_count + 1;
106+
}
107+
108+
static inline int
109+
rtl_capture_restart_fragmented_u8_bytes(int byte_count, unsigned int* partial_byte_count) {
110+
/* A new stream boundary cannot complete a partial I/Q byte from the old stream. */
111+
if (partial_byte_count) {
112+
*partial_byte_count = 0;
113+
}
114+
return rtl_capture_align_u8_iq_bytes(byte_count);
115+
}
116+
117+
static inline int
118+
rtl_capture_restart_u8_stream(int* phase, int byte_count, unsigned int* partial_byte_count) {
119+
/* A fresh stream always resumes with phase-0 sample alignment. */
120+
if (phase) {
121+
*phase = 0;
122+
}
123+
return rtl_capture_restart_fragmented_u8_bytes(byte_count, partial_byte_count);
124+
}
125+
126+
static inline int
127+
rtl_capture_restart_u8_stream_with_pending(int* phase, int byte_count, unsigned int* partial_byte_count,
128+
size_t* pending_byte_count) {
129+
/* Buffered raw bytes cannot be stitched across an explicit stream restart. */
130+
if (pending_byte_count) {
131+
*pending_byte_count = 0;
132+
}
133+
return rtl_capture_restart_u8_stream(phase, byte_count, partial_byte_count);
134+
}
135+
136+
#endif /* DSD_NEO_IO_RADIO_RTL_CAPTURE_PHASE_H */

0 commit comments

Comments
 (0)