Skip to content

Commit 109e890

Browse files
refactor: update SensorDataset and parser for improved microphone data handling
1 parent 11e4ab7 commit 109e890

3 files changed

Lines changed: 156 additions & 125 deletions

File tree

src/open_earable_python/dataset.py

Lines changed: 38 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,10 @@ class SensorDataset:
132132
def __init__(self, filename: str, verbose: bool = False):
133133
self.filename = filename
134134
self.verbose = verbose
135-
self.parse_result: Dict[int, List] = defaultdict(list)
135+
self.parse_result: parser.ParseResult = parser.ParseResult(
136+
sensor_dfs={},
137+
mic_samples=[],
138+
)
136139
# Per-SID dataframes built in _build_accessors
137140
self.sensor_dfs: Dict[int, pd.DataFrame] = {}
138141
self.audio_stereo: Optional[np.ndarray] = None
@@ -141,101 +144,31 @@ def __init__(self, filename: str, verbose: bool = False):
141144
self.bone_sound: Optional[np.ndarray] = None
142145
self.df: pd.DataFrame = pd.DataFrame()
143146

144-
self.imu = _SensorAccessor(pd.DataFrame(columns=LABELS["imu"]), LABELS["imu"])
145-
self.barometer = _SensorAccessor(pd.DataFrame(columns=LABELS["barometer"]), LABELS["barometer"])
146-
self.ppg = _SensorAccessor(pd.DataFrame(columns=LABELS["ppg"]), LABELS["ppg"])
147-
self.bone_acc = _SensorAccessor(pd.DataFrame(columns=LABELS["bone_acc"]), LABELS["bone_acc"])
148-
self.optical_temp = _SensorAccessor(pd.DataFrame(columns=LABELS["optical_temp"]), LABELS["optical_temp"])
149-
self.microphone = _SensorAccessor(pd.DataFrame(columns=LABELS["microphone"]), LABELS["microphone"])
150-
151-
self.parser: parser.Parser = parser.Parser({
152-
self.SENSOR_SID["imu"]: parser.SchemePayloadParser(scheme.SensorScheme(
153-
name='imu',
154-
sid=self.SENSOR_SID["imu"],
155-
groups=[
156-
scheme.SensorComponentGroupScheme(
157-
name='acc',
158-
components=[
159-
scheme.SensorComponentScheme('x', scheme.ParseType.FLOAT),
160-
scheme.SensorComponentScheme('y', scheme.ParseType.FLOAT),
161-
scheme.SensorComponentScheme('z', scheme.ParseType.FLOAT),
162-
]
163-
),
164-
scheme.SensorComponentGroupScheme(
165-
name='gyro',
166-
components=[
167-
scheme.SensorComponentScheme('x', scheme.ParseType.FLOAT),
168-
scheme.SensorComponentScheme('y', scheme.ParseType.FLOAT),
169-
scheme.SensorComponentScheme('z', scheme.ParseType.FLOAT),
170-
]
171-
),
172-
scheme.SensorComponentGroupScheme(
173-
name='mag',
174-
components=[
175-
scheme.SensorComponentScheme('x', scheme.ParseType.FLOAT),
176-
scheme.SensorComponentScheme('y', scheme.ParseType.FLOAT),
177-
scheme.SensorComponentScheme('z', scheme.ParseType.FLOAT),
178-
]
179-
),
180-
])),
181-
self.SENSOR_SID["barometer"]: parser.SchemePayloadParser(scheme.SensorScheme(
182-
name='barometer',
183-
sid=self.SENSOR_SID["barometer"],
184-
groups=[
185-
scheme.SensorComponentGroupScheme(
186-
name='barometer',
187-
components=[
188-
scheme.SensorComponentScheme('temperature', scheme.ParseType.FLOAT),
189-
scheme.SensorComponentScheme('pressure', scheme.ParseType.FLOAT),
190-
]
191-
),
192-
])),
193-
self.SENSOR_SID["ppg"]: parser.SchemePayloadParser(scheme.SensorScheme(
194-
name='ppg',
195-
sid=self.SENSOR_SID["ppg"],
196-
groups=[
197-
scheme.SensorComponentGroupScheme(
198-
name='ppg',
199-
components=[
200-
scheme.SensorComponentScheme('red', scheme.ParseType.UINT32),
201-
scheme.SensorComponentScheme('ir', scheme.ParseType.UINT32),
202-
scheme.SensorComponentScheme('green', scheme.ParseType.UINT32),
203-
scheme.SensorComponentScheme('ambient', scheme.ParseType.UINT32),
204-
]
205-
),
206-
])),
207-
self.SENSOR_SID["optical_temp"]: parser.SchemePayloadParser(scheme.SensorScheme(
208-
name='optical_temp',
209-
sid=self.SENSOR_SID["optical_temp"],
210-
groups=[
211-
scheme.SensorComponentGroupScheme(
212-
name='optical_temp',
213-
components=[
214-
scheme.SensorComponentScheme('optical_temp', scheme.ParseType.FLOAT),
215-
]
216-
),
217-
])),
218-
self.SENSOR_SID["bone_acc"]: parser.SchemePayloadParser(scheme.SensorScheme(
219-
name='bone_acc',
220-
sid=self.SENSOR_SID["bone_acc"],
221-
groups=[
222-
scheme.SensorComponentGroupScheme(
223-
name='bone_acc',
224-
components=[
225-
scheme.SensorComponentScheme('x', scheme.ParseType.INT16),
226-
scheme.SensorComponentScheme('y', scheme.ParseType.INT16),
227-
scheme.SensorComponentScheme('z', scheme.ParseType.INT16),
228-
]
229-
),
230-
])),
231-
self.SENSOR_SID["microphone"]: parser.MicPayloadParser(
232-
sample_count=48000,
233-
),
234-
}, verbose=verbose)
147+
for sensor_name, labels in LABELS.items():
148+
setattr(
149+
self,
150+
sensor_name,
151+
_SensorAccessor(pd.DataFrame(columns=labels), labels),
152+
)
153+
154+
self.parser: parser.Parser = self._build_parser(verbose=verbose)
235155

236156
self.parse()
237157
self._build_accessors()
238158

159+
@classmethod
160+
def _build_parser(cls, verbose: bool = False) -> parser.Parser:
161+
sensor_schemes = scheme.build_default_sensor_schemes(cls.SENSOR_SID)
162+
dataset_parser = parser.Parser.from_sensor_schemes(
163+
sensor_schemes=sensor_schemes,
164+
verbose=verbose,
165+
)
166+
dataset_parser.parsers[cls.SENSOR_SID["microphone"]] = parser.MicPayloadParser(
167+
sample_count=48000,
168+
verbose=verbose,
169+
)
170+
return dataset_parser
171+
239172
def parse(self) -> None:
240173
"""Parse the binary recording file into structured sensor data."""
241174
with open(self.filename, "rb") as f:
@@ -252,10 +185,11 @@ def _build_accessors(self) -> None:
252185
self.audio_stereo = self.parse_result.audio_stereo
253186
self.audio_df = pd.DataFrame()
254187
self._audio_df_sampling_rate = None
188+
self.sensor_dfs = {}
255189

256190
data_dict = self.parse_result.sensor_dfs
257191
for name, sid in self.SENSOR_SID.items():
258-
labels = LABELS.get(name, [f"val{i}" for i in range(0)])
192+
labels = LABELS.get(name, [])
259193
if name == "microphone":
260194
df = self.get_audio_dataframe()
261195
elif sid in data_dict and isinstance(data_dict[sid], pd.DataFrame):
@@ -348,10 +282,7 @@ def get_audio_dataframe(self, sampling_rate: int = 48000) -> pd.DataFrame:
348282
if sampling_rate <= 0:
349283
raise ValueError(f"sampling_rate must be > 0, got {sampling_rate}")
350284

351-
if (
352-
self._audio_df_sampling_rate == sampling_rate
353-
and not self.audio_df.empty
354-
):
285+
if self._audio_df_sampling_rate == sampling_rate:
355286
return self.audio_df
356287

357288
mic_packets = getattr(self.parse_result, "mic_packets", [])
@@ -362,27 +293,17 @@ def get_audio_dataframe(self, sampling_rate: int = 48000) -> pd.DataFrame:
362293
return self.audio_df
363294

364295
timestamps: List[np.ndarray] = []
365-
inner_values: List[np.ndarray] = []
366-
outer_values: List[np.ndarray] = []
296+
stereo_frames: List[np.ndarray] = []
367297

368298
for packet in mic_packets:
369-
samples = np.asarray(packet["samples"], dtype=np.int16)
370-
if samples.size < 2:
299+
ts, stereo = parser.mic_packet_to_stereo_frames(
300+
packet=packet,
301+
sampling_rate=sampling_rate,
302+
)
303+
if stereo.size == 0:
371304
continue
372-
373-
# Interleaved stream: [outer0, inner0, outer1, inner1, ...]
374-
frame_count = samples.size // 2
375-
trimmed = samples[: frame_count * 2]
376-
377-
outer = trimmed[0::2]
378-
inner = trimmed[1::2]
379-
380-
start_ts = float(packet["timestamp"])
381-
ts = start_ts + (np.arange(frame_count, dtype=np.float64) / sampling_rate)
382-
383305
timestamps.append(ts)
384-
inner_values.append(inner)
385-
outer_values.append(outer)
306+
stereo_frames.append(stereo)
386307

387308
if not timestamps:
388309
self.audio_df = pd.DataFrame(columns=["mic.inner", "mic.outer"])
@@ -391,13 +312,12 @@ def get_audio_dataframe(self, sampling_rate: int = 48000) -> pd.DataFrame:
391312
return self.audio_df
392313

393314
all_ts = np.concatenate(timestamps)
394-
all_inner = np.concatenate(inner_values)
395-
all_outer = np.concatenate(outer_values)
315+
all_stereo = np.vstack(stereo_frames)
396316

397317
self.audio_df = pd.DataFrame(
398318
{
399-
"mic.inner": all_inner,
400-
"mic.outer": all_outer,
319+
"mic.inner": all_stereo[:, 0],
320+
"mic.outer": all_stereo[:, 1],
401321
},
402322
index=all_ts,
403323
)

src/open_earable_python/parser.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
import struct
22
from open_earable_python.scheme import SensorScheme, ParseType
33
import pandas as pd
4-
from typing import BinaryIO, Dict, List, Optional, TypedDict
4+
from typing import BinaryIO, Dict, List, Optional, Tuple, TypedDict, Union
55
from dataclasses import dataclass, field
66
import numpy as np
77

8+
9+
def interleaved_mic_to_stereo(
10+
samples: Union[np.ndarray, List[int], tuple[int, ...]],
11+
) -> np.ndarray:
12+
"""Convert interleaved [outer, inner, ...] int16 samples to [inner, outer] frames."""
13+
interleaved = np.asarray(samples, dtype=np.int16)
14+
if interleaved.size < 2:
15+
return np.empty((0, 2), dtype=np.int16)
16+
17+
frame_count = interleaved.size // 2
18+
interleaved = interleaved[: frame_count * 2]
19+
return np.column_stack((interleaved[1::2], interleaved[0::2]))
20+
21+
822
class PayloadParser:
923
"""Abstract base class for payload parsers.
1024
@@ -57,12 +71,28 @@ class ParseResult:
5771
def mic_samples_to_stereo(mic_samples: List[int]) -> Optional[np.ndarray]:
5872
if not mic_samples:
5973
return None
60-
mic_array = np.array(mic_samples, dtype=np.int16)
61-
# If odd number of samples, drop the last one to ensure even pairing
62-
if len(mic_array) % 2 != 0:
63-
mic_array = mic_array[:-1]
64-
# Original behavior: [inner, outer] = [odd, even]
65-
return np.column_stack((mic_array[1::2], mic_array[0::2]))
74+
stereo = interleaved_mic_to_stereo(mic_samples)
75+
if stereo.size == 0:
76+
return None
77+
return stereo
78+
79+
80+
def mic_packet_to_stereo_frames(
81+
packet: MicPacket,
82+
sampling_rate: int,
83+
) -> Tuple[np.ndarray, np.ndarray]:
84+
"""Return timestamps and stereo frames for a parsed microphone packet."""
85+
if sampling_rate <= 0:
86+
raise ValueError(f"sampling_rate must be > 0, got {sampling_rate}")
87+
88+
stereo = interleaved_mic_to_stereo(packet["samples"])
89+
if stereo.size == 0:
90+
return np.empty((0,), dtype=np.float64), stereo
91+
92+
timestamps = float(packet["timestamp"]) + (
93+
np.arange(stereo.shape[0], dtype=np.float64) / sampling_rate
94+
)
95+
return timestamps, stereo
6696

6797
class Parser:
6898
def __init__(self, parsers: dict[int, PayloadParser], verbose: bool = False):

src/open_earable_python/scheme.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import enum
2+
from typing import Dict, Mapping, Sequence
23

34
class ParseType(enum.Enum):
45
UINT8 = "uint8"
@@ -38,3 +39,83 @@ def __init__(self, name: str, sid: int, groups: list[SensorComponentGroupScheme]
3839

3940
def __repr__(self):
4041
return f"SensorScheme(name={self.name}, sid={self.sid}, groups={self.groups})"
42+
43+
44+
def _group(
45+
name: str,
46+
components: Sequence[tuple[str, ParseType]],
47+
) -> SensorComponentGroupScheme:
48+
return SensorComponentGroupScheme(
49+
name=name,
50+
components=[
51+
SensorComponentScheme(component_name, parse_type)
52+
for component_name, parse_type in components
53+
],
54+
)
55+
56+
57+
def build_default_sensor_schemes(sensor_sid: Mapping[str, int]) -> Dict[int, SensorScheme]:
58+
"""Build default non-microphone sensor schemes keyed by SID."""
59+
return {
60+
sensor_sid["imu"]: SensorScheme(
61+
name="imu",
62+
sid=sensor_sid["imu"],
63+
groups=[
64+
_group(
65+
"acc",
66+
[("x", ParseType.FLOAT), ("y", ParseType.FLOAT), ("z", ParseType.FLOAT)],
67+
),
68+
_group(
69+
"gyro",
70+
[("x", ParseType.FLOAT), ("y", ParseType.FLOAT), ("z", ParseType.FLOAT)],
71+
),
72+
_group(
73+
"mag",
74+
[("x", ParseType.FLOAT), ("y", ParseType.FLOAT), ("z", ParseType.FLOAT)],
75+
),
76+
],
77+
),
78+
sensor_sid["barometer"]: SensorScheme(
79+
name="barometer",
80+
sid=sensor_sid["barometer"],
81+
groups=[
82+
_group(
83+
"barometer",
84+
[
85+
("temperature", ParseType.FLOAT),
86+
("pressure", ParseType.FLOAT),
87+
],
88+
)
89+
],
90+
),
91+
sensor_sid["ppg"]: SensorScheme(
92+
name="ppg",
93+
sid=sensor_sid["ppg"],
94+
groups=[
95+
_group(
96+
"ppg",
97+
[
98+
("red", ParseType.UINT32),
99+
("ir", ParseType.UINT32),
100+
("green", ParseType.UINT32),
101+
("ambient", ParseType.UINT32),
102+
],
103+
)
104+
],
105+
),
106+
sensor_sid["optical_temp"]: SensorScheme(
107+
name="optical_temp",
108+
sid=sensor_sid["optical_temp"],
109+
groups=[_group("optical_temp", [("optical_temp", ParseType.FLOAT)])],
110+
),
111+
sensor_sid["bone_acc"]: SensorScheme(
112+
name="bone_acc",
113+
sid=sensor_sid["bone_acc"],
114+
groups=[
115+
_group(
116+
"bone_acc",
117+
[("x", ParseType.INT16), ("y", ParseType.INT16), ("z", ParseType.INT16)],
118+
)
119+
],
120+
),
121+
}

0 commit comments

Comments
 (0)