-
Notifications
You must be signed in to change notification settings - Fork 150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How OutputStream callback is supposed to stop on eof? #306
Comments
Well that's your job as implementer of the callback function. If you want the callback function to not be called anymore, you should raise
Why would a block be zero-filled? Unless the audio file contains zeros?
Checking for zero has very consistent timing and is quite fast, so this is not a problem. Still, checking for zeros is not the right answer for different reasons.
The
I think with this you would always be one block too late.
Why do you think anything is zero-filled? The documentation says "The rest of the buffer is not filled with meaningful data." BTW, you should definitely not ignore the return value! You should think about all the possible situations that could happen after calling
You should think about whether those are really all possible cases and what to do in each case. Here is a similar situation in one of the examples: python-sounddevice/examples/play_long_file.py Lines 78 to 83 in f063cd1
|
Thanks for reply, as always, it's definitely helpful to get direct opinion from dev! For my addiction with zero-filling - when I leave About the example play_long_file, I admit that I overlooked it after having two confusion on it. Document says:
But the example uses Additionally, for what I've seen that example is set to use buffer dtype of float32, however - that example is filling buffer with byte strings. Noticing that, I tried with this callback. def stream_cb(data_out, frames: int, time, status: sd.CallbackFlags) -> None:
assert not status
data_out[:] = b"\x00" * frames This immediately caused error. # Skipped scripts names
File "E:\github\CUIAudioPlayer\SoundModule.py", line 177, in stream_cb
data_out[:] = b"\x00" * frames
ValueError: could not convert string to float: '' Since example scripts does not cover that if block, scripts just skips over and go Other than that, there's still more questions I'd like to ask, but I'm afraid it doesn't belong to this issue anymore. If you don't mind answering those, can I have your preferred email address, maybe? |
It may actually fill with zeros. Maybe it does it some times and sometimes not, maybe only on certain operating systems, but you should not rely on it. You should check the documentation in such a case and not assume general behavior from a special case.
The sentence "This is the same as the length of the input and output buffers." only appears in the docs for In the case of In the case of https://python-sounddevice.readthedocs.io/en/latest/api/streams.html#sounddevice.Stream.samplesize Probably we should clarify this in the docs, do you want to make a PR for that? That's why I'm using
Yes, the example uses What are you using? If you use
It should cover the You could try using a very short audio file for testing?
I will only answer usage questions if the question and answer is publicly accessible. If you need to contact me privately, my e-mail address is visible in the Git commits. |
I completely overlooked the difference of buffer objects between callbacks of Raw and numpy variants. I'm terribly sorry about that. What a shame to have a headache on it for hours, now I think I'm going in right direction. Since I tried that callback on last_frame = -1
audio_ref # SoundFile Object
channel = audio_ref.channels
def stream_cb(data_out, frames: int, time, status: sd.CallbackFlags) -> None:
nonlocal last_frame
assert not status
if (written := audio_ref.buffer_read_into(data_out, dtype)) < frames:
data_out[written:] = [[0.0] * channel for _ in range(frames - written)]
raise sd.CallbackStop
if last_frame == (current_frame := audio_ref.tell()):
raise sd.CallbackAbort
last_frame = current_frame With this pause/resume is working greatly, and I can't be happier than right now. Also now I also see underflow happening too, I probably made a mistake previously. About the PR, I think this module is overall well-documented, but I find that some methods' aren't enough.
Apparently "plain Python buffer objects" was a Like this, few parameters or arguments might need few more users are the one who is writing a callback, they, myself included, might have no clue about the exact object/interface that the signature is pointing at. And just like I couldn't, it might be hard to navigate thru source codes and find a complete signature for others too. If we know exactly what object/interface the signature is referencing about, we can simply add a type hint and IDE can help us rest of the way. From this context, I'd like to add type hints for these documents on PR on top of the relationship between buffer, frames, samplesize, and channels - after figuring those out myself first. I owe you a lot for all your helps! |
Don't worry, no problem at all! For me this is a sign that the documentation should be improved to make it less confusing for new users. Your code example with the callback function looks good. I'm not sure whether the And you can simplify the zero-filling: data_out[written:].fill(0) If I understood correctly, now you are using It doesn't make a huge difference in this case, but the
Yes, that's the problem, the returned type isn't a public type.
That's probably because the callback isn't called by the Python module! The Python code only provides a function pointer called python-sounddevice/sounddevice.py Lines 824 to 885 in f063cd1
The raw buffers for the callback are prepared there: python-sounddevice/sounddevice.py Lines 2692 to 2694 in f063cd1
Once the callback has been called by PortAudio, the user-defined callback function is called there: python-sounddevice/sounddevice.py Lines 2682 to 2689 in f063cd1
For the "buffer" type it might not be easy, because is isn't a native Python type. I don't have any experience with Python type hints, but I found this issue: python/typing#593
I'm glad I could help! You can repay me by helping me improve the documentation and the examples! |
Just tried out last_frame: int = -1
def stream_cb(data_out, frames: int, time, status: sd.CallbackFlags) -> None:
nonlocal last_frame
assert not status
data_out[:] = audio_ref.read(frames, fill_value=0)
if last_frame == (current_frame := audio_ref.tell()):
raise sd.CallbackAbort Thanks for tip, I literally forgot that
Just found out that developers decided not too for the similar reason, from Raymond Hettinger himself:
on the last of thread is a github issue page you've linked, yet there's no update since then. Until then, seems like all we can do is checking out docs for general bytes-like object or
For what I've tried until now, checking if last frame is same with current File-object cursor position returned by And there's additional reason that's not shown up until now as it wasn't part of minimum reproducible example. From actual code using the callback: last_frame: int = -1
cycle = itertools.cycle((not n for n in range(stream_manager.callback_minimum_cycle)))
# to reduce load, custom callback will be called every n-th iteration of this generator.
def stream_cb(data_out, frames: int, time, status: sd.CallbackFlags) -> None:
nonlocal last_frame
assert not status
data_out[:] = audio_ref.read(frames, fill_value=0)
if last_frame == (current_frame := audio_ref.tell()):
raise sd.CallbackAbort
last_frame = current_frame
if next(cycle):
callback(audio_info, current_frame)
# Stream callback signature for user-supplied callbacks
# Providing current_frame and duration to reduce call overhead from user-callback side. Results of Since py_cui has no exposed event loops or no public API for doing such, I'm using Sounddevice callback to drive CUI elements instead! So far this isn't putting pressure on playback, but would be safe to guess the maximum time on callback should be under |
That's definitely an upper limit, but the actually available time might be significantly less. The problem is that you should always consider the worst case, that the operating system is blocking for some reason which might lead to an audible artifact (a drop-out or a click or something like this). You can of course never guarantee anything, but you should try to keep some additional time for such cases. Doing GUI stuff in the audio callback is normally a no-go, but I'm not sure about TUIs. Anyway, if you don't need very short latencies and if you use a conservatively large latency setting (e.g. 100 milliseconds), you should be fine. |
Sorry for reviving this again, I just cloned module while ago to see how documents - more likely, how sphinx autodoc - are working in this module. Found out that I cloned There's bunch more like these folks with various commit histories. Is all other than |
@jupiterbjy The Github name of the
One (this here) is providing the Both use Sphinx's |
Oh I made a big mistake, I copy-pasted completely wrong thing while writing, mybad! I was trying to ask about the difference with your folk and this one. Originally I was planning to make a PR on your folk, as you were replying me on this issue. Since then I saw your latest branch in yours is And that's why I was trying to ask if other folks - like yours - and this one was independent or not, as I thought committing directly to another one's repository was impossible unless accepted via Pull request. |
I think you mean "fork" instead of "folk", right? You should use the official repo (https://github.com/spatialaudio/python-sounddevice/) to make PRs. My personal fork (https://github.com/mgeier/python-sounddevice/) should be irrelevant to you, but you can have a look if you want!
I actually do use my fork for making PRs here, but I often merge them using a "fast-forward" merge, that's why there are no merge commits. It is mostly a matter of the maintainers' taste whether merge commits are used or not.
You can make a PR on any fork and you will see if and how the owner of the fork reacts. In some cases it makes sense to make PRs on non-official forks, but if you want to contribute to the And yes, you can of course only directly commit to repos where you have the relevant access rights. But by means of PRs you can make changes to any repo, as long as its maintainers accept them. |
Here's what I find confusing.
On documents about Stream callback, it says:
While so, document don't mention about hot to stop a callback.
So I made a guess that am I supposed to keep buffer zero-filled all time even if eof was reached.
However, if an audio files reaches EOF and I need to stop the callback, how a callback is supposed to know whether it's EOF or not? If it's possible, then what's the designed / intended way of doing so?
Below are things I've tried of:
assert soundfile.SoundFIle.tell() != soundfile.SoundFIle.frames
, check if current frame and total frames equals.soundfile.SoundFIle.tell()
is same.First one immediately fails if there's silence in audio files, and calculation cost may be expensive, thus violating following line:
Second fails as
SoundFile.buffer_read_into()
stop advancing cursor earlier thanframes
count and keep zero-filling the buffer. I can't check if last frame was same in case ofFrom this, I thought of third. If current frame(aka cursor) position is same and there's no duplicated frames, then I could check if last frame was same or not, and this works so far. Code as follows.
https://github.com/jupiterbjy/CUIAudioPlayer/blob/4495df15ae36fa98a2c84a5cc7f619e2abc25770/Demo/SoundDeviceCallbackTest.py
But this is dependent on SoundFont Module thus I don't think this is any closer to designer's intention.
The text was updated successfully, but these errors were encountered: