Hi,
the good news first: I've some code which seems to return the "position of the sample that is currently playing through the speakers" that MSDN requires GetPosition to return. It passes my mmdevapi rendering tests when used with ALSA's dmix and hw:0 devices.
The new WinMM over mmdevapi depends on GetPosition for its buffer management. Hence a correct GetPosition seems essential for Wine.
The bad news: I've logs which unambiguously prove that at least in Ubuntu Lucid (2010), PulseAudio yields values of snd_pcm_avail_update and snd_pcm_delay that are completely inadequate to compute what GetPosition should return. I'll write more about that in the ALSA or PA mailing list.
Before doing that, I'd like to sample the behaviour of my rendering tests on a few machines.
You'll find the executable at http://testbot.winehq.org/JobDetails.pl?Key=13489
I'm interested in results from:
- Linux, esp. with something newer than Ubuntu Lucid, or different sound cards.
- Native, because testbot shows some failures and I'd like to see results from real machines, not vmware.
Beware: - Ignore the test failures about IsFormatSupported/Initialize. I'm misusing ok() for reporting.
- Please send the complete log, not the short form with failures only, because the test now loops twice: once for SHARED mode, once for EXCLUSIVE. Without the complete log, it's not possible to tell from the line number only which mode is in use.
- The test may crash in Wine, after my loop, in test_session, because sometimes Initialize fails and the test doesn't catch that.
Testbot shows failures such as: render.c:1034: Test failed: Position 56880 too far after 1070ms render.c:1035: Test failed: Position delta 5712 not regular
I've yet to analyse testbot's logs in detail. - "Position delta" means that after 100ms, one expects the speaker position to advance by ~4800 (or 48000) positions. How come it can be 5712? - "Position too far" means that after 1000ms of playing, the speaker position cannot be larger than FramesPerSecond (modulo Frequency). How can it be 56880 when I expect 48000? (modulo some period increments)
Regards, Jörg Höhle
On 18 August 2011 13:04, Joerg-Cyril.Hoehle@t-systems.com wrote:
I'm interested in results from:
- Linux, esp. with something newer than Ubuntu Lucid, or different sound cards.
NOTE: This is with the alsa and oss drivers selected in the audio tab of winecfg.
$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu oneiric (development branch) Release: 11.10 Codename: oneiric
$ pulseaudio --version pulseaudio 0.9.23
$ cat /proc/asound/version Advanced Linux Sound Architecture Driver Version 1.0.24.
$ uname -r 3.0.0-6-generic
$ ./wine --version wine-1.3.26
$ ./wine /home/reece/Downloads/mmdevapi_test.exe render 2>&1 | tee render.log fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 8 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 8 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 8 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 8 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels render.c:182: Returned periods: 20.0000 ms 10.0000 ms
fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels render.c:194: pwfx: 00130E20
render.c:195: Tag: fffe
render.c:196: bits: 32
render.c:197: chan: 2
render.c:198: rate: 48000
render.c:199: align: 8
render.c:200: extra: 22
render.c:205: Res: 32
render.c:206: Mask: 3
render.c:207: Alg: FLOAT
render.c:276: Returned latency: 50.0000 ms
render.c:350: Test failed: IsFormatSupported(8000x 8x1) call returns 00000000
render.c:351: IsFormatSupported(8000x 8x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(8000x 8x2) call returns 00000000
render.c:351: IsFormatSupported(8000x 8x2) returns 00000000
render.c:350: Test failed: IsFormatSupported(8000x16x1) call returns 00000000
render.c:351: IsFormatSupported(8000x16x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(8000x16x2) call returns 00000000
render.c:351: IsFormatSupported(8000x16x2) returns 00000000
render.c:350: Test failed: IsFormatSupported(11025x 8x1) call returns 00000000
render.c:351: IsFormatSupported(11025x 8x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(11025x 8x2) call returns 00000000
render.c:351: IsFormatSupported(11025x 8x2) returns 00000000
render.c:350: Test failed: IsFormatSupported(11025x16x1) call returns 00000000
render.c:351: IsFormatSupported(11025x16x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(11025x16x2) call returns 00000000
render.c:351: IsFormatSupported(11025x16x2) returns 00000000
render.c:350: Test failed: IsFormatSupported(12000x 8x1) call returns 00000000
render.c:351: IsFormatSupported(12000x 8x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(12000x 8x2) call returns 00000000
render.c:351: IsFormatSupported(12000x 8x2) returns 00000000
render.c:350: Test failed: IsFormatSupported(12000x16x1) call returns 00000000
render.c:351: IsFormatSupported(12000x16x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(12000x16x2) call returns 00000000
render.c:351: IsFormatSupported(12000x16x2) returns 00000000
render.c:350: Test failed: IsFormatSupported(16000x 8x1) call returns 00000000
render.c:351: IsFormatSupported(16000x 8x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(16000x 8x2) call returns 00000000
render.c:351: IsFormatSupported(16000x 8x2) returns 00000000
render.c:350: Test failed: IsFormatSupported(16000x16x1) call returns 00000000
render.c:351: IsFormatSupported(16000x16x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(16000x16x2) call returns 00000000
render.c:351: IsFormatSupported(16000x16x2) returns 00000000
render.c:350: Test failed: IsFormatSupported(22050x 8x1) call returns 00000000
render.c:351: IsFormatSupported(22050x 8x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(22050x 8x2) call returns 00000000
render.c:351: IsFormatSupported(22050x 8x2) returns 00000000
render.c:350: Test failed: IsFormatSupported(22050x16x1) call returns 00000000
render.c:351: IsFormatSupported(22050x16x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(22050x16x2) call returns 00000000
render.c:351: IsFormatSupported(22050x16x2) returns 00000000
render.c:350: Test failed: IsFormatSupported(44100x 8x1) call returns 00000000
render.c:351: IsFormatSupported(44100x 8x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(44100x 8x2) call returns 00000000
render.c:351: IsFormatSupported(44100x 8x2) returns 00000000
render.c:350: Test failed: IsFormatSupported(44100x16x1) call returns 00000000
render.c:351: IsFormatSupported(44100x16x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(44100x16x2) call returns 00000000
render.c:351: IsFormatSupported(44100x16x2) returns 00000000
render.c:350: Test failed: IsFormatSupported(48000x 8x1) call returns 00000000
render.c:351: IsFormatSupported(48000x 8x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(48000x 8x2) call returns 00000000
render.c:351: IsFormatSupported(48000x 8x2) returns 00000000
render.c:350: Test failed: IsFormatSupported(48000x16x1) call returns 00000000
render.c:351: IsFormatSupported(48000x16x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(48000x16x2) call returns 00000000
render.c:351: IsFormatSupported(48000x16x2) returns 00000000
render.c:350: Test failed: IsFormatSupported(96000x 8x1) call returns 00000000
render.c:351: IsFormatSupported(96000x 8x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(96000x 8x2) call returns 00000000
render.c:351: IsFormatSupported(96000x 8x2) returns 00000000
render.c:350: Test failed: IsFormatSupported(96000x16x1) call returns 00000000
render.c:351: IsFormatSupported(96000x16x1) returns 00000000
render.c:350: Test failed: IsFormatSupported(96000x16x2) call returns 00000000
render.c:351: IsFormatSupported(96000x16x2) returns 00000000
fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels render.c:564: Test failed: Wait(event) after Stop gave 102
render.c:567: Test failed: Wait(event) after Stop #2 gave 102
fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels render.c:629: Test failed: GetBuffer 0 gave 00134A30
render.c:633: Test failed: Reset failed: 00000000
render.c:665: Test failed: NULL expected FFFFFFFFDEADF00D
render.c:682: Test failed: GetBuffer 0 gave FFFFFFFFDEADF00D
render.c:687: Test failed: GetBuffer 0 size #2 failed: 88890007
render.c:688: Test failed: GetBuffer 0 #2 gave FFFFFFFFDEADF00D
render.c:695: Test failed: GetBuffer failed: 88890007
fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels render.c:767: Testing shared mode
render.c:789: Latency: 50.0000 ms
render.c:797: BufferSize 24000 frames
render.c:807: Clock Frequency 48000
render.c:811: Test failed: Clock Frequency 48000
render.c:836: data at 00134A30
render.c:872: Test failed: Position 24000 too far after 100ms
render.c:882: padding 0 past sleep #2
render.c:889: padding 0 past stop #1
render.c:894: Test failed: Position should have been further along...
render.c:896: position 24000
render.c:897: Test failed: Position 24000 too far after 200ms
render.c:922: data at 00134A30
render.c:947: position 5577
render.c:948: Test failed: Position 5577 too far after 100ms
render.c:958: padding 0 past stop #2
render.c:964: position 24000
render.c:966: Test failed: Position 24000 too far after 100ms
render.c:986: data at 00134A30 for prefill 22500
render.c:996: Test failed: Reset failed: 00000000
render.c:1015: hpctime 374 after 370ms
render.c:1033: padding 0 position 22500 slept 470ms iteration 0
render.c:1035: Test failed: Position delta 6126 not regular
render.c:1040: hpctime 475 pcpos 475
render.c:1060: data at 00134A30
err:alsa:wine_snd_pcm_recover underrun occurred render.c:1033: padding 16888 position 28112 slept 570ms iteration 1
render.c:1035: Test failed: Position delta 5612 not regular
render.c:1040: hpctime 578 pcpos 578
render.c:1065: data at 00134A30 (small 7112)
render.c:1033: padding 19159 position 32953 slept 670ms iteration 2
render.c:1040: hpctime 679 pcpos 679
render.c:1065: data at 00134A30 (small 4841)
render.c:1033: padding 18218 position 38735 slept 770ms iteration 3
render.c:1035: Test failed: Position delta 5782 not regular
render.c:1040: hpctime 779 pcpos 779
render.c:1065: data at 00134A30 (small 5782)
render.c:1033: padding 21100 position 41635 slept 870ms iteration 4
render.c:1035: Test failed: Position delta 2900 not regular
render.c:1040: hpctime 880 pcpos 880
render.c:1065: data at 00134A30 (small 2900)
render.c:1033: padding 18210 position 47425 slept 970ms iteration 5
render.c:1035: Test failed: Position delta 5790 not regular
render.c:1040: hpctime 980 pcpos 980
render.c:1065: data at 00134A30 (small 5790)
render.c:1033: padding 18218 position 53207 slept 1070ms iteration 6
render.c:1035: Test failed: Position delta 5782 not regular
render.c:1040: hpctime 1081 pcpos 1081
render.c:1065: data at 00134A30 (small 5782)
render.c:1033: padding 21144 position 56063 slept 1170ms iteration 7
render.c:1035: Test failed: Position delta 2856 not regular
render.c:1040: hpctime 1181 pcpos 1181
render.c:1065: data at 00134A30 (small 2856)
render.c:1033: padding 18201 position 61862 slept 1270ms iteration 8
render.c:1035: Test failed: Position delta 5799 not regular
render.c:1040: hpctime 1282 pcpos 1282
render.c:1065: data at 00134A30 (small 5799)
render.c:1080: position 61862
render.c:1090: position 85862 past underrun
render.c:1100: hpctime 2283 after underrun
fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels render.c:778: Testing exclusive mode at 48000
render.c:789: Latency: 50.0000 ms
render.c:793: Test failed: Latency not twice period, delta 35000us
render.c:797: BufferSize 24000 frames
render.c:807: Clock Frequency 48000
render.c:836: data at 00134A30
render.c:872: Test failed: Position 24000 too far after 100ms
render.c:882: padding 0 past sleep #2
render.c:889: padding 0 past stop #1
render.c:894: Test failed: Position should have been further along...
render.c:896: position 24000
render.c:897: Test failed: Position 24000 too far after 200ms
render.c:922: data at 00134A30
render.c:947: position 5603
render.c:948: Test failed: Position 5603 too far after 100ms
render.c:958: padding 0 past stop #2
render.c:964: position 24000
render.c:966: Test failed: Position 24000 too far after 100ms
render.c:986: data at 00134A30 for prefill 22500
render.c:996: Test failed: Reset failed: 00000000
render.c:1015: hpctime 375 after 370ms
render.c:1033: padding 0 position 22500 slept 470ms iteration 0
render.c:1040: hpctime 476 pcpos 476
render.c:1060: data at 00134A30
err:alsa:wine_snd_pcm_recover underrun occurred render.c:1033: padding 16897 position 28103 slept 570ms iteration 1
render.c:1035: Test failed: Position delta 5603 not regular
render.c:1040: hpctime 579 pcpos 579
render.c:1065: data at 00134A30 (small 7103)
render.c:1033: padding 18201 position 33902 slept 670ms iteration 2
render.c:1035: Test failed: Position delta 5799 not regular
render.c:1040: hpctime 679 pcpos 680
render.c:1065: data at 00134A30 (small 5799)
render.c:1033: padding 21100 position 36802 slept 770ms iteration 3
render.c:1035: Test failed: Position delta 2900 not regular
render.c:1040: hpctime 780 pcpos 780
render.c:1065: data at 00134A30 (small 2900)
render.c:1033: padding 18210 position 42592 slept 870ms iteration 4
render.c:1035: Test failed: Position delta 5790 not regular
render.c:1040: hpctime 880 pcpos 880
render.c:1065: data at 00134A30 (small 5790)
render.c:1033: padding 19176 position 47416 slept 970ms iteration 5
render.c:1040: hpctime 981 pcpos 981
render.c:1065: data at 00134A30 (small 4824)
render.c:1033: padding 18209 position 53207 slept 1070ms iteration 6
render.c:1035: Test failed: Position delta 5791 not regular
render.c:1040: hpctime 1081 pcpos 1081
render.c:1065: data at 00134A30 (small 5791)
render.c:1033: padding 21109 position 56098 slept 1170ms iteration 7
render.c:1035: Test failed: Position delta 2891 not regular
render.c:1040: hpctime 1182 pcpos 1182
render.c:1065: data at 00134A30 (small 2891)
render.c:1033: padding 18210 position 61888 slept 1270ms iteration 8
render.c:1035: Test failed: Position delta 5790 not regular
render.c:1040: hpctime 1282 pcpos 1282
render.c:1065: data at 00134A30 (small 5790)
render.c:1080: position 61888
render.c:1090: position 85888 past underrun
render.c:1100: hpctime 2283 after underrun
fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetService stub {bfb7ff88-7239-4fc9-8fa2-07c950be9c6d} fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels fixme:alsa:AudioClient_GetMixFormat Don't know what to do with 32 channels, pretending there's only 2 channels render: 842 tests executed (0 marked as todo, 70 failures), 0 skipped.
- Reece
Reece,
I wrote the text in a hurry and forgot that Wine results are most interesting to me with my patch applied.
Please use my patch with something like: WINETEST_DEBUG=3 WINEDEBUG=warn+alsa wine mmdevapi_test.exe render
render.c:897: Test failed: Position 24000 too far after 200ms
That's not PA's fault. IMHO AudioClient_Stop must not map to snd_pcm_drop. It is more like snd_pcm_pause. Or perhaps simply lead ALSA into an underrun. I've not made up my mind yet as the models (mmdevapi vs. ALSA) are different w.r.t. buffering.
As you can see, the patch is nowhere final.
#From 60689763bd21513bd9b8dbd2df3abc5f2586f1f2 Mon Sep 17 00:00:00 2001 #From: =?UTF-8?q?J=C3=B6rg=20H=C3=B6hle?= hoehle@users.sourceforge.net Date: Wed, 17 Aug 2011 21:04:34 +0200 Subject: winealsa: Play with GetPosition.
--- dlls/winealsa.drv/mmdevdrv.c | 52 ++++++++++++++++++++++++++++++++++------- 1 files changed, 43 insertions(+), 9 deletions(-)
diff --git a/dlls/winealsa.drv/mmdevdrv.c b/dlls/winealsa.drv/mmdevdrv.c index 3e3edc3..51e9b81 100644 --- a/dlls/winealsa.drv/mmdevdrv.c +++ b/dlls/winealsa.drv/mmdevdrv.c @@ -2289,26 +2289,60 @@ static HRESULT WINAPI AudioClock_GetPosition(IAudioClock *iface, UINT64 *pos, UINT64 *qpctime) { ACImpl *This = impl_from_IAudioClock(iface); - UINT32 pad; - HRESULT hr; + int err; + snd_pcm_uframes_t avail_frames; + snd_pcm_sframes_t delay_frames, pad_frames; + snd_pcm_status_t *status;
TRACE("(%p)->(%p, %p)\n", This, pos, qpctime);
if(!pos) return E_POINTER; + snd_pcm_status_alloca(&status);
EnterCriticalSection(&This->lock);
- hr = IAudioClient_GetCurrentPadding(&This->IAudioClient_iface, &pad); - if(FAILED(hr)){ + if(!This->initted){ LeaveCriticalSection(&This->lock); - return hr; + return AUDCLNT_E_NOT_INITIALIZED; }
- if(This->dataflow == eRender) - *pos = This->written_frames - pad; - else if(This->dataflow == eCapture) - *pos = This->written_frames + pad; + if((err = snd_pcm_status(This->pcm_handle, status)) < 0){ + LeaveCriticalSection(&This->lock); + ERR("ALSA status error: %d (%s)\n", + err, snd_strerror(err)); + return E_FAIL; + } + if(0){ + avail_frames = snd_pcm_status_get_avail(status); + delay_frames = snd_pcm_status_get_delay(status); + }else{ + avail_frames = snd_pcm_avail_update(This->pcm_handle); + err = snd_pcm_delay(This->pcm_handle, &delay_frames); + if(err < 0){ /* e.g. in STATE_PREPARED */ + ERR("ALSA delay error: %d (%s)\n", + err, snd_strerror(err)); + delay_frames = 0; + } + } + pad_frames = This->bufsize_alsa - avail_frames; +#define MAX_LATE_SECONDS 5 /* huge USB or network latency */ + if(avail_frames <= This->bufsize_alsa + MAX_LATE_SECONDS * This->fmt->nSamplesPerSec + && delay_frames > 0) + *pos = This->written_frames - This->held_frames - delay_frames; + else if(pad_frames > 0) + /* delay may be slightly < 0 past reset */ + *pos = This->written_frames - This->held_frames - pad_frames; + else + *pos = This->written_frames - This->held_frames; + /* FIXME: if(This->dataflow == eCapture) */ + + ERR("avail %lu, delay %ld, sum %ld, alsa %lu, written %lu, held %u: %lu\n", + avail_frames, delay_frames, avail_frames+delay_frames, This->bufsize_alsa, (ulong)This->written_frames, This->held_frames, (ulong)*pos); + avail_frames = snd_pcm_avail_update(This->pcm_handle); + err = snd_pcm_delay(This->pcm_handle, &delay_frames); + ERR("avail %lu, delay %ld, sum %ld, alsa %lu, written %lu, held %u: %lu\n", + avail_frames, delay_frames, avail_frames+delay_frames, This->bufsize_alsa, (ulong)This->written_frames, This->held_frames, (ulong)*pos);
LeaveCriticalSection(&This->lock);
On 08/19/2011 01:30 PM, Joerg-Cyril.Hoehle@t-systems.com wrote:
Reece,
I wrote the text in a hurry and forgot that Wine results are most interesting to me with my patch applied.
Please use my patch with something like: WINETEST_DEBUG=3 WINEDEBUG=warn+alsa wine mmdevapi_test.exe render
render.c:897: Test failed: Position 24000 too far after 200ms
That's not PA's fault. IMHO AudioClient_Stop must not map to snd_pcm_drop. It is more like snd_pcm_pause. Or perhaps simply lead ALSA into an underrun. I've not made up my mind yet as the models (mmdevapi vs. ALSA) are different w.r.t. buffering.
afaict pause, with reset mapped to drop, but for historic reasons that didn't work (surprise! love from dmix). Even worse, snd_pcm_drain would deadlock if called twice.
I can't remember why pause didn't work, but if it works go for it.
As you can see, the patch is nowhere final.
#From 60689763bd21513bd9b8dbd2df3abc5f2586f1f2 Mon Sep 17 00:00:00 2001 #From: =?UTF-8?q?J=C3=B6rg=20H=C3=B6hle?= hoehle@users.sourceforge.net Date: Wed, 17 Aug 2011 21:04:34 +0200 Subject: winealsa: Play with GetPosition.
dlls/winealsa.drv/mmdevdrv.c | 52 ++++++++++++++++++++++++++++++++++------- 1 files changed, 43 insertions(+), 9 deletions(-)
diff --git a/dlls/winealsa.drv/mmdevdrv.c b/dlls/winealsa.drv/mmdevdrv.c index 3e3edc3..51e9b81 100644 --- a/dlls/winealsa.drv/mmdevdrv.c +++ b/dlls/winealsa.drv/mmdevdrv.c @@ -2289,26 +2289,60 @@ static HRESULT WINAPI AudioClock_GetPosition(IAudioClock *iface, UINT64 *pos, UINT64 *qpctime) { ACImpl *This = impl_from_IAudioClock(iface);
- UINT32 pad;
- HRESULT hr;
int err;
snd_pcm_uframes_t avail_frames;
snd_pcm_sframes_t delay_frames, pad_frames;
snd_pcm_status_t *status;
TRACE("(%p)->(%p, %p)\n", This, pos, qpctime);
if(!pos) return E_POINTER;
snd_pcm_status_alloca(&status);
HeapAlloc(GetProcessHeap(), HEAP_ZERO_FLAG, snd_pcm_status_sizeof()) or something like that if available please..
EnterCriticalSection(&This->lock);
- hr = IAudioClient_GetCurrentPadding(&This->IAudioClient_iface, &pad);
- if(FAILED(hr)){
- if(!This->initted){ LeaveCriticalSection(&This->lock);
return hr;
}return AUDCLNT_E_NOT_INITIALIZED;
Unneeded part. Follow that flow..
- if(This->dataflow == eRender)
*pos = This->written_frames - pad;
- else if(This->dataflow == eCapture)
*pos = This->written_frames + pad;
- if((err = snd_pcm_status(This->pcm_handle, status)) < 0){
LeaveCriticalSection(&This->lock);
ERR("ALSA status error: %d (%s)\n",
err, snd_strerror(err));
return E_FAIL;
- }
- if(0){
- avail_frames = snd_pcm_status_get_avail(status);
- delay_frames = snd_pcm_status_get_delay(status);
- }else{
- avail_frames = snd_pcm_avail_update(This->pcm_handle);
- err = snd_pcm_delay(This->pcm_handle, &delay_frames);
- if(err < 0){ /* e.g. in STATE_PREPARED */
ERR("ALSA delay error: %d (%s)\n",
err, snd_strerror(err));
delay_frames = 0;
- }
- }
if 0 is bad...
- pad_frames = This->bufsize_alsa - avail_frames;
+#define MAX_LATE_SECONDS 5 /* huge USB or network latency */
- if(avail_frames <= This->bufsize_alsa + MAX_LATE_SECONDS * This->fmt->nSamplesPerSec
&& delay_frames > 0)
*pos = This->written_frames - This->held_frames - delay_frames;
Isn't delay_frames < 0 the definition of underrun? no point in adding MAX_LATE_SECONDS
- else if(pad_frames > 0)
/* delay may be slightly < 0 past reset */
*pos = This->written_frames - This->held_frames - pad_frames;
- else
*pos = This->written_frames - This->held_frames;
- /* FIXME: if(This->dataflow == eCapture) */
ERR("avail %lu, delay %ld, sum %ld, alsa %lu, written %lu, held %u: %lu\n",
avail_frames, delay_frames, avail_frames+delay_frames, This->bufsize_alsa, (ulong)This->written_frames, This->held_frames, (ulong)*pos);
avail_frames = snd_pcm_avail_update(This->pcm_handle);
err = snd_pcm_delay(This->pcm_handle, &delay_frames);
ERR("avail %lu, delay %ld, sum %ld, alsa %lu, written %lu, held %u: %lu\n",
avail_frames, delay_frames, avail_frames+delay_frames, This->bufsize_alsa, (ulong)This->written_frames, This->held_frames, (ulong)*pos);
LeaveCriticalSection(&This->lock);
Getting an avail update again? Why?
Maarten Lankhorst wrote: [nice to hear from you]
IMHO AudioClient_Stop must not map to snd_pcm_drop. It is more like snd_pcm_pause. Or perhaps simply lead ALSA into an underrun.
afaict pause, with reset mapped to drop,
Indeed. But I believe I need a fallback because ALSA says that pause "works only on the hardware which supports" it.
I can't remember why pause didn't work, but if it works go for it.
I was solely thinking aloud that pause is TRT, not tried out yet.
However, I received test results from a "Windows 7 Ultimate" machine. It exhibits a similar bug -- in exclusive mode only: render.c:948: Test failed: Position 18191 too far after 100ms Shared mode works as my tests expect it (<= 48000/10 frames).
- snd_pcm_status_alloca(&status);
HeapAlloc(GetProcessHeap(), HEAP_ZERO_FLAG, snd_pcm_status_sizeof()) or something like that if available please..
Really? I don't want to go through the overhead of memory allocation when all I need is a stupid small amount of stack allocated memory.
- if(!This->initted){
return AUDCLNT_E_NOT_INITIALIZED;
Unneeded part.
Can't you obtain a handle to that COM object prior to calling Initialize which sets This->fmt?
Follow that flow..
I beg your pardon?
- if(0){
- avail_frames = snd_pcm_status_get_avail(status);
- delay_frames = snd_pcm_status_get_delay(status);
if 0 is bad...
I tried out pcm_status because somebody in alsa-devel mentioned that it allows to grab avail + delay in one (sync'ed?). However, I found delay to be always 0 inside status!?!
Also, I found out that I need to call avail_update and delay in a particular order, otherwise I get stale values from an old call prior to the last sleep...
- if(avail_frames <= This->bufsize_alsa + MAX_LATE_SECONDS * This->fmt->nSamplesPerSec
&& delay_frames > 0)
Isn't delay_frames < 0 the definition of underrun?
Indeed. There are potentially N distinct underruns: - the front end -- what snd_pcm_avail_update knows about; - intermittent buffers (USB); - the speaker -- what snd_pcm_delay knows about. There could be a short front-end buffer underrun that goes unnoticed by the speaker if the TCP or USB in between buffers enough data *and* is able to speed up.
no point in adding MAX_LATE_SECONDS
That is some form of guard against broken values. E.g. people reporting in alsa-devel that PA sometimes complains about avail ~ MAXINT and such weird values.
Getting an avail update again? Why?
The theory is: position = written_frames(into ALSA) - delay and translates to: This->written_frames - This->held_frames - delay
However sometimes I can't trust delay. I still need to figure out when. - IIRC after an underrun, snd_pcm_delay yields error X. - or was it before starting? - ...
The upper bound on position is always: This->written_frames(ReleaseBuffer) - This->held_frames - ALSA_padding (what ALSA's front end has not yet processed, in absence of underrun).
Perhaps that would be robust: 1. Compute upper bound 2. position = clamp(0, delay > 0 ? written-delay : written, upper_bound); 2b. except when not yet started ... 2c. except while stopped ...
I was even considering: 3. if position < This->previous_position stick to previous...
Yet perhaps it's better to allow intermittent garbage values than to stick to garbage!
OTOH, the delay values I see in the logs are subject to such variation (with PA) that I'm considering going with a clock instead, or perhaps: - query delay once per tick (e.g. 10ms) => last_pos - when asked, compute position from last_pos + time since tick * rate
The last_pos slot may be needed anyway once stopped in pause mode.
Regards, Jörg Höhle
On 08/19/2011 05:10 PM, Joerg-Cyril.Hoehle@t-systems.com wrote:
Maarten Lankhorst wrote: [nice to hear from you]
IMHO AudioClient_Stop must not map to snd_pcm_drop. It is more like snd_pcm_pause. Or perhaps simply lead ALSA into an underrun.
afaict pause, with reset mapped to drop,
Indeed. But I believe I need a fallback because ALSA says that pause "works only on the hardware which supports" it.
I can't remember why pause didn't work, but if it works go for it.
I was solely thinking aloud that pause is TRT, not tried out yet.
However, I received test results from a "Windows 7 Ultimate" machine. It exhibits a similar bug -- in exclusive mode only: render.c:948: Test failed: Position 18191 too far after 100ms Shared mode works as my tests expect it (<= 48000/10 frames).
- snd_pcm_status_alloca(&status);
HeapAlloc(GetProcessHeap(), HEAP_ZERO_FLAG, snd_pcm_status_sizeof()) or something like that if available please..
Really? I don't want to go through the overhead of memory allocation when all I need is a stupid small amount of stack allocated memory.
- if(!This->initted){
return AUDCLNT_E_NOT_INITIALIZED;
Unneeded part.
Can't you obtain a handle to that COM object prior to calling Initialize which sets This->fmt?
No.
Follow that flow..
I beg your pardon?
IAudioClock is not available until Initialize has succeeded, so the check above is pointless.
- if(0){
- avail_frames = snd_pcm_status_get_avail(status);
- delay_frames = snd_pcm_status_get_delay(status);
if 0 is bad...
I tried out pcm_status because somebody in alsa-devel mentioned that it allows to grab avail + delay in one (sync'ed?). However, I found delay to be always 0 inside status!?!
Also, I found out that I need to call avail_update and delay in a particular order, otherwise I get stale values from an old call prior to the last sleep...
- if(avail_frames <= This->bufsize_alsa + MAX_LATE_SECONDS * This->fmt->nSamplesPerSec
&& delay_frames > 0)
Isn't delay_frames < 0 the definition of underrun?
Indeed. There are potentially N distinct underruns:
- the front end -- what snd_pcm_avail_update knows about;
- intermittent buffers (USB);
- the speaker -- what snd_pcm_delay knows about.
There could be a short front-end buffer underrun that goes unnoticed by the speaker if the TCP or USB in between buffers enough data *and* is able to speed up.
no point in adding MAX_LATE_SECONDS
That is some form of guard against broken values. E.g. people reporting in alsa-devel that PA sometimes complains about avail ~ MAXINT and such weird values.
Getting an avail update again? Why?
The theory is: position = written_frames(into ALSA) - delay and translates to: This->written_frames - This->held_frames - delay
However sometimes I can't trust delay. I still need to figure out when.
- IIRC after an underrun, snd_pcm_delay yields error X.
- or was it before starting?
- ...
The upper bound on position is always: This->written_frames(ReleaseBuffer) - This->held_frames - ALSA_padding (what ALSA's front end has not yet processed, in absence of underrun).
Perhaps that would be robust:
- Compute upper bound
- position = clamp(0, delay > 0 ? written-delay : written, upper_bound);
2b. except when not yet started ... 2c. except while stopped ...
I was even considering: 3. if position < This->previous_position stick to previous...
Yet perhaps it's better to allow intermittent garbage values than to stick to garbage!
OTOH, the delay values I see in the logs are subject to such variation (with PA) that I'm considering going with a clock instead, or perhaps:
- query delay once per tick (e.g. 10ms) => last_pos
- when asked, compute position from last_pos + time since tick * rate
The last_pos slot may be needed anyway once stopped in pause mode.
Don't worry about underruns and checking the way you do is fragile, I prefer 0 tracking, and just check the return value of snd_pcm_writei. Experience taught me this is the most stable way of doing underrun handling in alsa. Remove all the checks you're doing please, they will just break things more.
Hi,
here's just for fun an interpretation of some test data:
A native machine passes all my tests in shared mode.
In exclusive mode, that machine behaves like PulseAudio in Wine now :-) render.c:797: BufferSize 21846 frames render.c:799: Test failed: BufferSize 21846 too small for duration at rate 44100 render.c:807: Clock Frequency 44100 render.c:872: Test failed: Position 21829 too far after 100ms render.c:882: padding 0 past sleep #2
It ate 495ms worth of samples (the completely prefilled buffer - mysterious 17) within 100ms after Start then returned a local underrun (padding 0)! Clearly a bug in MS-Windows or some driver.
If you start with an underrun like my loop does, then that machine returns data that appears valid: render.c:1033: padding 1324 position 19648 slept 470ms iteration 0
Testbot shows how MS "fixed" a bug between Vista and w2k8R2/w7 in exclusive mode: render.c:1086: Test failed: GetCurrentPadding returned 576, should be 0 render.c:1092: Test failed: Position 90720 at end vs. 91296 played frames Somewhat I found the old behavior more consistent. After all, 90720 + 576 = 91296
What's noteworthy is that only a multiple of period frames appears to get written. Unlike shared mode, the remainder will be left in the buffer, even if you wait for an underrun to occur (like my tests do).
IOW, native players using exclusive mode are well-advised to add a few silence frames at the end of the samples, or they won't be heard. At least, that's what I infer from render.c:1092: Test failed: Position 90720 at end vs. 91296 played frames since I've never put my hands on a native system new enough to have mmdevapi.
Testbot confirms again that you must not trust timers in vmware, which I already experienced with the MCI tests. render.c:1040: hpctime 828 pcpos 843 render.c:1033: padding 16560 position 50832 slept 870ms iteration 4 render.c:1034: Test failed: Position 50832 too far after 870ms render.c:1035: Test failed: Position delta 7392 not regular render.c:1040: hpctime 937 pcpos 952
Within 109ms its speaker position advanced by 7392 frames, whereas 5232 would be normal at 48000 in that time frame.
Other than that, data looks good.
It will be no fun to correct the tests to accept testbot's daily results on test.winehq. Currently I lean towards if (winetest_debug > 0) position-tests such that testbot's patch watcher skips over position-based tests which we would know pass on native.
Regards, Jörg Höhle
Hi,
IMHO AudioClient_Stop must not map to snd_pcm_drop. It is more like snd_pcm_pause. Or perhaps simply lead ALSA into an underrun.
afaict pause, with reset mapped to drop,
Indeed.
IMHO, the confusion around which of pause/reset/drop/drain applies arises because Wine does not behave like mmdevapi closely enough.
All my tests converge on showing that native's processing occurs on period ticks only(?/mostly) and that it sends data to the underlying HW devices in chunks of period size frames. In particular, it does not send megabytes of data down the pipe in advance.
If Wine were to write no more than a period worth of data to ALSA/OSS/CoreAudio, then IAudioClient_Stop could do exactly what MSDN says: "This method stops data from streaming through the client's connection with the audio engine." IOW, just let the underlying device enter an underrun, which the code must handle anyway. There would be no need for any of snd_pcm_pause/drop/drain!
The need for pause etc. arises in Wine because it may feed a whole buffer (200 times! period size, e.g. 2s vs 10ms) to ALSA/OSS/CoreAudio. Ensues complexity and deviant behaviour as snd_pcm_drop is not applicable since Stop is defined *not* to throw these 2s of data away, snd_pcm_drain is not callable in non-blocking mode, snd_pcm_pause may not be supported, etc.
OTOH, I recognize that sending 2s of data down the UNIX pipe lets one sleep well and without glitches.
The question becomes: can Wine act like native's mmdevapi? Can it be guaranteed sustained rates of wake-ups every 10ms? Was too tight timing a known cause of glitches in the past such that we're very happy that mmdevapi allows a much larger buffer than period size? Is 100 wake-ups per second too much for a simple winmm:PlaySound on a laptop runing on batteries?
Note that no game will probably use 2s buffers except for BGM. It will expect tight timing and little latency (I'd say < 80ms, e.g. 2 video frames at 24fps).
On native, the question does not arise: mmdevapi is the low-level interface to audio, part of the OS. The OS will make tremendous efforts such as drive its audio engine(s) at the highest priority ("Audio Pro") (+ a couple other measures out of topic) so as to guarantee a wake-up every 10ms.
Benefits I see in equating mmdevapi periods with ALSA/OSS periods: - Get as close to ALSA/OSS recommended programming practices as possible, e.g. may even end up using a) blocking mode in the player; b) snd_pcm_wait(period) to sleep for the next relevant event instead of using a timer unrelated to actual audio throughput. - Hopefully get low response times from Linux as sleeping on an event then doing little processing fast is exactly what the kernel people recommend for threads to be scheduled timely.
Benefits I see in throwing large data in advance at ALSA/OSS: - Don't worry about glitches, let others care. - Don't care about 10ms periods for BGM, PlaySound or MCIWAVE.
Regards, Jörg Höhle
Hi,
These exclusive mode test failures look like nothing: render.c:1086: Test failed: GetCurrentPadding returned 576, should be 0 render.c:1092: Test failed: Position 90720 at end vs. 91296 played frames but they made me completely rework my model of how mmdevapi works.
91296 = 90720 + 576 remainder 90720 = 720 * 126 720 = 15ms period at 48000 frames/s
It looks like only a multiple of period_size frames are rendered. There are also test runs which end with GCP > period_size despite underrun
Only Vista and w2k8 present these 2 errors. w2k8R2 and w7 only present the second one. In shared mode, GetCurrentPadding (GCP) returns 0 and GetPosition returns all submitted ("played") frames. What happened?
Possible explanations: Vista + w2k8 exclusive mode: a) Bug in native that forgets to account for sub-period_size data. Or b) Play only multiples of period size. Wait until period_size data accumulates before sending the next packet. c) other...
w2k8R2 + w7 exclusive mode: a) Bug in native that sets GCP to 0 (like in shared mode). Or b) At each period tick, only play data if a full period is available. Drop partially filled buffers (explains padding = 0) instead of playing them much later when the buffer fills up (no ghost sounds). c) Bug in native that forgets to increment Position yet plays data. d) other...
shared mode: a) At each 10ms period tick, mix a period_size of all streams that provided a full period of data. Ignore other streams yet always decrement GetCurrentPadding and sum played/ignored frames. b) At each 10ms period tick, mix all streams. Handle streams with padding < period_size as follows: - if previous frame was full, mix at beginning of buffer (trailer) - otherwise mix at end (expect beginning of next sound) c) At each 10ms period tick, mix all streams, padding streams with less than period_size frames with silence (i.e. mix at beginning). d) other... If I were MS, I'd implement heuristic b).
This is all unlike what Wine does...
I'll probably write audible audio tests in the next few weeks to be able to test what native does (e.g. emit a short beep < period size every few ms and listen to what happens).
Regards, Jörg Höhle
On 08/22/2011 02:13 PM, Joerg-Cyril.Hoehle@t-systems.com wrote:
Hi,
These exclusive mode test failures look like nothing: render.c:1086: Test failed: GetCurrentPadding returned 576, should be 0 render.c:1092: Test failed: Position 90720 at end vs. 91296 played frames but they made me completely rework my model of how mmdevapi works.
91296 = 90720 + 576 remainder 90720 = 720 * 126 720 = 15ms period at 48000 frames/s
It looks like only a multiple of period_size frames are rendered. There are also test runs which end with GCP > period_size despite underrun
Only Vista and w2k8 present these 2 errors. w2k8R2 and w7 only present the second one. In shared mode, GetCurrentPadding (GCP) returns 0 and GetPosition returns all submitted ("played") frames. What happened?
Possible explanations: Vista + w2k8 exclusive mode: a) Bug in native that forgets to account for sub-period_size data. Or b) Play only multiples of period size. Wait until period_size data accumulates before sending the next packet.
Have to go with this one, exclusive mode wants you to fill the entire buffer at the same time..
c) other...
w2k8R2 + w7 exclusive mode: a) Bug in native that sets GCP to 0 (like in shared mode). Or b) At each period tick, only play data if a full period is available. Drop partially filled buffers (explains padding = 0) instead of playing them much later when the buffer fills up (no ghost sounds). c) Bug in native that forgets to increment Position yet plays data. d) other...
If I look at msdn, exclusive mode wants you to fill the entire buffer in 1 go, doing partial data doesn't make sense, and it wouldn't surprise me if that's why it drops things..
shared mode: a) At each 10ms period tick, mix a period_size of all streams that provided a full period of data. Ignore other streams yet always decrement GetCurrentPadding and sum played/ignored frames. b) At each 10ms period tick, mix all streams. Handle streams with padding < period_size as follows:
- if previous frame was full, mix at beginning of buffer (trailer)
- otherwise mix at end (expect beginning of next sound)
c) At each 10ms period tick, mix all streams, padding streams with less than period_size frames with silence (i.e. mix at beginning). d) other... If I were MS, I'd implement heuristic b).
This is all unlike what Wine does...
I'm inclined to believe it's C
I'll probably write audible audio tests in the next few weeks to be able to test what native does (e.g. emit a short beep < period size every few ms and listen to what happens).
Regards, Jörg Höhle
Hi,
Maarten Lankhorst wrote:
exclusive mode wants you to fill the entire buffer at the same time.
You're confusing this with exclusive + EVENTCALLBACK mode, which I'm not using. There are 4 combinations of shared/exclusive and EVENTCALLBACK or not. Only one of the 4 needs no GetCurrentPadding and uses a full buffer each time.
So far I've only been testing without EVENTCALLBACK (though test_event contains some non-rendering code using it).
it wouldn't surprise me if that's why it drops things..
I'd expect GetBuffer to return AUDCLNT_E_BUFFER_SIZE_ERROR as documented for that case.
Last week I wrote:
Somewhat I found the old behavior more consistent.
Upon reflection, the new behavior is better. It makes more sense to drop old frames than to keep them around thus play ghost sounds from the past.
However, I'd even more prefer shared and exclusive mode to behave the same *and* not drop frames. I believe shared mode does not drop partial fills (based solely on what GetPosition returns) and would have expected exclusive mode to do that too. I don't know why MS decided to do that but I now see why they recommend that programs add silence at the end of their sound: it works around that odd behaviour.
Maarten also wrote:
I prefer 0 tracking, and just check the return value of snd_pcm_writei.
I can understand that but then we need to throw rate-limiting into the discussion. We are talking about GetPosition here; computing GetPosition solely on the base of snd_pcm_writei (let's throw in avail_update without delay) gives us already known bugs, e.g. PulseAudio backend audio 2s off video sync.
And without snd_pcm_delay, the sole culprit would be: Wine. If, OTOH, we use it, we can hope other projects will fix their bugs.
Regards, Jörg Höhle
On 08/22/2011 05:31 PM, Joerg-Cyril.Hoehle@t-systems.com wrote:
Hi,
Maarten Lankhorst wrote:
exclusive mode wants you to fill the entire buffer at the same time.
You're confusing this with exclusive + EVENTCALLBACK mode, which I'm not using. There are 4 combinations of shared/exclusive and EVENTCALLBACK or not. Only one of the 4 needs no GetCurrentPadding and uses a full buffer each time.
So far I've only been testing without EVENTCALLBACK (though test_event contains some non-rendering code using it).
http://msdn.microsoft.com/en-us/library/dd370844%28v=vs.85%29.aspx
I meant an entire period then, this describes how windows handles it, my guess is that if you don't fill a period, you get an underrun, no matter how you handle it.
it wouldn't surprise me if that's why it drops things..
I'd expect GetBuffer to return AUDCLNT_E_BUFFER_SIZE_ERROR as documented for that case.
Ah true. Event driven is slightly different. :)
Last week I wrote:
Somewhat I found the old behavior more consistent.
Upon reflection, the new behavior is better. It makes more sense to drop old frames than to keep them around thus play ghost sounds from the past.
However, I'd even more prefer shared and exclusive mode to behave the same *and* not drop frames. I believe shared mode does not drop partial fills (based solely on what GetPosition returns) and would have expected exclusive mode to do that too. I don't know why MS decided to do that but I now see why they recommend that programs add silence at the end of their sound: it works around that odd behaviour.
But it's not the same, wine doesn't officially handle exclusive mode. Instead it might be better to just report failure and only allow shared mode. On windows you're not required to support exclusive mode, and you can disable exclusive mode in the audio control panel.
Maarten also wrote:
I prefer 0 tracking, and just check the return value of snd_pcm_writei.
I can understand that but then we need to throw rate-limiting into the discussion. We are talking about GetPosition here; computing GetPosition solely on the base of snd_pcm_writei (let's throw in avail_update without delay) gives us already known bugs, e.g. PulseAudio backend audio 2s off video sync.
Ah true, I do think you only want to observe underruns, and not act on them in GetPosition and GetClock, just report that all data is played. If that still breaks pulseaudio, I think it's time to get a real pulseaudio driver..
And without snd_pcm_delay, the sole culprit would be: Wine. If, OTOH, we use it, we can hope other projects will fix their bugs.
snd_pcm_delay would map to clock, while snd_pcm_avail maps to position, they're not the same, so it would make sense to handle them separately.
~Maarten
Hi,
Maarten Lankhorst wrote:
But it's not the same, wine doesn't officially handle exclusive mode.
Eh? This is new to me! Back in June Andrew Eikum wrote:
These limitations might be true for exclusive mode, which isn't really implemented in Wine.
... which is not the same in my ears.
Andrew back in June:
I think it would be worth having a loud FIXME when exclusive mode is requested, though.
Maarten today:
Instead it might be better to just report failure and only allow shared mode.
I prefer a FIXME in exclusive non-event mode and the error in exclusive event mode because that particular GetBuffer protocol is not supported in the current code. FIXME because exclusive non-event mode basically works except that we don't know yet when exactly native delivers events. As nobody else wrote that patch so far, I'll do.
On windows you're not required to support exclusive mode, and you can disable exclusive mode in the audio control panel.
I've read that games offer it as an option but believe that all apps will use the shared mode by default for the reason you say. Mixing has become a commodity.
http://msdn.microsoft.com/en-us/library/dd370844%28v=vs.85%29.aspx I meant an entire period then, this describes how windows handles it
What part of that document do you have in mind? Their example uses exclusive+EVENTCALLBACK, which my test doesn't.
my guess is that if you don't fill a period, you get an underrun, no matter how you handle it.
Sure. Yet I want to know how it affects GetPosition, e.g. bug #28039 IMHO I'm free to use GetBuffer+ReleaseBuffer twice or 3x within whatever MSDN calls a "buffer-processing period" if it pleases me. What matters is how much has accumulated once the period tick occurs.
If period sizes would really matter, then the API would provide means to obtain the period size in frames. It does not. Initialize takes a period in milliseconds and MSDN does not guarantee what you actually get. So the average app simply does not know the period size in frames. (My tests do try to second-guess mmdevapi's exact behaviour). Actually, mmdevapi tries to abstract away from these parameters.
BTW, a bug in MSDN: render.c:799: Test failed: BufferSize 21846 too small for duration at rate 44100 mmdevapi appears to use not ceiling(), but floor(duration, period_frames). The actual duration is *not* at least as large as asked for. (For the period, it uses ceiling).
Regards, Jörg Höhle