Ah yes, sorry, Tritone has indeed only global tempo. Also, yes, speed value = row length, and that should be bne play_note.
I'll have to dig deeper, then. Hope I'll have some time tomorrow.

I was actually thinking you might want to bypass 1tracker and just dump directly to asm data, so you can save one conversion step and design the data format and loader however you see fit.

Spotted another one - you're missing the row length counting, ie.

    dex                         ;2  row length low byte
    beq .play_note              ;3 -- 95 ~ 21053Hz (original is 22876Hz)

    dey                         ; row length hi byte
    beq .play_note

where X should be 0 on init, and Y should be whatever Tritone spits out as row length. I don't have Tritone's data format at hand right now but iirc it should be the first byte of a pattern row.

One thing I spotted skimming through quickly is that you're loading a drum marker per tone channel, but there's only one global drum channel in Tritone.

Wow, didn't know Stephane Picq was still active yikes

My French hasn't gotten any better over the years so I'm afraid listening to that much of it in one go will make my head explode, but somebody should pester these guys to play some modern 1-bit stuff.

http://garvalf.free.fr/var/upload/musiq … ectrum.1tm

Alright, so the gist of (equal volume) Tritone is basically this:

    ldx #$0                     ;   reset row length counter lo byte

.play_note
    clc                         ;2  update osc
.ch1_div_lo
    lda #$0                     ;2
.ch1_acc_lo
    adc #$0                     ;2
    sta .ch1_acc_lo+1           ;4
.ch1_div_hi
    lda #$0                     ;2
.ch1_acc_hi
    adc #$0                     ;2
    sta .ch1_acc_hi+1           ;4
.ch1_duty
    cmp #$0                     ;2  compare against duty threshold
    sbc .ch1_acc_hi             ;4  A = 0 on low half-cycle, FF on hi half-cycle
    and #%00001111              ;2
    sta SYSVIA_ORAS             ;4

    clc                         ;2
.ch2_div_lo
    lda #$0                     ;2
.ch2_acc_lo
    adc #$0                     ;2
    sta .ch2_acc_lo+1           ;4
.ch2_div_hi
    lda #$0                     ;2
.ch2_acc_hi
    adc #$0                     ;2
    sta .ch2_acc_hi+1           ;4
.ch2_duty
    cmp #$0                     ;2
    sbc .ch2_acc_hi             ;4
    and #%00001111              ;2
    sta SYSVIA_ORAS             ;4

    clc                         ;2
.ch3_div_lo
    lda #$0                     ;2
.ch3_acc_lo
    adc #$0                     ;2
    sta .ch3_acc_lo+1           ;4
.ch3_div_hi
    lda #$0                     ;2
.ch3_acc_hi
    adc #$0                     ;2
    sta .ch3_acc_hi+1           ;4
.ch3_duty
    cmp #$0                     ;2
    sbc .ch3_acc_hi             ;4
    and #%00001111              ;2
    sta SYSVIA_ORAS             ;2

    dex                         ;2  row length low byte
    beq .play_note              ;3 -- 95 ~ 21053Hz (original is 22876Hz)

    dey                         ; row length hi byte
    beq .play_note

If you want to use unequal volumes (which could definitely come in handy), you need to shift the outputs around a bit somehow. In the original unqual volume, the outputs are distributed at a ratio of 34:49:70 cycles.

As far as data reading goes, I didn't bother since you're probably better off just design your own data scheme.

Noise mode can be tacked onto this design easily by adding

    rol
    adc #0

after adding chx_acc_hi, and using #$2174 as frequency divider value. The duty setting will act more or less as a volume control on that. Or tack on the PWM sample code from SquatM, but beware that this engine is overall much quieter, so PWM samples at full blast may be too loud. Speaking of quiet, I think this will be the main problem with this code - it'll probably be too quiet on the SN, so you might want to write to all 3 tone channels instead of just one. Or even better, output each channel on its own SN tone channel.

As to why the sound is so noisy/muffled, I don't know enough about the beeb to help you there. Are there any waitstates/badlines/whatever it's called to be aware of?

Btw, that comment on youtube suggesting to use the shift register is a good lead, I think. Not necessarily as a hardware mod like the guy is suggesting, but as a means to offload some processing.

negative charge wrote:

The BBC Micro uses an SN76489.  The Acorn Electron has no dedicated hardware for sound - it uses a ULA.  I can adapt the code for the Electron fairly easily once there's a working SN76489 version.

Ok, now I understand your plan. I was kind of betting on being able to just use SN volume to render PWM frames, but so this has to be done the proper 1-bit way.

negative charge wrote:
utz wrote:

Also, am I correct in assuming that I could just keep pumping out DATA bytes (LATCH = 0) to set the volume of a given channel?

That's correct - it's pretty much what the SquatM engine is doing:

    ora     #%10010000         ; bit-7 (latch/data), bits-6/5 (channel - 0), bit 4 (type - latch volume)
    sound_write_slow             ; output sound bit

Sorry, I meant taking this even further: only ora #%10010000 at the beginning, and then just use the active latch to set the lower 4 bits, without needing to set channel and type again. Though considering how quiet the SN is at the 50% base volume we get by setting period to 1, it might be necessary to actually set multiple channels at once.

Ah, so we're targetting SN76489. I assumed there might be some sort of native device.

Would it be acceptable to leave Port B I/O in WRITE mode and only switch to READing between pattern rows? That would speed up things quite a bit.

Also, am I correct in assuming that I could just keep pumping out DATA bytes (LATCH = 0) to set the volume of a given channel?

So, regarding optimization options, 1tracker has a row optimization thingy that you can access with Ctrl-F4. That'll only save you a couple of kb though.
Otherwise you'd need to write your own optimizer that breaks up 1tracker's long single pattern into a sequence of smaller chunks, emitting duplicates. If you can afford to unpack into a large buffer at runtime, you could also just compress the player+music data. Something like ZX0 usually does a pretty good job for that.

Btw, Empty 512 Bytes sounds surprisingly convincing, despite such crude limitations. You would probably get much further with a Tritone-like engine, though. @xxl to the rescue, perhaps you could share your GTIA port? Otherwise, it shouldn't be too difficult to port the z80 code. If you give me a summary on how things work on the BBC Micro side (ie. how to output sound, disable interrupts, available zp locations, standard compile address, etc), I could try to whip up a rough 6502 draft. Will take a few days before I get around to that, though.

Hello there bataais, welcome aboard! I'm liking a lot what I'm seeing on your blog. Our forum is mostly about music as you can see, but I'm very interested to hear more about your video projects as well.

Hi, I'm pretty pressed on time atm so will try to answer in more detail in the coming days. For now just a couple of quick replies:

negative charge wrote:

I'm wondering if there are options to optimize this file automatically, as compiled with player is over 27kb (out of 32kb available)?

No automatic options, I'm afraid. Generally it is possible to optimize the song data though.

negative charge wrote:

I'm also interested in whether any of the other engines have been ported to the 6502 (or variants) with source code available?  My Z80 knowledge to do this myself isn't up to the task.

Ports exist, but none with source available.

negative charge wrote:

Finally, a long shot but does anyone have ideas on how to convert SN76489 LFSR noise to SquatM noise/percussion? (fixed rather than tuned).

Eh, finally some good news. Yes, this can be done. If you can generate the bitstream from the LFSR, you can convert it to SquatM's PWM sample format, which is basically just a bitstream with run length encoding. So eg. %1111000000011000000111000011 becomes 2,4,3,6,2,7,4,0 - the final 0 is the end marker.

37

(2 replies, posted in Sinclair)

Pah, kids and their hipster gadgets nowadays big_smile

There are plenty of z80 instructions which are neither 4 cycles nor a multiple thereof, so that statement is kind of bollocks. Sampling every 4 cycles should still be good enough though, since most beeper engines align output to 8 cycles. To do things properly however one should sample at every clock cylce, and then apply a low-pass at half the sample rate of the emulated output.

38

(5 replies, posted in Sinclair)

I could also pre-generate the tables, if that helps.

39

(6 replies, posted in Other Platforms)

Shiru wrote:

Another weird method of controlling a speaker is found in the 8080-based Soviet DIY computer RK86. It lacks any interrupts whatsoever, so EI/DI control the speaker.

The PTC Sol-20 Music System is another (very early) example of this. Coincidentally it's also an 8080-based machine, though the Music System itself is an S100 card.

bushy555 wrote:

I have since experimented with 1k2b but unfortunately didn't get very far

On ZX81 it's actually a little more complicated since you write to port #ff, but read from port #fe. So if you comment out lines 64-66 and 85-87, and change line 206 to 'ld bc,#01fe', you should be nearly there, save for the drums. Really, don't torture yourself with 1k2b though. that's a horrible piece of code. Funnily enough, these new engines I made (ulasyn and PhaserF) should be fairly straightforward to port.

40

(6 replies, posted in Other Platforms)

Ah, that's similar to how it works on ZX81, which isn't surprising considering the machine's history. A rather inconvenient setup for 1-bit music, but not impossible. You'll probably want to write your own engine for this though, rather than porting an existing one.

Just how many different machines do you have in that barn of yours, though? yikes

41

(5 replies, posted in Sinclair)

What would be the challenge for adding it to 1tracker? The data format is quite similar to ulasyn's. In terms of interface, it could be shown as 4 channels, perhaps calling them 1A, 1B, 2A, 2B.

Btw forgot to mention in the readme that row length is 6-bit only, due to using the I register for counting.

42

(5 replies, posted in Sinclair)

As promised, here it is, Phaser with filters.

- 2 Phaser channels, XOR/OR/AND mixing
- variable duty for both oscillators (channel 2 only)
- volume control (up to 6 levels)
- filters with variable cutoff (6 levels for lo-pass, 5 levels for hi-pass)
- sample rate 9114 Hz
- interrupting PWM sampled drums at 27343 Hz with 7 pitch and 3 volume levels
- player size 3333 bytes (when assembled at a 256b border)

Channel 2 can be split into two independent pulse wave channels that are mixed
Squeeker-style (OR mixing). Beware that changing channel 2's mixing mode is slow
(~200t), so avoid doing so if you are concerned about transition noise.

source code

This one was quite a challenge, and I'm not entirely sure if it was worth the effort. Somehow it sounds muddier than ulasyn. Part of it is because length counter updates are slower, but there might also be a bug lurking in one of the volume cores. I've checked them all multiple times in various ways though, and can't spot any obvious culprits.

PWM drums are slightly different from ulasyn - higher sample rate, but less volume resolution. There's no noise mode in this engine, so the samples need to make up for it.

43

(9 replies, posted in Sinclair)

Squashed one more bug today. Also tried to implement resonance, but either I'm doing it wrong, or the volume resolution is insufficient to do it. In any case, this should be it, not planning to do more work on this in the near future.

Would be great to have it in 1tracker. I'm not adding it to Bintracker yet, because the engine plugin format still isn't stable.

44

(9 replies, posted in Sinclair)

Fixed some bugs, and found a way to properly calculate the cutoff 1.5 tables (by doing level 2 on a rising edge and level 1 on a falling edge). In theory this could be extended to other intermediate levels, doubling the cutoff resultion. However the filter response is such that the perceived difference between eg. level 2 and 3 is actually smaller than between 1.5 and 2, so I didn't bother implementing any more *.5 tables. Users can add their own filter tables if needed.

45

(9 replies, posted in Sinclair)

Aaaand voilà. Volume control implemented. Lower volume means less precision for the filters, of course. Updated source is on github, see above.

Now what I would really like to have is resonance. Not sure if it's actually possible with this setup. In any case I can't wrap my head around the math of it.

46

(9 replies, posted in Sinclair)

Ha, last night I realized that volume control is actually possible with this engine. It just requires lots of additional filter tables, but that can be done without increasing the code size.

Anyway, thanks guys.

Ah, L/C/R panning definable in software, excellent. Like the idea of a RISC V MCU, too. How does it fare in terms of price, though? Mouser claims they're selling a ESP32-C3 for around 1€ over here, but pretty much any other seller is asking around 20€.

Yes, that all makes a lot of sense. Also, damn, I keep forgetting that AVR is a Harvard arch.

About the sound system, my main concern at this point would be that it becomes too powerful. 8x1-bit with auto-reset would be wild, of course, but at some point it becomes tempting to treat the whole thing as a quirky kind of DAC and stream PCM on it, similar to how it works on RaspPi.

Btw, any plan for how to implement WiFi yet? I imagine this would add quite some complexity, in addition to possibly needing to introduce not-so-open components to the design.

It all sounds great to me. Unfortunately I can't be of much help with this, since I'm not versed in hardware design at all.

Regarding the sound system, your second idea appeals most to me, as it offers a lot of flexibility while being straightforward and simple to conceptualize. Buffered beeper would be a bit like if PET shift reg would actually work as described. Very powerful, but would lead to a completely different way of writing engines.

One thing that would be very desirable is proper audio plugs, as in RCA or big jack. Small jacks are an absolute nightmare when playing live. And I do want to play this thing live.

It might also be worth considering whether to use a modern instruction set instead of Z80, eg. AVR? Personally my heart beats for Z80, of course, but using AVR might make this device more accessible for programmers that aren't deeply rooted in the 8-bit world.

Regarding configurable emulation rate, this would be great to have exposed as a knob on the hardware.

50

(9 replies, posted in Sinclair)

tldr: 2 pulse channels with lo-pass/hi-pass filters with somewhat variable cutoff.

- 2 pulse wave channels
- variable duty cycle
- duty cycle sweep with variable speed
- noise mode (channel 2 only)
- filters with variable cutoff (6 levels for lo-pass, 5 levels for hi-pass)
- sample rate 9114 Hz
- interrupting PWM sampled drums at 18229 Hz with 7 pitch and 3 volume levels
- player size 3333 bytes (when assembled at a 256b border)

source code


Some points of interest:

- The filter implementation itself is pretty boring, just a lookup into a 14-byte table with values for each pair of (current volume, next raw channel state).

- Noise mode uses a new technique. The engine has one version of the main loop for each volume level, so using self-modifying code to enable/disable the effect was not an option. After just randomly experimenting, I ended up with

  ; hl is oscillator state
  ld a,(MASK)
  and h
  rlca
  xor h
  ld h,a

At 29t it's rather expensive, but I really wanted noise mode and could spare the cycles. Depending on MASK, the divider and the duty cycle, this can produce many wildly different sounds. I found that using a mask of 0x76 and a divider of 0x2712 works well to fake standard white noise, with duties from 0x30-0xc0 being somewhat usable for simulating volume levels. Unfortunately the filters are a bit to crude to really work well on noise.

- Using a rather low sample rate for the PWM drums here. Not sure if that's such a great idea, but hey, we've got noise mode to make up for the low upper frequency limit. Originally I wanted to do synth drums instead (using the new noise mode of course), but in the end PWM is much more convenient for the composer.

- Still got some cycles left, so this can be pushed further. Phaser with filters would be really cool, and might be doable. Aside from needing to find some additional registers, the main challenge would be to swap the xor/or/and without smc.

I'm also wondering whether it is possible to increase the cutoff resolution. In theory the number of volume levels can be doubled by going from 16t to 8t per volume level, but that has the problem that we cannot render level 1 properly on ZX beeper. On hardware, this can be done by sacrificing levels 0 and 1 and using level 2 as new level 0, but on emulators this is inherently beepy (that's why zbmod has an "emulator" version). So I don't want to go down that route. In ulasyn, each volume level is generated twice per sample, so one could theoretically jump to the previous or next level half-way. The problem is that if this happens repeatedly at the same volume, we get an audible parasite tone from the mixing. So the filter tables and total volume calculation would need to take that into account. Right now what's being looked up is the answer to the question: Given the current volume 0..6 and the unfiltered next volume (either 0 or 6), what should be the next volume? This needs an added delta, ie: Assuming that the unfiltered next volume does not change,will the volume increase, decrease, or stay the same in the sample after the next one? Based on this, we can select the next volume "core" as follows:

- If both channels want to increase the volume further, jump half-way from volume to volume+1.
- Likewise, if both channels want to decrease the volume further, jump half-way from volume to volume-1.
- Else, do not jump half-way.

This way, repeated jumps from the same initial volume can only occur when the volume lookahead is wrong (because unfiltered state changed from 0 to 6 or vice versa), which should be rare enough to avoid parasite tones. This would give us one additional bit of cutoff precision.