MODTracker audio replay on early 68k macs

Mu0n

Well-known member
The project looks really neat!
You've even made me attempt to install Retro68 after resisting it for several eternities, just because I want to check out the current state of your app!
I tried playing a nirvana.mod renamed to song.mod, but even at 75kb, it complained it didn't have enough memory. it's from the 2nd image you posted in the thread, hence my attempt to check the latest version at this moment.
 

MIST

Well-known member
On a 512k Mac? I somewhat wasted memory here and there. Since the last release I already reduced the workspace area of the "internal" axel-f that comes with my player. Removing it altogether should free even more space. Also the additional 64k i allocate as workspace for newly loaded mods may be more then required. So if you have retro68 up and running there's many things to try for you.
 

Mu0n

Well-known member
On a 512k Mac? I somewhat wasted memory here and there. Since the last release I already reduced the workspace area of the "internal" axel-f that comes with my player. Removing it altogether should free even more space. Also the additional 64k i allocate as workspace for newly loaded mods may be more then required. So if you have retro68 up and running there's many things to try for you.
I used a Mac Plus, running 6.0.8.

I tried to install Retro68 on my modern win11 PC inside a WSL window, followed a 2021 installation guide and I get show stopping errors during the process. I don't have a shred of Linux competence in my bones, so any small sidestep outside of an ELI5 guide is gonna thwart me.
 

Mu0n

Well-known member
on a whim, I tried it under mini-vMac and no issue about reading other mod files renamed as song.mod!
(my physical Mac Plus has 4Mb btw)
 

MIST

Well-known member
The physical Mac with 4MB was running out of memory? That doesn't make much sense to me. Especially if the same setup does work on MiniVMac.
 

Mu0n

Well-known member
The physical Mac with 4MB was running out of memory? That doesn't make much sense to me. Especially if the same setup does work on MiniVMac.
this is a direct screenshot from my Mac Plus:
1754165795787.png
 

MIST

Well-known member
The screenshot doesn't give any additional clue. You need to debug what's going on and why allocating memory from macos fails. Also that version is outdated. Please use the latest code from the GitHub.

For some reason you seem to expect that this is fully functional product. It's not. It's a proof of concept.
 

Mu0n

Well-known member
As soon as I figure out how to get Retro68 going on my win11 machine, I'm happy to provide more useful information.
I promise I'm not expecting this to be a fully functional product. I know this stage of development very well, most of my projects are right in that level.
 

MIST

Well-known member
If retro68 is not working for you guys, then I am sure there are other development environments. What are you normally working with?

You just need an assembler and a c compiler and that's it. The assembler part may need some porting but that's at most an hour of work. The source I started with was from a different assembler and I also had to port it first.
 

Snial

Well-known member
If retro68 is not working for you guys, then I am sure there are other development environments. What are you normally working with?

You just need an assembler and a c compiler and that's it. The assembler part may need some porting but that's at most an hour of work. The source I started with was from a different assembler and I also had to port it first.
I would normally use THINK C 5 for development. The nice thing is that it will run happily on a Mac Plus emulator in 4MB. However, it uses Motorola syntax for assembly rather than gas. For example, %d0 instead of d0. Also, it only supports inline assembly and doesn't support .rept.

I haven't given up on trying to use Retro68, I think I ought to be using it anyway, because it's getting popular. I'm getting closer to figuring out the problem I had, so I might get there before too long.
 

Mu0n

Well-known member
I finally built Retro68, and NanoMacTracker soon after.
here's some additions I fiddled with:

1) std file get dialog
2) during playback, shows the file name in the dialog
3) updates the StaticText dialog item to the chosen mod file, or AXELF.MOD if the file picking fails or returns nothing

Bugs I'm dealing with:
1) if you open a new mod while one is playing, the new song won't start at the beginning but rather the same time offset as you were in the previous song
2) for one .mod at least, I got an address error crash on my real Mac Plus - doesn't happen in mini-vMac. That's probably an odd-aligned byte address issue, a classic.


(terrible capture, I can do much better, but I wanted to just get a quick thing)
 

MIST

Well-known member
1) if you open a new mod while one is playing, the new song won't start at the beginning but rather the same time offset as you were in the previous song
Very cool!

This probably just requires some variables to be reset to zero ...
 

Snial

Well-known member
here's some additions I fiddled with:
1) std file get dialog
2) during playback, shows the file name in the dialog
3) updates the StaticText dialog item to the chosen mod file, or AXELF.MOD if the file picking fails or returns nothing

Bugs I'm dealing with:
1) if you open a new mod while one is playing, the new song won't start at the beginning but rather the same time offset as you were in the previous song
2) for one .mod at least, I got an address error crash on my real Mac Plus - doesn't happen in mini-vMac. That's probably an odd-aligned byte address issue, a classic.
Could you publish your changes? Either by forking the original and pushing that or by zipping the .c and .a?

I think it would be a good idea to not have a default .MOD, because if you don't want to play AXELF.MOD (as great as it is), then you're still wasting hundreds of kB.

I think bug (1) is some kind of race condition, so rather than just resetting the pointer, it'd be better to stop the current playback at the current point as soon as the menu option is picked or the load button is pressed; and then resume it if the file is cancelled or restart otherwise. And I'd stop it by:

  1. Turn the volume off (I think this is a VIA bit, but could be done via a toolbox call).
  2. Stopping the VBL (by unhooking it from the VBL list as that's an atomic operation). The current buffer will continue to play, but you can't hear it.
  3. Clear the hardware buffer (0x80). That way, when the new audio starts or if you quit, then there won't be horrible glitches.
  4. Load the new file (or Quit).
  5. If the load was successful, reset the song position.
  6. If it wasn't a Quit then turn on the volume and then restart the VBL (is this Muson?).
  7. If it was Quit, then restore the volume off/on bit (so it needs to be saved when the program is run).
-cheers from Julz
 

Mu0n

Well-known member
@Snial here you go: https://github.com/Mu0n/NanoMac
let me know if you want to be coauthor of this fork. Feel free to implement everything you posted about or more.

I changed
Str255 name;
uint8_t *data;
to globals for quick editing of the original project, this can be changed back or shoved into a permanent struct, whatever it is computer science types prefer to do.
 

Snial

Well-known member
Thanks! On GitHub I am also Snial.
I changed
Str255 name;
uint8_t *data;
to globals
Good idea. I think there’s some ambiguity when it comes to mapping Pascal’s Strxx types to ‘C’, because sometimes in Pascal is could cause storage to be allocated whereas at other times it’s just a pointer. Also, ‘C’ would expect zero-terminated strings meaning Str255 would have to be Str[257] because you need 255 bytes for the string + a ‘\0’ terminator + the length byte at the beginning. And although MacPascal (and I think THINK Pascal) had dedicated Strxx functions, they’re not part of the toolbox.
 

MIST

Well-known member
Feel free to put this into a separate repository just containing the mod payer. I'll then remove the code from mine and instead just point to yours.
 

Mu0n

Well-known member
Feel free to put this into a separate repository just containing the mod payer. I'll then remove the code from mine and instead just point to yours.
@Snial
if you're willing, do this on your end and feel free to invite me. You seem to have more fire lit up under you at the moment, but I'm glad to contribute minor stuff from time to time. I'm occupied by other projects right now.
 

Snial

Well-known member
@MIST, @Mu0n . I've been modifying the code a bit to free it from the built-in .mod file. I found that the assembly code in prepare probably isn't correct for the Mac.

Here's what happens. After Init, all the waveform pointers are stored in sample starts: DS.L 31 and the waveforms themselves start at the beginning of the waveform data in the .mod file and end at the calculated address from init: , stored in end_of_samples: DC.L . So, the mod_ptr memory block looks like this:

+---------+----+--------+---------+---------+
|ModHeader|Song|Patterns|Waveforms|Workspace|
+---------+----+--------+---------+---------^workspc


What prepare does is shift all the waveforms to the end of the memory block, initially leaving the equivalent workspace gap just after patterns.

+---------+----+--------+-------------------+
|ModHeader|Song|Patterns|Garbage...Waveforms|
+---------+----+--------+-------------------^workspc


Then it goes through all the Waveforms 0..31; copying each one back from the start of the original waveform area but expanding each waveform by its repeated sample section - what's called the Sample loop in synthesiser terminology. Since the waveform envelope of a waveform is always:

+---------(46)---------+----------------+
|Unlooped |LoopSection |NoNoteOffSection|
<--(42)--><----(48)---->----------------+


Then duplicating the loop section at the end of a waveform means that DMA can play off the end of a loop section for all the samples in a given tick and it'll work as if it had looped round.

There are a few bugs in the code that means it's quite possible for the expanded Waveforms to overflow the workspace and I guess that's what's causing the crashes when we allocate data for a mod file; whereas when the mod file is embedded in the CODE segment, if the workspace overflows, it's just overflowing into the unused heap space between the end of CODE and the return stack.

Whatever code changes we make, the Mac NanoTrack player needs to do pretty much the same thing, because the waveform pointer can only jump back at the end of the sample: function. It mustn't go off the end of a LoopSection into the next waveform. But because even an early Mac has well managed heaps; we shouldn't just guess the workspace needed; we should calculate it properly. We should be able to do that in 'C', because it's not timing-critical.

Also, the proper way to do it is to read in the mod header (which is quite short). We can allocate that to a handle and move it hi. We parse the header to find out how much actual waveform space is needed + the repeated looping sections. Then do a NewPtrClear for all of that; read in each waveform in turn and fill in the correct SampleStart, which is what init does.

How much duplicated LoopSection is really needed? If we look at the table from this spec URL we get these periods:

C C# D D# E F F# G G# A A# B
Octave 1: 856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453
Octave 2: 428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226
Octave 3: 214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113

Octave 0:1712,1616,1525,1440,1357,1281,1209,1141,1077,1017, 961, 907
Octave 4: 107, 101, 95, 90, 85, 80, 76, 71, 67, 64, 60, 57


Amiga sample rates are PAL = 7093789.2/(2*periods) NTSC=7159090.5/(2*periods).

On the Mac, increment rates are: INC=(3579546/(60*LEN)*65536) = 161*65536 =10,551,296 or 0xA10000. So, for a start, it's not correct, because it should be: 10,567,078 or 0xA13DA6. Anyway, the lowest frequency in the frequency table is 0x30, which means that the sample increment for that is: 3.354171753.

So, at most we can have an extra 370x3.354171753=1,241 samples read at the end of a waveform. This needs to be rounded up to an even number so, in turn that means the maximum workspace is around: 1,242*31=38,502 bytes.

I think a suitable replacement for the assembly prepare function will look like this:

C:
#define kModWaveNameLen (22)
#define kModWaveFormsMax (31)
#define kWaveExpansion (1242)

typedef struct {
    char iName[kModWaveNameLen]; //22
    uint16_t iWordLen;        //2
    uint8_t iFineTune;        //1
    uint8_t iVolume;        //1
    uint16_t iLoopStartWords; //2
    uint16_t iLoopLenWords;    // 2
}tModWaveHeader; // 30 bytes.

void ModWaveExpand(tModWaveHeader aWaveInfo[], uint8_t *aWaveStarts[], uint8_t *aWavesEnd)
{
    int16_t wave=kModWaveFormsMax; // Start at last waveform.
    uint32_t len,loopLen;
    uint8_t *loopFrom;
    while(wave>=0 && aWaveStarts[wave]==NULL) {
        --wave;
    }    // find last wave.
    for(; wave>=0; wave--) {    // searching backwards.
        len=ModWordsToBytes(aWaveInfo[wave].iWordLen);
        BlockMove(aWaveStarts[wave],aWavesEnd-len-kWaveExpansion,len);
        aWaveStarts[wave]=aWavesEnd-len-kWaveExpansion;    // update waveStarts.
        aWavesEnd=aWaveStarts[wave]+len;
        loopLen=aWaveInfo[wave].iLoopLenWords;
        if(loopLen!=0) {    // fill expansion with repeats of loop.
            len=kWaveExpansion; // bytes to fill.
            loopFrom=aWaveStarts[wave]+ModWordsToBytes(aWaveInfo[wave].
                                iLoopStartWords);
            while(len) {
                if(len<loopLen) { // just copy the remaining len
                    loopLen=len;    // bytes on last pass.
                }
                BlockMove(loopFrom,aWavesEnd,loopLen);
                aWavesEnd+=loopLen;
                len-=loopLen;
            }
        }
        else {  //Non-loop case:
            memset(aWavesEnd, 0, kWaveExpansion); // clear expansion area
        }
        aWavesEnd=aWaveStarts[wave];
    }
}

So, I intend to eliminate the init and prepare assembly functions and use this and my replacement for init.
 
Last edited:
Top