• Please review our updated Terms and Rules here

MOD player Porta to Note effect

pan069

Member
Joined
Jun 4, 2019
Messages
49
Hey everyone,

I have a question re. the Porta to Note effect that I am trying to implement in my MOD player.

My MOD player seems to be working correctly, I'm loading the patterns and sample data and I can playback MODs that don't use effects too much and they sound correct. However, once I started implementing the Porta to Note effect [1] I ran into a bit of a snag. The idea behind this effect is pretty simple, e.g. if you're on note F-1 it allows you to slide up to another note, eg. F-2 with a specified increment (step).

However, the problem I am running into is that it never reached the target note. Take eg. the classic MOD Space Debris [2] which almost immediately starts off with a Porta to Note effect. On the first pattern (played), in channel #3, row 0 (1st) it sets the note to F-1 and triggers a Volume Slide effect (0xA) to volume 10. So far all good, the channel plays at F-1. It then gets to row 12 (13th) when it does a "F-2 .. 308", meaning, "3" (the porta to note effect), it wants to slide to note F-2 (the channel is still on F-1) with steps of 08. So, we don't change the channel frequency, we stay on F-1 but are going to slide to F-2 with steps of 8.

Now, in the Amiga period table the value for F-1 is 640 and the value for F-2 is 320. Meaning there is a 320 difference going from 640 to 320. If we're doing this with steps of 8 then we need 40 ticks to accomplish the fulll slide (320 / 8 = 40). However, there are only 25 ticks available. You see, the Porta to Note effect starts on row 12 (13th) and ends on row 17 (18th) because on row 17 for that channel a Volume Slide effect is started with stops the Porta to Note effect. So, this gives us 5 rows to perform the Porta to Note. The speed is set to 6, so 6 ticks per row. However, tick 0 doesn't do anything for Porta to Note, i.e. Porta to Note is not updated on tick 0, which leavs us with 5 ticks per row in which we can update the Porta to Note. I.e. 5 rows * 5 ticks = 25 ticks. This is not enough, just over halfway there.

So, I am not entirely sure what I am doing wrong. There is clearly something I am overlooking and any thoughts and or insights would be appreicated.

I have added a visual reference that might be helpful to visualise the problem. This screenshot was taken from MiklyTracker (Ubuntu). This tracker does something funky with the representation of the notes (i.e. it shows period 640 as F-4 where in the Amiga period table it is F-1), but that is just a representation thing, it still sounds the same.

space.png


[1] https://github.com/vlohacks/misc/blob/master/modplay/docs/FMODDOC.TXT#L1608
[2] https://markuskaarlonen.com/space-debris
 
Maybe helpful, this is a dosbox recording of the first part (Porta to Note), basically the F-1 to the F-2 and back to the F-1. As you can hear, the slide gets about half way there. You can compare the pitch of what it supposed to sound like on YouTube:

View attachment sb_000.wav
 
You don't slide on tick 0, but you do slide on every other tick. You might accidentally be not sliding on tick 0 of every new row, which is wrong, and wouldn't give you enough steps.

This might also be an off-by-one problem, where you think you're sliding by SPEED but are instead sliding by SPEED-1. Without seeing your code, can't tell.
 
Thanks for that, much appreciated!

> You don't slide on tick 0, but you do slide on every other tick. You might accidentally be not sliding on tick 0 of every new row, which is wrong, and wouldn't give you enough steps.

Yeah, I was wondering about that, I haven't found any documention that says to do a slide on zero ticks of subsequent rows, but it does make sense. However, sliding on tick zero of subsequent rows only give me 4 extra ticks (i.e. 5 rows of sliding in total is 5 zero ticks but the first one is not used). But I will certainly implement sliding on tick zero of subsequent rows.

> This might also be an off-by-one problem, where you think you're sliding by SPEED but are instead sliding by SPEED-1. Without seeing your code, can't tell.

I don't think this is the case. The effect parameter says 8 and I am adding/subtracting that value on each tick. Below is some of my code that I think might be relevant, I removed a bunch of stuff for brevity purposes...

This is the function to update a row. It does a whole bunch of stuff, like setting the channel note when there is one etc, but it also checks the effects (tick #0). In this case the Porta to Note. A composer can program this effect in a bunch of variations that you need to cater to e.g. no note set or no parameter etc. I have also introduced your suggestion to keep sliding on subsequent tick #0's by using the "porta_tick" variable. This will be zero only the first time when the effects are update so it keeps slding on subsequent tick #0's:
Code:
void _mod_update_row()
{
  MOD_NOTE* note;
  MOD_CHANNEL* channel;

  // i = the channel we're processing (0 to 3)

  channel = channels[i];

  note = &patterns[current_pattern]->rows[current_row].notes[i];

  // The channel->effect and channel->effect_param are set here.

  if (channel->effect <= 0x0f) {
    switch (channel->effect) {

      case MOD_EFFECT_PORTA_TO_NOTE : // 0x03
        if (note->period_index >= 0) {
          // when a period/note is defined then use that as the new target porta note
          channel->porta_index = note->period_index;
          channel->porta_value = _mod_tunings[channel->finetune][channel->porta_index];
          // starting on tick 0 so we can keep sliding on subsequent row updates
          channel->porta_tick  = 0;
        }

        if (channel->effect_param > 0) {
          // If an effect parameter was specified then use that parameter
          channel->porta_param = channel->effect_param;
        }

        break;

    }
  }

}

This is called on every tick. It checks that "porta_tick" variable and only skips this effect update the first time:
Code:
void _mod_update_effects()
{

  if (channel->effect <= 0x0f) {
    switch (channel->effect) {

      case MOD_EFFECT_PORTA_TO_NOTE : // 0x03
        if (channel->porta_tick > 0) {
          // Always update except tick #0
          _mod_update_effect_porta_to_note(channel);
        }

        channel->porta_tick++;
        break;

    }
  }

}

The actual Porta to Note effect update. It just adds/subtracts the porta parameter based on which direction we're going:
Code:
void _mod_update_effect_porta_to_note(MOD_CHANNEL* channel)
{
  if (channel->period_value < channel->porta_value) {
    channel->period_value += channel->porta_param;

    if (channel->period_value > channel->porta_value) {
      channel->period_value = channel->porta_value;
    }
  }
  else
  if (channel->period_value > channel->porta_value) {
    channel->period_value -= channel->porta_param;

    if (channel->period_value < channel->porta_value) {
      channel->period_value = channel->porta_value;
    }
  }
}

This is the main update loop, I thought that might be interesting to see, it does all the house keeping to keep track of how many samples to mix, which tick we are on, which row and pattern etc:
Code:
void mod_update(char* mix_buffer, uint16_t samples_to_mix)
{
  unsigned i;

  while (samples_to_mix--) {
    current_samples_per_tick++;

    if (current_samples_per_tick >= samples_per_tick) {
      current_samples_per_tick = 0;

      current_tick++;

      if (current_tick >= speed) {
        _mod_update_post_effects(); // effects that happen at the end of a pattern, e.g. pattern break

        current_tick = 0;

        current_row++;

        if (current_row >= MOD_MAX_ROWS) {
          current_row = 0;

          current_order++;

          if (current_order >= pattern_count) {
            bpm = MOD_DEFAULT_BPM;
            speed = MOD_DEFAULT_SPEED;

            _mod_calc_speed(); // updates "samples_per_tick" based on bpm/speed settings.

            current_order = 0;
          }

          current_pattern = order[current_order];
        }

        _mod_update_row();
      }
   
      _mod_process_tick_effects();

      // Here I calculate the playback frequency for each channel.
    }

    // Here we mix the channels into the output buffer (22050 / mono / 8 bit)
  }

}

Pretty standard way to caluclate the samples per tick that we need to mix:
Code:
void _mod_calc_speed()
{
  samples_per_tick = playback_frequency / (_mod_player->bpm * 2 / 5);
}

PS: These are the period/tunings I am using in case you're wondering: https://github.com/vlohacks/misc/blob/master/modplay/docs/FMODDOC.TXT#L1132
 
I stared at this for over an hour; maybe it's because it's 1am, but I'm stumped. There's something obvious we're missing. Maybe @FreddyV might have a clue.

I've attached Bubsy's pure C rewrite of the original PT2 sources; it's the current gold standard. Maybe reading it can shake something loose.
 

Attachments

  • PT2PLAY v1.00.c.txt
    39.5 KB · Views: 3
Thank you. I will have a look at that source. I have been peeking into the source code of smod by Mark Feldman which seems to play the MOD back correctly but I can't figure out where I am making the mistake.

I am actually not sure how it is supposed to work mathmatically. It is sliding a full octave, from F-1 (640) to F-2 (320) which is a 320 difference. It has to do this over a time span of 5 rows at a SPEED of 6 which gives a total of 5 * 6 - 1 = 29 ticks. Minus 1 because we don't porta slide on tick 0. The parameter for the Porta to Note is set to 8. In those 5 rows it needs to cover the 320 difference, so 320 / 8 = 40. 11 ticks missing. It might be it doesn't have to do the full slide to get to the correct pitch but even with your suggestion of sliding on tick #0 of subsequent rows, it doesn't reach the correct pitch... :(
 
Last edited:
Not that this helps answer your question directly, but my overriding memory of MOD file playback in the early and mid '90s was that every MOD player I tried on my PC made any given MOD sound different in some way (sometimes radically) from every other MOD player I'd tried, and none of them sounded like Intuitracker on my friends' Amigas. This went double for portamento type effects. I was told that even on Amiga, there were sometimes noticeable differences between players. I eventually concluded I had no way of finding out what it meant for a MOD file to play "correctly".
 
Fair point, but because this was composed on protracker, and we have a reference from protracker playing it, that's the reference the OP is trying to hit.
 
I have been going over my code again and again. I think there is a fundamental problem but I have trouble pin pointing it. Can I run some formulas past you guys?

Some constants:
Code:
MOD_REGION_PAL     7093789.2
MOD_REGION_NSTC    7159090.5

What exactly is the formula to convert an Amiga period value to hz? I see this in a lot of documentation:
Code:
hz = MOD_REGION_x / (amiga_period * 2)

Or its variation:
Code:
hz = (MOD_REGION_x / 2) / amiga_period

However, in my code I am using:
Code:
hz = MOD_REGION_x / amiga_period

My full channel frequency calculation is like this this (all are floats and _mod_region is one of the MOD_REGION_x constants):
Code:
float period_value = (float)channel->period_value;

channel->frequency = _mod_region / period_value; // <- This one...

channel->delta = channel->frequency / _mod_playback_frequency;
So, I have the full MOD_REGION_x value (not the halfed) and I am not multiplying the Amiga period by 2... 🤔

When I do half the MOD_REGION_x value, or multiply the Amiga period value by 2, the playback is lower pitched.

The channel->delta calculation above is now used to advance the sample position:
Code:
    mix_sample = 0;

    for (i = 0; i < MOD_MAX_CHANNELS; i++) {
      MOD_CHANNEL* channel = channels[i];

      uint16_t position = (uint16_t)channel->sample_position;

      if (position < channel->sample_length) {
        mix_sample += _mod_instruments[channel->instrument_index]->data[position] * channel->volume;

        channel->sample_position += channel->delta; // both are float
      }
      else {
        MOD_INSTRUMENT* instrument = _mod_instruments[channel->instrument_index];

        if (instrument->repeat_length > 0) {
          channel->sample_position = (float)instrument->repeat_point;
          channel->sample_length = instrument->repeat_point + instrument->repeat_length;
        }
      }
    }

Then I was thinking, maybe I am not initialising the Sound Blaster playback frequency correctly, but I am using this technique which seems to correct according to the docs (channels = 1, freq = 11025 or 22050):
Code:
sb_dsp_write(base_io, SB_SET_PLAYBACK_FREQUENCY); // DSP command 0x40
sb_dsp_write(base_io, (unsigned short)(65536 - ((long)256000000 / (channels * freq))) >> 8);
The docs say to use that formula to calculate the time constant and to use the high-byte of that value.

However, beside the wonky Porta to Note, the playback is actually quite good.
View attachment space.mp3

Any thoughts apperciated...
 
Hi.
When the tone portamento parameter is 0, use the previous value.
I think it is as simple as this.
The tone portamento is not executed during the ticks, only between
 
I am. I only change portamento speed when the effect parameter is greater than zero.

The code I posted earlier:
Code:
void _mod_update_row()
{
  MOD_NOTE* note;
  MOD_CHANNEL* channel;

  // i = the channel we're processing (0 to 3)

  channel = channels[i];

  note = &patterns[current_pattern]->rows[current_row].notes[i];

  // The channel->effect and channel->effect_param are set here.

  if (channel->effect <= 0x0f) {
    switch (channel->effect) {

      case MOD_EFFECT_PORTA_TO_NOTE : // 0x03
        if (note->period_index >= 0) {
          // when a period/note is defined then use that as the new target porta note
          channel->porta_index = note->period_index;
          channel->porta_value = _mod_tunings[channel->finetune][channel->porta_index];
          // starting on tick 0 so we can keep sliding on subsequent row updates
          channel->porta_tick  = 0;
        }

        if (channel->effect_param > 0) {
          // If an effect parameter was specified then use that parameter
          channel->porta_param = channel->effect_param;
        }

        break;

    }
  }

}
 
Can you display the period value to see what is the initial and final one ?
 
To test, I create test module with for example a portamento up in FT2 and I compare the result.
Test within a module is not simple.
Then, this isolate the channel and effdct from anything else. I did create tons of test modules like this
 
Can you display the period value to see what is the initial and final one ?
Hi. Yes, it goes from an F-1 (640) to an F-2 (320), a difference of 320. Before it picks up the next effect on channel 3 (see image in my original post) it has 5 rows * 6 ticks each to perform the porta, not enough.


This is some debug output of my player (channel 3 only):
Code:
[r:12] change effect 3, 8                 <- row #12, found effect 3 param 8
[r:12] porta_to_note 640 -> 320, 8 <- row 12 / tick 0, init porta, don't slide
[t:01] porta_to_note 632 -> 320, 8 <- tick #1, slide
[t:02] porta_to_note 624 -> 320, 8
[t:03] porta_to_note 616 -> 320, 8
[t:04] porta_to_note 608 -> 320, 8
[t:05] porta_to_note 600 -> 320, 8

[r:13] change effect 3, 8                 <- row #13, found effect 3, but param 0 so not changing.
[r:13] porta_to_note 600 -> 320, 8 <- row #13, keep sliding (not sure if this is correct, i.e. slide on tick #0 of subsequent rows or not?)
[t:00] porta_to_note 592 -> 320, 8
[t:01] porta_to_note 584 -> 320, 8
[t:02] porta_to_note 576 -> 320, 8
[t:03] porta_to_note 568 -> 320, 8
[t:04] porta_to_note 560 -> 320, 8
[t:05] porta_to_note 552 -> 320, 8

[r:14] change effect 3, 8
[r:14] porta_to_note 552 -> 320, 8
[t:00] porta_to_note 544 -> 320, 8
[t:01] porta_to_note 536 -> 320, 8
[t:02] porta_to_note 528 -> 320, 8
[t:03] porta_to_note 520 -> 320, 8
[t:04] porta_to_note 512 -> 320, 8
[t:05] porta_to_note 504 -> 320, 8

[r:15] change effect 3, 8
[r:15] porta_to_note 504 -> 320, 8
[t:00] porta_to_note 496 -> 320, 8
[t:01] porta_to_note 488 -> 320, 8
[t:02] porta_to_note 480 -> 320, 8
[t:03] porta_to_note 472 -> 320, 8
[t:04] porta_to_note 464 -> 320, 8
[t:05] porta_to_note 456 -> 320, 8

[r:16] change effect 3, 8                 <- row #16, found effect 0, param 0, not changing effect, staying on effect 3 with parm 8
[r:16] porta_to_note 456 -> 320, 8
[t:00] porta_to_note 448 -> 320, 8
[t:01] porta_to_note 440 -> 320, 8
[t:02] porta_to_note 432 -> 320, 8
[t:03] porta_to_note 424 -> 320, 8
[t:04] porta_to_note 416 -> 320, 8
[t:05] porta_to_note 408 -> 320, 8

[r:17] change effect a, 1 <- row #17, different effect found, stop porta.

So, the period ends on 408, the channel then switches to effect A01 (which I assume stops the porta to note). 408 doesn't seem/sound high enough to be correct.

So, to sum it up, the porta has 5 row to perform the porta, this is 5 rows * 6 ticks - 1 tick (first tick #0 that inits the porta). This is 29 ticks but to do a full porta from period 640 to 320 it needs 40 ticks, i.e. 320 / 8 = 40.
 
Last edited:
In mod master, the period move from 320 to 160 : incremented 8×5×4 Timed.
To have it working on your code, multiply the parameter by 2.
You should do the same for portamento, vibrato. I hear that the vibrato is not correct as well in your audio extract
 
In mod master, the period move from 320 to 160 : incremented 8×5×4 Timed.
To have it working on your code, multiply the parameter by 2.
You should do the same for portamento, vibrato. I hear that the vibrato is not correct as well in your audio extract
Hmmm. That is interesting. It is what I originally did, just messing about to see what would make it work. It made Space Debris sound good but other MODs would sound out of whack, but that could could also be because of other reasons. I will have another play around with this to see what happens. Thanks for the feedback!
 
With what you listed above, you increment it too much.
it is 5 Times per tick, not 5 then 6
 
Other mods that sounds bad may be because of the way you manage finetune
 
Oh, just saw that your target is assembly, for 8088/8086

May I ask you why ? :) Because it is not easy at all.
it took me years to have Mod Master at its current level of performance.
 
Back
Top