View Issue Details

IDProjectCategoryView StatusLast Update
0001017OpenMPTlibopenmptpublic2023-01-13 17:47
ReporterSaga Musix Assigned To 
PrioritynormalSeverityfeatureReproducibilityN/A
Status newResolutionopen 
Target VersionOpenMPT 1.31 / libopenmpt 0.7 (goals) 
Summary0001017: Tick boundary rendering (was: Provide access to next play position)
Description

PoroCYon asked on IRC if it's possible to retrieve the next play position (row/pattern). We already have this information, but it is currently not exposed.

Additional Information

https://forum.openmpt.org/index.php?topic=5935

TagsNo tags attached.
Has the bug occurred in previous versions?
Tested code revision (in case you know it)

Relationships

related to 0001287 acknowledgedmanx make libopenmpt samplerate non-variable 

Activities

manx

manx

2017-08-27 06:38

administrator   ~0003194

Uhm, what would be the use case here?

I'd rather see this aspect to be factored into the already planned tick-boundary accurate playback.

Saga Musix

Saga Musix

2017-08-27 14:34

administrator   ~0003195

I can see use cases like OpenMPT's smooth pattern scrolling - depending on the next play position, you may have to scroll either up or down.

Saga Musix

Saga Musix

2017-08-27 18:07

administrator   ~0003196

Another use case is any general monitoring of pattern transitions. Let's say you want to do a transition to a different pattern once the current pattern has finished playing. Since the pattern may end on any row due to pattern break commands, it may not be known what is going to be the last row of the current pattern. If you want to solve this using tick-boundary rendering, you will have to throw away rendered audio for at least one tick, which I think would generally make control flow more complicated (and of course increase the required processing time).

manx

manx

2017-08-27 18:27

administrator   ~0003197

Another use case is any general monitoring of pattern transitions. Let's say you want to do a transition to a different pattern once the current pattern has finished playing. Since the pattern may end on any row due to pattern break commands, it may not be known what is going to be the last row of the current pattern.

In that case, the required API would be more narrow than what is required to solve this issue. It would only be necessary to query the next play position after a tick has been rendered completely. And I would be very much OK with an API only implementing exactly that (once we have tick-boundary rendering).

I am very hesitant to add an API like the one described in this issue as it (on the API level) requires predicting the future. After all, semantically, the next play position does not actually need to be known until after the current tick has been rendered completely. It may be possible to implement that unambiguously for the currently known and supported formats, but if some other format or tracker quirk shows up that makes the next play position depending on anything mixer-related (sample loops or whatever), this API would not be supportable without adding another buffering or prediction layer inside libopenmpt.

I do not think it makes much sense to try to handle any of the use cases described here while not thinking about tick-boundary rendering at the same time.

Saga Musix

Saga Musix

2018-02-14 19:48

administrator   ~0003421

Related: It might make sense to provide two more functions,

  • get_current_tick(): Returns the currently processed tick on the current row.
  • get_ticks_on_current_row(): Some pattern commands can extend the duration of a row and are not considered by get_current_speed(). This function would return the true number of ticks on the current row.
manx

manx

2018-02-14 20:04

administrator   ~0003422

To summarize the additional design suggestions so far:

  • set_tick_auto_advance(bool): Set tick-auto-advance mode, true meaning the default and old behaviour, false requiring manual tock advancing
  • get_tick_auto_advance(): Getter for the above setting
  • get_current_tick_frames(): Returns the number of sample frames in the current tick
  • get_current_tick_remaining_frames(): Returns the number of sample frames until the current tick is done (this maps 1:1 to the internal m_PlayState.m_nBufferCount value)
  • advance_tick(): Advances the internal play state by one tick. This would only be valid when get_current_tick_remaining_frames() == 0.
    This is by no means meant to be the final API design, but merely a suggestion for further discussion.
    How to handle read() in tick-boundary mode still has to be designed.
    Also, most of the API goes somewhat haywire when considering changing sample rates (which probably were not that good of a design decision in the first place). We might just deprecate changing sample rates and require explicit sample rate setting when desiring a change. However, the even more concerning (future) feature in this area is internal mixer oversampling and the inherent delay it would cause in the output path.
manx

manx

2018-02-14 20:08

administrator   ~0003423

We also should investigate whether splitting advance_tick() into finish_tick() and start_tick() (or similar) could turn out to be useful for certain use cases.

Saga Musix

Saga Musix

2018-02-14 20:13

administrator   ~0003424

One challenge here is going to be that, from what I understand, information like get_current_tick_frames() needs to be available before the actual read() call, but currently this information is updated during the read() call (i.e. at the beginning of a tick). Internally, we might have to split up the calls to CSoundFile::ReadNote() and the rest of CSoundFile::Read(). Further complications arise from interactive functions that e.g. set the current play position or tempo. The semantics of doing such things before calling advance_tick() and after calling it need to be considered.

manx

manx

2018-02-14 20:23

administrator   ~0003425

One challenge here is going to be that, from what I understand, information like get_current_tick_frames() needs to be available before the actual read() call, but currently this information is updated during the read() call

This is precisely the reason why I think we need the explicit advance_tick() call or something similar. Unless advance_tick() got called, the information would apply to the previous, just finished, tick (what happens on seek and song start needs to be considered here). And yes, that obviously requires splitting CSoundFile::Read() into multiple functions. However, I do not think that can be avoided anyway, regardless of what the API looks like. It currently does too many things at once, which makes designing an API on top of the current implementation close to infeasible, or at the very least inflexible and limited in what use cases it could solve. Preferably, the CSoundFile::Read() split would both, closely match the exported libopenmpt API, as well as suite the internal needs of the OpenMPT playback engine.

manx

manx

2020-05-28 11:02

administrator   ~0004353

https://forum.openmpt.org/index.php?topic=6367.0

manx

manx

2021-08-09 18:48

administrator   ~0004836

https://forum.openmpt.org/index.php?topic=6656

Saga Musix

Saga Musix

2021-12-04 22:50

administrator   ~0004939

Note that the recently-added CSoundFile::ReadOneTick demonstrates how this can be implemented:

  1. Call CSoundFile::ReadNote, which parses the next tick and returns the length of that tick.
  2. After that, CSoundFile::Read can be called with that length.
manx

manx

2023-01-10 09:55

administrator   ~0005459

https://forum.openmpt.org/index.php?topic=6919

violgamba

violgamba

2023-01-10 19:50

reporter   ~0005460

Last edited: 2023-01-11 03:41

Hi. I'm not very familiar with the standard way of doing these things, but might it make sense to have this setup?

"stopReadAtOrderEnd" and "priorReadHitOrderEnd" flags

If user sets the stopReadAtOrderEnd flag, each call to read() will fill the buffer as normal, unless it hits the end of an order, at which point it will fill the buffer only up to the end of that order. When this happens, read() will also set the "priorReadHitOrderEnd" flag (which is otherwise cleared). Subsequent calls to read() continue filling the buffer as normal until the next order-end event.

To react to a change in order, the user could set the "stopReadAtOrderEnd" flag, then check the "priorReadHitOrderEnd" flag after each read(). When the "priorReadHitOrderEnd" flag becomes set, the user could react by running their order-change logic, then immediately call read() again to fill the rest of the buffer.

The same could be done for rows with "stopReadAtRowEnd" and "priorReadHitRowEnd" or even ticks with "stopReadAtTickEnd" and "priorReadHitTickEnd".

violgamba

violgamba

2023-01-12 22:43

reporter   ~0005472

(Sorry for the mass revisions. I didn't realize they'd all show up in the history.)

manx

manx

2023-01-13 08:27

administrator   ~0005473

The same could be done for rows with "stopReadAtRowEnd" and "priorReadHitRowEnd" or even ticks with "stopReadAtTickEnd" and "priorReadHitTickEnd".

All of these are representable in a more general way by allowing to query the tick length and always reading the exact tick length amount. There are no additional flags or state required.

violgamba

violgamba

2023-01-13 09:24

reporter   ~0005474

I see. So can you get a tick length (in samples) when you starting playing the mod file, and then just keep using that same tick length throughout the play?

manx

manx

2023-01-13 09:30

administrator   ~0005475

No, you would query the tick length before/inbetween/after each tick. This is a detail that has yet to be determined.

The tick length changes based on speed/tempo/shuffle.

violgamba

violgamba

2023-01-13 09:52

reporter   ~0005476

Last edited: 2023-01-13 10:00

Ok. So, the user follows up each read with a query of how many ticks that read contained, keep a tally of the ticks read, use that to determine when they are approaching the point in the mod that they care about, then shift to reading tick-by-tick until they have hit their mark. Is that the idea?

EDIT: Or, you say "This is a detail that has yet to be determined.", so is this one of a few possible ideas?

manx

manx

2023-01-13 09:56

administrator   ~0005477

Yes, that's the idea. Note that this is in no way different in principle from what you are suggesting, with the only difference that the position checks happen outside of libopenmpt instead of inside. This has the major advantage of covering any and all kinds of events any user might care about, as opposed to adding functions and state the libopenmpt for each and every one of them.

manx

manx

2023-01-13 10:30

administrator   ~0005478

The aspect that has yet to be determined, is how to handle tick transition. I suspect there may be legitimate reasons to inspect playback state right after a tick has been completely rendered, as well as right after a tick has been parsed and not yet rendered at all, or even in an in-between state (whatever that implies).

examples:

  • jumping to another pattern has to be done before a new tick has been started/parsed
  • querying the current/future tempo/speed makes more sense after the next tick has already been parsed
violgamba

violgamba

2023-01-13 11:51

reporter   ~0005479

Last edited: 2023-01-13 12:30

I see one technical difference:
Libopenmpt is already tracking ticks, rows and orders as a natural part of its process, so there is a negligible performance penalty for exposing this information to the user. On the other hand, the act of reading tick-by-tick does incur a notable performance penalty.

To examine this performance penalty, I picked a simple song that has stable ticks, rows and orders and worked out the samples per tick:

  • 146875 samples per order - calculated using the callback I wrote earlier as an average from 38 order renderings and with a standard deviation of 514 (a reasonably consistent result). The callback is performant: one function pointer call at the end of each row.
  • 32 rows per order
  • 5 ticks per row
  • 918 samples per tick (rounded up)

I altered a test program from filling up the audio buffer in one mod::read() call, to filling it up in multiple mod::read() calls that were maxed at 918 samples per read. This represents the equivalent of reading tick-by-tick for this particular song.

When I ran the song with the test program, it came out heavily distorted. Admittedly, the distortion went away after I bumped the max samples to (int)(918*1.2) = 1101 samples per read. However, my computer has a 4GHz quad-core processor, running at 17% cpu outside of the test program. The song has 9 channels and the test program is a console application dedicated exclusively to playing the mod (I removed the callback and all console output). I expect that this test is easier for libopenmpt than the average use case.

I recognize that there may be better ways for a user to count ticks than what I am testing for. I'm also open to any critiques on this test.

EDIT: I just tried the test again, with the same song, but with a single solo'd channel. I tried soloing each channel in turn. It didn't have an appreciable effect.

violgamba

violgamba

2023-01-13 12:50

reporter   ~0005480

Last edited: 2023-01-13 12:55

And, yeah, I recognize that my test invalidates my earlier idea of flags... at least as far as ticks are concerned.

It occurred to me that reading tick-by-tick might work better if it was only for a short while. I modified the program to fill the buffer normally unless I hit a particular key, then it would read tick-by-tick for 2 ticks before returning to filling the buffer again. I chose to use two ticks because I realized that my audio buffer was at 1024, which is less than 918*2.

The result was more palatable. However, there was a quick "crunch" like a footstep in snow. Sometimes it was too quiet to hear, sometimes loud and distinct.

manx

manx

2023-01-13 13:58

administrator   ~0005481

Your measurements do not make much sense to me. libopenmpt already renders at max one single tick at a time. You cannot render any module any other way anyway. The only thing that would differ is returning to the caller instead of continuing inside libopenmpt.

If the output was distorted for you, you probably did something wrong, or coupled the rendered amount to the sound output buffer length or to the screen update interval (which is a fair thing to do if you control the granularity, but makes no sense if the granularity is adopted to the module tick structure).

violgamba

violgamba

2023-01-13 14:09

reporter   ~0005482

It's possible. I was thinking that the read function call had some static time overhead that was being compounded with more frequent calls, though I haven't actually read the function to see if this is true. I'll double check my code.

violgamba

violgamba

2023-01-13 17:41

reporter   ~0005483

Last edited: 2023-01-13 17:47

You were right. I had a bug while advancing the audio stream pointer as I was filling the stream piece by piece from libopenmpt. After I fixed that, things worked fine all the way down to adding 6 samples per read. It started glitching at 5 samples per read, but that is far lower than what is needed. Anyway, I am sorry for wasting your time with all that.

It occurs to me that your design suggestion of using the "tick_auto_advance" flag actually seems somewhat similar to my earlier suggestion. The second flag in my suggestion wouldn't make sense for ticks anyway, since they are smaller than any reasonable read() and the second flag was to let you wait for the next read that contained the event.

However, I'm confused about what you mean when you say that tick_auto_advance==false makes the system require "manual tock advancing". How does manual tock advancing occur? Does the read() only accept sample size of 1, or is the read disabled altogether and advance_tick() is the way advance?

You describe your dilemma as "regardless of what the API looks like. It currently does too many things at once". It sounds like you're describing the solution as breaking up the steps of methods like read(). This ise a useful way to give more control to those that need it, but would it make sense to also leave the monolithic methods for those who don't need that granularity, or would be daunted by its complexity? The monolithic methods could just be a series of calls to other functions that the user would have access to directly if they wanted. In fact, the monolithic methods could provide a form of documentation for how to effectively manage advanced control.

Also, it seems like "advance_tick()" should end at the earliest possible point of interest, then have new methods to advance to subsequent points of interest, like "render_tick()" and "parse_tick()". What should happen if "advance_tick()" is called after calls to subsequent stage methods like "render_tick()" have occurred? It could recognize which stage the process is in, complete the uncompleted stages, then advance to the end of the current tick (and, thus, the start of the next). Alternately, it could error out.

But just having "advance_tick()" pause at the earliest possible point is a good start and provides the space to add more interaction points down the road.

Issue History

Date Modified Username Field Change
2017-08-26 21:27 Saga Musix New Issue
2017-08-27 06:38 manx Note Added: 0003194
2017-08-27 09:27 manx Target Version => OpenMPT 1.29.01.00 / libopenmpt 0.5.0 (upgrade first)
2017-08-27 14:34 Saga Musix Note Added: 0003195
2017-08-27 18:07 Saga Musix Note Added: 0003196
2017-08-27 18:27 manx Note Added: 0003197
2018-02-13 15:39 manx Additional Information Updated
2018-02-14 19:48 Saga Musix Note Added: 0003421
2018-02-14 20:04 manx Note Added: 0003422
2018-02-14 20:08 manx Note Added: 0003423
2018-02-14 20:08 manx Target Version OpenMPT 1.29.01.00 / libopenmpt 0.5.0 (upgrade first) => libopenmpt 0.4 (goals)
2018-02-14 20:13 Saga Musix Note Added: 0003424
2018-02-14 20:23 manx Note Added: 0003425
2018-02-14 20:25 manx Summary libopenmpt: Provide access to next play position => Tick boundary rendering (was: Provide access to next play position)
2018-08-18 12:49 Saga Musix Target Version libopenmpt 0.4 (goals) => OpenMPT 1.29.01.00 / libopenmpt 0.5.0 (upgrade first)
2020-01-05 10:44 manx Target Version OpenMPT 1.29.01.00 / libopenmpt 0.5.0 (upgrade first) => OpenMPT 1.30.01.00 / libopenmpt 0.6.0 (upgrade first)
2020-01-07 10:54 manx Relationship added related to 0001287
2020-05-28 11:02 manx Note Added: 0004353
2020-11-28 19:59 manx Target Version OpenMPT 1.30.01.00 / libopenmpt 0.6.0 (upgrade first) => OpenMPT 1.31 / libopenmpt 0.7 (goals)
2021-08-09 18:48 manx Note Added: 0004836
2021-12-04 22:50 Saga Musix Note Added: 0004939
2023-01-10 09:55 manx Note Added: 0005459
2023-01-10 19:50 violgamba Note Added: 0005460
2023-01-10 19:54 violgamba Note Edited: 0005460
2023-01-10 19:55 violgamba Note Edited: 0005460
2023-01-10 19:55 violgamba Note Edited: 0005460
2023-01-10 19:56 violgamba Note Edited: 0005460
2023-01-10 19:58 violgamba Note Edited: 0005460
2023-01-10 20:00 violgamba Note Edited: 0005460
2023-01-10 20:01 violgamba Note Edited: 0005460
2023-01-10 20:07 violgamba Note Edited: 0005460
2023-01-10 20:08 violgamba Note Edited: 0005460
2023-01-10 20:10 violgamba Note Edited: 0005460
2023-01-10 20:10 violgamba Note Edited: 0005460
2023-01-10 20:11 violgamba Note Edited: 0005460
2023-01-10 20:16 violgamba Note Edited: 0005460
2023-01-10 20:16 violgamba Note Edited: 0005460
2023-01-10 20:25 violgamba Note Edited: 0005460
2023-01-10 20:33 violgamba Note Edited: 0005460
2023-01-10 20:46 violgamba Note Edited: 0005460
2023-01-11 03:09 violgamba Note Edited: 0005460
2023-01-11 03:10 violgamba Note Edited: 0005460
2023-01-11 03:41 violgamba Note Edited: 0005460
2023-01-12 22:43 violgamba Note Added: 0005472
2023-01-13 08:27 manx Note Added: 0005473
2023-01-13 09:24 violgamba Note Added: 0005474
2023-01-13 09:30 manx Note Added: 0005475
2023-01-13 09:52 violgamba Note Added: 0005476
2023-01-13 09:55 violgamba Note Edited: 0005476
2023-01-13 09:56 manx Note Added: 0005477
2023-01-13 10:00 violgamba Note Edited: 0005476
2023-01-13 10:30 manx Note Added: 0005478
2023-01-13 11:51 violgamba Note Added: 0005479
2023-01-13 11:54 violgamba Note Edited: 0005479
2023-01-13 12:30 violgamba Note Edited: 0005479
2023-01-13 12:50 violgamba Note Added: 0005480
2023-01-13 12:54 violgamba Note Edited: 0005480
2023-01-13 12:55 violgamba Note Edited: 0005480
2023-01-13 13:58 manx Note Added: 0005481
2023-01-13 14:09 violgamba Note Added: 0005482
2023-01-13 17:41 violgamba Note Added: 0005483
2023-01-13 17:44 violgamba Note Edited: 0005483
2023-01-13 17:47 violgamba Note Edited: 0005483