• Hello MLAers! We've re-enabled auto-approval for accounts. If you are still waiting on account approval, please check this thread for more information.

MODTracker audio replay on early 68k macs

I fully agree and I also think that this really needs some major tweaking to be satisfying. And yes, synthesizing a full 370 samples in one run makes this loose too much granularity and even worse sometimes doing it only after 740 samples is even worse.

My intention was to prove that this can be done at all. Thus I was satisfied when things worked well enough to see that this was fact possible.

I am no classic Mac owner and writing software for it using the toolbox takes me forever since I don't even know how a proper macos program is s supposed to work, look and feel. Would I need a new mod file type? How to properly select files? Would I need a creator id? I don't even know all that ...

So now it's up to one of you to make this a nice little usable program ... or not, if there's not enough interest ...
 
I fully agree and I also think that this really needs some major tweaking to be satisfying. And yes, synthesizing a full 370 samples in one run makes this loose too much granularity and even worse sometimes doing it only after 740 samples is even worse.
It's OK, it sounded great when I played it.
My intention was to prove that this can be done at all. Thus I was satisfied when things worked well enough to see that this was fact possible.

I am no classic Mac owner and writing software for it using the toolbox takes me forever since I don't even know how a proper macos program is s supposed to work, look and feel. Would I need a new mod file type? How to properly select files? Would I need a creator id? I don't even know all that ...
There used to be a central point for obtaining new creator codes and file types (APDA), but these days I think people just make them up using a combination of lower and upper case letters (or symbols). We could call the file type 'MOD4' and app, maybe 'mstM' or 'MstM' (for MIST Mod tracker) or 'WzMT' (for WizzModTracker). I think the last one is my favourite.
So now it's up to one of you to make this a nice little usable program ... or not, if there's not enough interest ...
Oh, there will be enough interest. I have a little audio app I've written; I could probably turn that into a simple MOD tracker (it'd take me a bit of time though, as I split myself between too many projects).

-cheers from Julz
 
Proper synchronisation means making sure music is called every 22250/50=445 (89x5) generated samples. Yet we need to generate 370 samples per frame as an unrolled loop. The "easiest" way, IMHO is to split the unrolled loop into a separate subroutine (Samp1Gen) and then be able to JSR into the unrolled loop up to two times.
That's the key. As mentioned before, the Mac doesn't run at exactly 60Hz and the sample rate isn't exactly 22250 Hz. But I think it's absolutely fine to assume it is 60Hz. And we have 370 samples per VBL. If we want to run the music evaluation at 50Hz we'd need to run music every 370*60/50 = 444 samples. That's pretty close to the 445 you determined. But 444 has a big advantage: It's 6*74 while 370 is 5*74. That means we can run the sample generation for 74 samples and six times per VBL resulting in 370 samples to be calculated. And if we run the music routine at the right moment between these six runs, then we'd get a perfect 50hz frequency for the invocation of the music routine. This actually shouldn't be too complex.
 
Major remaining issue now is that many mods crash during initialization ...
Compare them to mods that don't play properly on Sound Trekker; there's a lot of "MOD" files that aren't actually 4-channel or have sounds sampled at the wrong frequency or are too big to load.
 
Major remaining issue now is that many mods crash during initialization ...
Compare them to mods that don't play properly on Sound Trekker; there's a lot of "MOD" files that aren't actually 4-channel or have sounds sampled at the wrong frequency or are too big to load.
OK, I've run it now. Sounds pretty good. It's quite instructive that using actual sampled sounds makes it sound a lot less chip-tune and far more like a multi-tracked piece. That is, it's harder to identify the individual tracks which means that it often sounds like there's more than 4 tracks going on. I suspect that drum tracks are usually combined, because there's relatively few combinations, like a hi-hat by itself or a hi-hat + snare on a given beat (that'd only be two fairly short samples); then combinations of that with a tom would still be very feasible.

Axel-F has 4 main tracks anyway. There's the drum track (which we can combine) + bass + main tune. The main tune is usually monophonic, but the second main tune sequence is duo-phonic with notes that are usually a 5th apart. So, that can be merged into a single waveform (though not a single wave cycle as a fifth isn't an integer multiple frequency of the root note on an even tempered scale). This means there's usually room for a counterpoint melody or effect sound. And because it's not in stereo, it's harder to count the individual tracks. Upshot: convincing.

Your code appeared to define 6 PARTS (which intuitively sounds like it means 6 voices), but of course, there's really only 4 voices, because PARTS was/is only used to determine whether to skip the call to music. At first I thought up to 8 voices could be defined, but the wizxlc: data structures are different to the audxlc: data structures - they're not doing the same things.

There's a warning about the amount of workspace needed for a MODule; which they say they've set at 16kB; and some MODules need more which the code says could lead to a crash.
 
Heh; that brings back memories; Axel-F was one of my test MODs back in the mid-to-late-90s, precisely because of how it used the voices. I had a reference output waveform and compared what the various trackers did to add "depth" and "space" -- tested the same set of MODs, ITs, S3Ms, FMs, etc. across Mac, ST, Amiga and PC on a variety of trackers. Mostly used AmiNet MODs from the competitions. Some guys like Victor Vergara went on to re-release their music as MP3, professionally mastered.

I also have a set of MODs that I intentionally down-mixed at the time to four voices of max 16kb size so they'd play on everything available. I should see if I can dig up that set; I'm pretty sure I've still got it around somewhere.
 
Major remaining issue now is that many mods crash during initialization ...
OK, I'm trying to turn your app into a proper classic Mac OS app, but the repo doesn't have a link to the:

.include "../axel_f.mod.s"

in wc_modMac.s .

Of course, I could pick up a version of axel_f.mod, but if it's not the same one, I can't be sure if my code is correct.

-cheers from Julz
 
Major remaining issue now is that many mods crash during initialization ...
I've been trying to build Retro68 for ARM64 on my MBA M2, but after much effort I ended up with:

Code:
esourceFiles.a[5](ResourceFile.cc.o)
  "boost::filesystem::path::find_parent_path_size() const", referenced from:
      boost::filesystem::path::parent_path() const in libResourceFiles.a[5](ResourceFile.cc.o)
  "boost::filesystem::path::stem_v3() const", referenced from:
      ResourceFile::assign(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, ResourceFile::Format) in libResourceFiles.a[5](ResourceFile.cc.o)
      ResourceFile::write() in libResourceFiles.a[5](ResourceFile.cc.o)
      ResourceFile::write() in libResourceFiles.a[5](ResourceFile.cc.o)
ld: symbol(s) not found for architecture arm64
clang++: error: linker command failed with exit code 1 (use -v to see invocation)

Of course, since all the commands are buried somewhere in scripts that drive scripts, that drive scripts, that drive scripts, that drive scripts, etc in a massive mountain of spew I can't use -v to see the invocation. Though I guess it's probably some kind of x64 vs aarch64 screw-up. Odd, because the pce-mac emulator builds fine on ARM64.

How I hate modern development environments for this kind of awfulness. Installing THINK C on a Mac via 4 Floppy disks in 1992 was a doddle.
 
Ironically it might not work on an emulator, which might just copy the Hw buffer after each frame, though I'd have to check this.
Current MAME at least does fetch the samples and disk PWM data per-scanline during the live raster for the 128/512/Plus/SE, and I have some stuff queued up to make the output filtering more accurate to Burrell's.

Having done it when I was young and stupid, half of writing a MOD player is determining what kind of MOD it is - early SoundTracker, late SoundTracker, ProTracker, the PC trackers, etc. On the Amiga there was famously a discontinuity between MODs where the sample size was in bytes vs. in words, but that's relatively easy to work out. It can definitely cause crashes if you blindly assume the later word convention.
 
Current MAME at least does fetch the samples and disk PWM data per-scanline during the live raster for the 128/512/Plus/SE, and I have some stuff queued up to make the output filtering more accurate to Burrell's.

Having done it when I was young and stupid, half of writing a MOD player is determining what kind of MOD it is - early SoundTracker, late SoundTracker, ProTracker, the PC trackers, etc. On the Amiga there was famously a discontinuity between MODs where the sample size was in bytes vs. in words, but that's relatively easy to work out. It can definitely cause crashes if you blindly assume the later word convention.
I'm not sure yet which MOD file types are supported by @MIST 's player. I'm fairly familiar with MIDI files, but not MOD files. Since, ultimately they're derived from Amiga sound hardware, it seems like they're not that sophisticated. I found a concise description here:


It's better than the earlier one I found here which has some stuff missing:

 
The ultimate reference is the ProTracker 68000 source, because there's a fair amount of corner-case behavior that certain MODs rely on. But that's only correct for newer MODs that were made on ProTracker rather than SoundTracker.

There's a famous (and very good) MOD called "Klisje Paa Klisje" that sets a tempo of 0x20 at one point. In SoundTracker, that just meant "update every 32 timer ticks", but in ProTracker that means "set timer to 32 BPM" which is quite different.
 
The ultimate reference is the ProTracker 68000 source, because there's a fair amount of corner-case behavior that certain MODs rely on. But that's only correct for newer MODs that were made on ProTracker rather than SoundTracker.

There's a famous (and very good) MOD called "Klisje Paa Klisje" that sets a tempo of 0x20 at one point. In SoundTracker, that just meant "update every 32 timer ticks", but in ProTracker that means "set timer to 32 BPM" which is quite different.
Yeah; that used to be one of my test MODs back in the day :) And I was always annoyed that ProTracker just up and changed the definitions of things, even if the changes were generally for the better. Most of my collection was pre-ProTracker and the lack of backwards-compatibility was rather frustrating.

I do recall using a tracker (maybe it was ProTracker with a plugin?) to take all those old files and re-write them assuming a real time clock, voice substitution, a larger bit field, etc. -- I seem to recall scripting some automation to convert everything. Some stuff worked great, some of it sounded horrible; I had to listen through the entire set to tell the difference. Ah, those were the days.
 
The ultimate reference is the ProTracker 68000 source, because there's a fair amount of corner-case behavior that certain MODs rely on. But that's only correct for newer MODs that were made on ProTracker rather than SoundTracker.

There's a famous (and very good) MOD called "Klisje Paa Klisje" that sets a tempo of 0x20 at one point. In SoundTracker, that just meant "update every 32 timer ticks", but in ProTracker that means "set timer to 32 BPM" which is quite different.
[ and @MIST ]. I've been looking at the assembler code a bit more, to turn this MOD player into a proper Mac app. This means I'll end up using things like NewPtr to allocate sample data and MOD data and being able to read in a MOD file properly.

I tend to find there's ambiguity with the term "sample", because it can mean an entire sampled waveform, or an individual sample value within that waveform. So, I'll use the term "waveform" to mean a full sampled waveform and "sample" to mean an individual sample value within it. I'll use "wave-cycle" to indicate a set of samples within a waveform that constitutes a complete cycle at the current pitch. So, for example, if a waveform doesn't have a constant pitch envelope, then wave-cycles vary throughout the waveform.

Without trying to repeat the spec in the earlier link too much, a Pro-tracker MOD music file has 4 major sections:
  1. A header containing information about the song and Instrument definitions (which consist of pointers to waveforms, their lengths, up to 64K x 16-bit words, some fine-tuning (+/-8/8 of a semitone), loop markers and master volume levels.
  2. Song data, which consists of a fixed-length 128 byte array containing indexes to one of 64 patterns.
  3. Patterns, each of which consist of, seemingly a fixed-length 64 element 2D table [Step][Voice], each of which is 4 bytes that specify the instrument to be used; a 12-bit pitch value (C'1' =856); and a 12-bit effect command.
  4. Waveforms which consists of signed 8-bit samples up to 128kB each (64K words).
The MOD tracker code mimics this structure, so there's code to handle waveform generation, which has to be mixed down to a 370 sample buffer per 60.15Hz Mac frame, which is the routine at nomus and the higher level routine, called music which sets up the waveform generation for each frame. So let's look at the core routine first, which isn't labelled, but is wrapped with a .rept LEN . I think @MIST will have changed a couple of things in the algorithm now, but it won't change the fundamental code.

I find the gas convention of preceding a register name with '%' somewhat annoying since registers are used most of the time (rather than labels) so everything gets peppered with multiple '%'s.

Anyway, on entry:
  • D0.w contains the current position in the waveform for voice 0. This could be an error, because a waveform can be up to 128kB.
  • D1.w contains the current phase position for voice 0, i.e. the fraction of a position.
  • A0.l contains the pointer to the beginning of the current waveform for voice 0. It doesn't seem like the pointer is updated.
  • A2.l contains the pointer to the volume adjustment for the current waveform for voice 0, which must be in the range -64..63.
  • D4.w, D5.w, A1.l, A3.L correspond to D0.w, D1.w, A0.L, A1.L for voice 1.
  • D3.b is a temp, containing the raw sample value read from a waveform at the current position.
  • D7.b is a temp, containing the volume-adjusted sample for voice 0, subsequently mixed with the volume-adjusted sample for voice 1.
  • A6.L points to the 370b audio buffer (which then gets copied into the actual Mac's hardware audio buffer at the beginning of the VBL flyback, but not shown here).

Code:
    add.w    %a4,%d1                #Current phase V0 (V2, 2nd pass)
    addx.w    %d2,%d0                #Current position V0 (V2, 2nd pass)
    add.w    %a5,%d5                #Current phase V1 (V3, 2nd pass)
    addx.w    %d6,%d4                #Current position V1 (V3, 2nd pass)
    move.b    0(%a0,%d0.l),%d3    #Sample[Pos[Voice0]] at full vol.
    move.b    0(%a2,%d3.w),%d7    #Attenuate volume
    move.b    0(%a1,%d4.l),%d3    #For Voice 1
    add.b    0(%a3,%d3.w),%d7    #Attenuate volume for Voice 1 & add.
    /* Sample audio is represented as a signed byte, -128 to 127.
       To convert to unsigned we need to add 128=> 0 to 255, which is
       equivalent to eor.b #0x80.
     */
    eor.b    #0x80,%d7       /* convert to unsigned */
    lsr.b   #1,%d7          /* divide by two to allow adding of second channel */
    move.b    %d7,(%a6)+        #Finally, write the sample to the buffer.

We can see that the routine handles 2 voices (channels) at the same time. The length is 32b, so the whole length is 11.5kB. 16c+56c+22c =94 cycles per loop or 34,780 for 2 voices. This basic loop is then repeated for the next two voices making 69,560 total per frame. In addition, the data transfer from the mixed 370b buffer to the Hardware buffer takes 102 cycles * 45+28 cycles = 4,618; 74,178 cycles per frame or 4461806.7 cycles per second, roughly 69% of CPU, if an original Mac can manage 6.5M cycles/s on average.

One, obvious improvement that can be made would be to only unroll the loop once and repeat the code for voices 2 and 3 instead of a second whole repeat. This would cost very little in terms of setting up the pointers, however, the move.b %d7,(%a6)+ at the end would become add.b in both cases, requiring the buffer to be cleared at the beginning of each frame (e.g. when copying to the hardware buffer), which would cost another move.l dn,(an) * 45 = 540c or 0.5% of CPU.

The next post, I think I will discuss the song and pattern playing code; what tables are defined and how they're used.
 
I've been wanting to make an accurate 22.5kHz 16-bit mixing 4-ch MOD player for Motorola 68000 Macs for a long time.
I already have an 68000 assembler project for 68k Amigas which loads a module, uses the PT2 replayer code and emulates Paula (Amiga sound chip) to play back modules accurately in 100% software. I wanted to port this over to work on my Macintosh SE, and I will probably do it at some point. Maybe having a peek at NanoMacTracker's VBL/audio-buffer code will make me inspired enough to get to it. :)

One thing is to just create another player as a "proof-of-concept", but what we really need (IMO) is an accurate one that plays back PT MODs like they should. Not trying to sound cocky, I love the NanoMacTracker project already!

Btw, you don't really want to tick the replayer at 50Hz, as this eliminates the possibility for tempo (BPM). A lot of MODs used that. Instead, like written earlier, just count N output samples then tick the replayer, where N is derived from the output audio rate and the MOD BPM. MOD has both "speed" (ticksPerRow) and "tempo" (tickDuration aka. BPM), where "speed" is the amount of ticks to delay before going to the next row, while "tempo" is the duration of one tick.
 
Last edited:
IBtw, you don't really want to tick the replayer at 50Hz, as this eliminates the possibility to change the tempo (BPM). Instead, like written earlier, just count N output samples then tick the replayer, where N is derived from the output audio rate and the MOD BPM. MOD has both "speed" (ticksPerRow) and "tempo" (tickDuration aka. BPM), where "speed" is the amount of ticks to delay before going to the next row, while "tempo" is the duration of one tick.
The Mac's hardware audio buffer ticks at 60.15Hz, which means a VBL task at that frequency is certainly needed. If you wanted to 'tick' the replayed after N samples, you'd need to manage that within the VBL period as per my post here:


In my SampSplit description I assume that music is called every 445 samples, which is 20ms. However this could be dynamically adapted to longer or shorter periods.
 
Yup, was thinking that one could have a VBL interrupt routine solely used for counting tick samples, ticking the replayer after N samples (for supporting MOD BPM), mixing audio, and transferring the block to the correct location in the hardware audio buffer.
 
Btw, you don't really want to tick the replayer at 50Hz, as this eliminates the possibility for tempo (BPM). A lot of MODs used that. Instead, like written earlier, just count N output samples then tick the replayer, where N is derived from the output audio rate and the MOD BPM.
You should really take a look at the published code and read the previous postings. The replayer does run every 444 samples and not at 50hz. In fact this results in the replayer being run at nearly 50 hz or to be precise at mac-vbl-rate*370/444. It does that inside the vbl handler as the vbl handler is also dealing with filling up the hardware queue and doing this separately would require an additional buffer.

I have updated my player a little bit. The Wizzcat player routine I am using has a limitation here and there and a new preprocessing routine deals with problematic files before the Wizzcat routine is being called.
 
Back
Top