Skip to content

Commit 3af889e

Browse files
committed
Add fetch_and_reset_stats()
1 parent cdd00d8 commit 3af889e

File tree

5 files changed

+121
-19
lines changed

5 files changed

+121
-19
lines changed

doc/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ API Documentation
1010
.. automodule:: rtmixer
1111

1212
.. autoclass:: Mixer
13-
:members: play_buffer, play_ringbuffer, actions, cancel, wait, stats
13+
:members: play_buffer, play_ringbuffer, actions, cancel, wait, stats,
14+
fetch_and_reset_stats
1415
:undoc-members:
1516

1617
.. autoclass:: Recorder

examples/fetch_stats.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env python3
2+
"""Example that shows how to use fetch_and_reset_stats().
3+
4+
"""
5+
import rtmixer
6+
import sounddevice as sd
7+
8+
9+
blocksize = 0
10+
latency = 0
11+
device = None
12+
samplerate = None
13+
channels = 1
14+
15+
16+
def print_stats(obj):
17+
print(' blocks:', obj.stats.blocks)
18+
print(' overflows:', obj.stats.input_overflows)
19+
20+
21+
stream = rtmixer.Recorder(
22+
device=device, channels=channels, blocksize=blocksize,
23+
latency=latency, samplerate=samplerate)
24+
25+
buffer = bytearray(10 * int(stream.samplerate) * stream.samplesize)
26+
ringbuffer = rtmixer.RingBuffer(stream.samplesize * channels, 128)
27+
28+
print('checking stats before opening stream:')
29+
print_stats(stream)
30+
assert stream.stats.blocks == 0
31+
assert stream.stats.input_overflows == 0
32+
33+
with stream:
34+
print('waiting a few seconds')
35+
sd.sleep(3 * 1000)
36+
print('checking stats:')
37+
action = stream.fetch_and_reset_stats()
38+
stream.wait(action)
39+
print_stats(action)
40+
print('starting recording to buffer')
41+
action = stream.record_buffer(buffer, channels=channels)
42+
# TODO: check if recording started successfully
43+
stream.wait(action)
44+
print('stats from finished recording:')
45+
print_stats(action)
46+
print('starting recording to ringbuffer')
47+
action = stream.record_ringbuffer(ringbuffer, channels=channels)
48+
# TODO: check if recording started successfully
49+
# TODO: record ringbuffer, but don't read from it (detect overflow)
50+
sd.sleep(3 * 1000)
51+
print('checking stats:')
52+
action = stream.fetch_and_reset_stats()
53+
stream.wait(action)
54+
print_stats(action)
55+
56+
print('checking stats after closing stream:')
57+
print_stats(stream)

src/rtmixer.c

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#include <portaudio.h>
77
#include "rtmixer.h"
88

9+
static const struct stats EMPTY_STATS;
10+
911
#ifdef NDEBUG
1012
#define CALLBACK_ASSERT(expr) ((void)(0))
1113
#else
@@ -78,6 +80,27 @@ frame_t seconds2samples(PaTime time, double samplerate)
7880
return (frame_t) llround(time * samplerate);
7981
}
8082

83+
PaTime get_relevant_time(const struct action* action
84+
, const PaStreamCallbackTimeInfo* timeInfo)
85+
{
86+
enum actiontype type = action->type;
87+
if (type == FETCH_AND_RESET_STATS)
88+
{
89+
return timeInfo->currentTime;
90+
}
91+
if (type == CANCEL)
92+
{
93+
CALLBACK_ASSERT(action->action);
94+
type = action->action->type;
95+
}
96+
if (type == PLAY_BUFFER || type == PLAY_RINGBUFFER)
97+
{
98+
return timeInfo->outputBufferDacTime;
99+
}
100+
CALLBACK_ASSERT(type == RECORD_BUFFER || type == RECORD_RINGBUFFER);
101+
return timeInfo->inputBufferAdcTime;
102+
}
103+
81104
int callback(const void* input, void* output, frame_t frameCount
82105
, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags
83106
, void* userData)
@@ -96,16 +119,7 @@ int callback(const void* input, void* output, frame_t frameCount
96119
{
97120
struct action* const action = *actionaddr;
98121

99-
enum actiontype type = action->type;
100-
if (type == CANCEL)
101-
{
102-
CALLBACK_ASSERT(action->action);
103-
type = action->action->type;
104-
}
105-
const bool playing = type == PLAY_BUFFER || type == PLAY_RINGBUFFER;
106-
107-
PaTime io_time = playing ? timeInfo->outputBufferDacTime
108-
: timeInfo->inputBufferAdcTime;
122+
PaTime time = get_relevant_time(action, timeInfo);
109123
frame_t offset = 0;
110124

111125
// Check if the action is due to start in the current block
@@ -114,7 +128,7 @@ int callback(const void* input, void* output, frame_t frameCount
114128
{
115129
// This action has not yet been "active"
116130

117-
PaTime diff = action->requested_time - io_time;
131+
PaTime diff = action->requested_time - time;
118132
if (diff >= 0.0)
119133
{
120134
offset = seconds2samples(diff, state->samplerate);
@@ -129,7 +143,7 @@ int callback(const void* input, void* output, frame_t frameCount
129143
continue;
130144
}
131145
// Re-calculate "diff" to propagate rounding errors
132-
action->actual_time = io_time + (double)offset / state->samplerate;
146+
action->actual_time = time + (double)offset / state->samplerate;
133147
}
134148
else
135149
{
@@ -140,7 +154,7 @@ int callback(const void* input, void* output, frame_t frameCount
140154
remove_action(actionaddr, state);
141155
continue;
142156
}
143-
action->actual_time = io_time;
157+
action->actual_time = time;
144158
}
145159
}
146160

@@ -161,7 +175,7 @@ int callback(const void* input, void* output, frame_t frameCount
161175
// delinquent is not yet playing/recording
162176

163177
frame_t delinquent_offset = 0;
164-
PaTime diff = delinquent->requested_time - io_time;
178+
PaTime diff = delinquent->requested_time - time;
165179
if (diff >= 0.0)
166180
{
167181
delinquent_offset = seconds2samples(diff, state->samplerate);
@@ -217,6 +231,16 @@ int callback(const void* input, void* output, frame_t frameCount
217231
continue;
218232
}
219233

234+
// Handle FETCH_AND_RESET_STATS action
235+
236+
if (action->type == FETCH_AND_RESET_STATS)
237+
{
238+
action->stats = state->stats;
239+
state->stats = EMPTY_STATS;
240+
remove_action(actionaddr, state);
241+
continue;
242+
}
243+
220244
// Store buffer over-/underflow information
221245

222246
get_stats(statusFlags, &(action->stats));
@@ -238,9 +262,17 @@ int callback(const void* input, void* output, frame_t frameCount
238262

239263
// Shove audio data around
240264

241-
float* device_data
242-
= playing ? (float*)output + offset * state->output_channels
243-
: (float*) input + offset * state->input_channels;
265+
float* device_data = NULL;
266+
if (action->type == PLAY_BUFFER || action->type == PLAY_RINGBUFFER)
267+
{
268+
device_data = (float*)output + offset * state->output_channels;
269+
}
270+
else
271+
{
272+
CALLBACK_ASSERT(action->type == RECORD_BUFFER
273+
|| action->type == RECORD_RINGBUFFER);
274+
device_data = (float*) input + offset * state->input_channels;
275+
}
244276

245277
if (action->type == PLAY_BUFFER || action->type == RECORD_BUFFER)
246278
{

src/rtmixer.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ enum actiontype
1717
RECORD_BUFFER,
1818
RECORD_RINGBUFFER,
1919
CANCEL,
20-
// TODO: action to query xrun stats etc.?
20+
FETCH_AND_RESET_STATS,
2121
};
2222

2323
struct stats

src/rtmixer.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ def cancel(self, action, time=0, allow_belated=True):
7979
self._enqueue(cancel_action)
8080
return cancel_action
8181

82+
def fetch_and_reset_stats(self, time=0, allow_belated=True):
83+
"""Fetch and reset over-/underflow statistics of the stream.
84+
85+
"""
86+
action = _ffi.new('struct action*', dict(
87+
type=FETCH_AND_RESET_STATS,
88+
allow_belated=allow_belated,
89+
requested_time=time,
90+
))
91+
self._enqueue(action)
92+
return action
93+
8294
def wait(self, action, sleeptime=10):
8395
"""Wait for *action* to be finished.
8496

0 commit comments

Comments
 (0)