Topic: 1-bit sound on BBC Micro / Acorn Electron

I've been experimenting with the SquatM engine on two reasonably popular 6502 8-bit systems in the UK in the 1980's.  I've built a number of converters from various PSG chips that I can then transform to the .1tm format.  To start with I took a simple C64 SID file (https://deepsid.chordian.net/?file=/MUS … af_Rag.sid) and transformed this into the attached .1tm file.

You can hear this playing back a BBC Micro emulator here: https://twitter.com/charge_negative/sta … 7711753485

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

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.

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

Thanks,

Negative Charge

Post's attachments

maple_leaf_rag.1tm 236.49 kb, 3 downloads since 2024-02-25 

You don't have the permssions to download the attachments of this post.

Re: 1-bit sound on BBC Micro / Acorn Electron

>  I've built a number of converters from various PSG chips that I can then transform to the .1tm format.
Thats pretty darn cool !
Will the converter be released?  Would love to hear how "A Mind Is Born" comes out. I love that!

6502 : Look at the Peskysound engine within 1Tracker. For the PET which also runs the 6502.
http://shiru.untergrund.net/software.shtml
I'll shutup now and leave it for Shiru to answer properly.

Re: 1-bit sound on BBC Micro / Acorn Electron

bushy555 wrote:

Will the converter be released?  Would love to hear how "A Mind Is Born" comes out. I love that!

6502 : Look at the Peskysound engine within 1Tracker. For the PET which also runs the 6502.

Thanks - yes, I do plan on releasing the converters once I’ve finished a frontend for them.  At the moment it’s a manual workflow involving up to 10 scripts.

I tried “A Mind Is Born” but it seems to heavily rely upon filters, modulation and volume changes, so isn’t a great fit.  The first 30+ seconds translate to the same couple of notes playing without the filters.  I need to think about how to support them.

Thanks for the suggestion on the Peskysound engine. I’ll take a look.

Re: 1-bit sound on BBC Micro / Acorn Electron

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.

Re: 1-bit sound on BBC Micro / Acorn Electron

utz wrote:

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.

Thank you!  I'll investigate this.  For the fixed white noise there is info here I'll look into: https://www.smspower.org/Development/SN … ftRegister

For now, I've crudely mapped these to the SquatM Noise percussion, which works but doesn't give the same force that sampled drums would.

I've attached another example C64 SID conversion based on 4-Mat's Empty (512 bytes) track (Original here: https://deepsid.chordian.net/?file=/MUS … bytes.sid)

Post's attachments

Empty_512_bytes.1tm 123.21 kb, 2 downloads since 2024-02-26 

You don't have the permssions to download the attachments of this post.

Re: 1-bit sound on BBC Micro / Acorn Electron

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.

Re: 1-bit sound on BBC Micro / Acorn Electron

Thank you utz!  I’m fairly limited on memory (32kb) without using swram banks, but I can try decompressing at runtime.

With regards to how the BBC Micro disables interrupts and emits sound, here’s a port I made of Shiru’s SquatM engine:

https://github.com/NegativeCharge/BBC-M … /main.6502

ZP can start at zero, but is $5f to $8f in the above.  Code is normally compiled to $1100 to allow disk access, but can drop to $e00 depending on the file system.  The code above is in BeebAsm format but should be easy enough to switch to other compilers.

Thanks!

8 (edited by utz 2024-03-03 22:47:38)

Re: 1-bit sound on BBC Micro / Acorn Electron

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?

9 (edited by negative charge 2024-03-04 11:44:18)

Re: 1-bit sound on BBC Micro / Acorn Electron

Hi utz,

utz wrote:

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

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.

utz wrote:

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.

Yes!  I just tested that with the SquatM engine and it makes quite a difference.  Here's a video of the playback: https://youtu.be/sqFRGEz9g_U

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

Thanks

10

Re: 1-bit sound on BBC Micro / Acorn Electron

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.

Re: 1-bit sound on BBC Micro / Acorn Electron

utz wrote:

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, I see.  Yes, you can do that, and it does work - maybe some other adjustments need to be made as although the sound is less muffled, the noise appears to become high pitched.

12

Re: 1-bit sound on BBC Micro / Acorn Electron

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.

Re: 1-bit sound on BBC Micro / Acorn Electron

Thanks utz!

That’s a great starting point.  I’ll try and find some time over the next few days to experiment with it.  I think I can use similar code to read the data as SquatM.

In the meantime I added some of your suggestions to the BBC Micro SquatM engine here: https://github.com/NegativeCharge/BBC-M … /main.6502

I couldn’t get it to work without the volume latch, but at least the high pitched noise has gone now.

I’ll let you know how I get on, and hopefully post a sample,

Re: 1-bit sound on BBC Micro / Acorn Electron

Hi utz,

I've given this a go, but I think I'm either doing something wrong iterating through the pattern data or I've messed up the play loop - all I'm getting is a high pitched squeal, and a few clicks.

I'm using Shiru's data export from 1tracker, so the format should be familiar.

I think (based on the tritone.1te) that I may be misinterpreting how the duty and frequency are being stored.

Is there anything obvious in the below code that jumps out at you as incorrect?

Thanks! 

;Tritone beeper music engine
;Original Z80 code by Shiru 03'2011, released as Public Domain
;1tracker version by Shiru 03'2018
;6502 engine port by utz 03'2024
;Ported to the BBC Micro by Negative Charge 03'2024

; Constants
OSBYTE                  = $FFF4
OSWRCH                  = $FFEE
OSNEWL                  = $FFE7
SHEILABASE              = $FE00             ; System peripherals
SYSVIA_DDRA             = SHEILABASE + $43  ; Data direction register A
SYSVIA_ORAS             = SHEILABASE + $4F  ; Same as REGA but with no handshake I/O
SYSVIA_REGB             = SHEILABASE + $40  ; Port B I/O

DISPLAY_START           = $7c00
DEBUG                   = TRUE

OP_NOP                  = $EA
OP_ROL_A                = $2A

; Zero Page
ORG     $5f
GUARD   $8f

.vars_start

.loop_ptr           SKIP 2
.pattern_ptr        SKIP 2
.speed              SKIP 2

.ch0_drum           SKIP 1
.ch1_drum           SKIP 1
.ch2_drum           SKIP 1

.vars_end

ORG     &1100
GUARD   DISPLAY_START

.start

INCLUDE "lib\os.s.6502" 

; Write data to sound chip then add processing delay
MACRO sound_write_slow
    sta     SYSVIA_ORAS        ;4 Write reg/data to SN76489

    lda     #%00000000         ;2
    sta     SYSVIA_REGB        ;4 
    nop                        ;2
    nop                        ;2
    nop                        ;2
    lda     #%00001000         ;2
    sta     SYSVIA_REGB        ;4
ENDMACRO

MACRO RESET_SOUND_CHIP
    ; Zero volumes on all SN76489 channels, just in case anything already playing
    lda     #%11111111
    sound_write_slow                                ; Channel 3 (Noise)
    lda     #%11011111
    sound_write_slow                                ; Channel 2
    lda     #%10111111
    sound_write_slow                                ; Channel 1
    lda     #%10011111
    sound_write_slow                                ; Channel 0
ENDMACRO

.init
    \ Print Track Title
    ldx     #1
    ldy     #22
    jsr     moveTextCursor

    jsr     printString
    equs    "The Liberty Bell",0

    \ Print Track Artist
    ldx     #1
    ldy     #24
    jsr     moveTextCursor

    jsr     printString
    equs    "John Philip Sousa",0

    ; Set up audio
    
    ; System VIA port A to all outputs
    lda     #%11111111
    sta     SYSVIA_DDRA

    sei

    RESET_SOUND_CHIP

    ; Period to 1 on tone channel 0
    lda     #%10000001
    sound_write_slow                                ; Channel 0
    lda     #%00000000
    sound_write_slow

    ; System VIA Port A, place accumulator on wires, no handshake
    lda     #%00000000         
    sta     SYSVIA_REGB

    lda     #LO(music_data)
    ldx     #HI(music_data)

.play

    pha
    txa
    pha
    
    lda     #0
    tax
.zero_page_reset_loop
    sta     vars_start,x
    inx
    cpx     #vars_end-vars_start
    bne     zero_page_reset_loop
    
    pla
    sta     pattern_ptr+1
    pla
    sta     pattern_ptr+0

IF DEBUG
        pha:txa:pha:tya:pha

        ldx     #1
        ldy     #1
        jsr     moveTextCursor
        jsr     printString
        equs    "Pattern Pointer: ",0

        lda     pattern_ptr+1
        jsr     s_print_hex
        lda     pattern_ptr+0
        jsr     s_print_hex
        jsr     OSNEWL

        pla:tay:pla:tax:pla
ENDIF

    ldy     #0
    lda     (pattern_ptr),y
    sta     loop_ptr+0

    iny
    lda     (pattern_ptr),y
    sta     loop_ptr+1
    
IF DEBUG
        pha:txa:pha:tya:pha

        ldx     #1
        ldy     #2
        jsr     moveTextCursor

        jsr     printString
        equs    "Loop Pointer   : ",0

        lda     loop_ptr+1
        jsr     s_print_hex
        lda     loop_ptr+0
        jsr     s_print_hex
        jsr     OSNEWL

        pla:tay:pla:tax:pla
ENDIF

    lda     pattern_ptr+0
    clc
    adc     #6
    sta     pattern_ptr+0
    bcc     play_loop
    inc     pattern_ptr+1

.play_loop

    ldy     #1
    lda     (pattern_ptr),y
    bne     no_loop
    
.return_loop

    lda     loop_ptr+0
    sta     pattern_ptr+0
    lda     loop_ptr+1
    sta     pattern_ptr+1
    jmp     play_loop

.no_loop
    iny
    lda     (pattern_ptr),y
    iny
    sta     speed+0
    lda     (pattern_ptr),y
    iny
    sta     speed+1

IF DEBUG
        pha:txa:pha:tya:pha

        ldx     #1
        ldy     #4
        jsr     moveTextCursor

        jsr     printString
        equs    "Speed          : ",0

        lda     speed+1
        jsr     s_print_hex
        lda     speed+0
        jsr     s_print_hex
        jsr     OSNEWL

        pla:tay:pla:tax:pla
ENDIF

jmp     row

.pattern_end
    cli     ; Enable interrupts

    RESET_SOUND_CHIP

    rts

.row
    ldy     #0
    lda     (pattern_ptr),y
    bne     ch0

    sta     ch0_drum        ; Silent
    sta     ch0_duty
    sta     ch0_div_lo
    sta     ch0_div_hi
    jmp     skip_ch0

.ch0
    cmp     #$ff
    beq     pattern_end
    cmp     #$01
    beq     skip_ch0        ; Same as previous ch0 entry
    cmp     #$80
    bcc     skip_drum_0
    sta     ch0_drum

    iny
    lda     (pattern_ptr),y

.skip_drum_0
    pha
    lsr     a
    lsr     a
    lsr     a
    lsr     a
    sta     ch0_duty
    pla

    ora     #%00001111
    sta     ch0_div_hi

    iny
    lda     (pattern_ptr),y
    sta     ch0_div_lo

.skip_ch0
    iny
    lda     (pattern_ptr),y
    bne     ch1             

    sta     ch1_drum        ; Silent
    sta     ch1_duty
    sta     ch1_div_lo
    sta     ch1_div_hi
    jmp     skip_ch1

.ch1
    cmp     #$01
    beq     skip_ch1        ; Same as previous ch0 entry
    cmp     #$80
    bcc     skip_drum_1
    sta     ch1_drum

    iny
    lda     (pattern_ptr),y

.skip_drum_1
    pha
    lsr     a
    lsr     a
    lsr     a
    lsr     a
    sta     ch1_duty
    pla

    ora     #%00001111
    sta     ch1_div_hi

    iny
    lda     (pattern_ptr),y
    sta     ch1_div_lo

.skip_ch1
    iny
    lda     (pattern_ptr),y
    bne     ch2

    sta     ch2_drum        ; Silent
    sta     ch2_duty
    sta     ch2_div_lo
    sta     ch2_div_hi
    jmp     skip_ch2

.ch2
    cmp     #$01
    beq     skip_ch2        ; Same as previous ch0 entry
    cmp     #$80
    bcc     skip_drum_2
    sta     ch2_drum

    iny
    lda     (pattern_ptr),y

.skip_drum_2
    pha
    lsr     a
    lsr     a
    lsr     a
    lsr     a
    sta     ch2_duty
    pla

    ora     #%00001111
    sta     ch2_div_hi

    iny
    lda     (pattern_ptr),y
    sta     ch2_div_lo

.skip_ch2
    tya
    clc
    adc     pattern_ptr+0
    sta     pattern_ptr+0
    bcc     play_note
    inc     pattern_ptr+1

.play_note

IF DEBUG
    jsr     printRowInfo
ENDIF

    clc                             ;2  update osc

ch0_div_lo=*+1
    lda     #$0                     ;2
ch0_acc_lo=*+1
    adc     #$0                     ;2
    sta ch0_acc_lo                  ;4
ch0_div_hi=*+1
    lda     #$0                     ;2
ch0_acc_hi=*+1
    adc     #$0                     ;2
    sta     ch0_acc_hi              ;4
ch0_duty=*+1
    cmp     #$0                     ;2  compare against duty threshold
    sbc     ch0_acc_hi              ;4  A = 0 on low half-cycle, FF on hi half-cycle
    ora     #%10010000              ;2
    sta     SYSVIA_ORAS             ;4

    clc                             ;2
ch1_div_lo=*+1
    lda     #$0                     ;2
ch1_acc_lo=*+1
    adc     #$0                     ;2
    sta     ch1_acc_lo              ;4
ch1_div_hi=*+1
    lda     #$0                     ;2
ch1_acc_hi=*+1
    adc     #$0                     ;2
    sta     ch1_acc_hi              ;4
ch1_duty=*+1
    cmp     #$0                     ;2
    sbc     ch1_acc_hi              ;4
    ora     #%10010000              ;2
    sta     SYSVIA_ORAS             ;4

    clc                             ;2
ch2_div_lo=*+1
    lda     #$0                     ;2
ch2_acc_lo=*+1
    adc     #$0                     ;2
    sta     ch2_acc_lo              ;4
ch2_div_hi=*+1
    lda     #$0                     ;2
ch2_acc_hi=*+1
    adc     #$0                     ;2
    sta     ch2_acc_hi              ;4
ch2_duty=*+1
    cmp     #$0                     ;2
    sbc     ch2_acc_hi              ;4
    ora     #%10010000              ;2
    sta     SYSVIA_ORAS             ;2

    jmp     row

.s_print_hex
        pha                            ; Save A
        lsr     a
        lsr     a
        lsr     a
        lsr     a                      ; Move top nybble to bottom nybble
        jsr     printNybble
        pla
        and     #&0f                   ; Mask out original bottom nybble
.printNybble
        sed
        clc
        adc     #&90                   ; Produce &90-&99 or &00-&05
        adc     #&40                   ; Produce &30-&39 or &41-&46
        cld
        jmp     OSWRCH                 ; Print it

.printRowInfo

        pha:txa:pha:tya:pha

        ldx     #1
        ldy     #8
        jsr     moveTextCursor

        jsr     printString
        equs    "Row: ",0

        lda     ch0_div_hi
        jsr     s_print_hex
        lda     ch0_div_lo
        jsr     s_print_hex

        ldx     #11
        ldy     #8
        jsr     moveTextCursor
        
        lda     ch1_div_hi
        jsr     s_print_hex
        lda     ch1_div_lo
        jsr     s_print_hex
        
        ldx     #16
        ldy     #8
        jsr     moveTextCursor

        lda     ch2_div_hi
        jsr     s_print_hex
        lda     ch2_div_lo
        jsr     s_print_hex

        pla:tay:pla:tax:pla

        rts

INCLUDE "tracks\liberty_bell_tritone.6502" 

.end

SAVE "MAIN",start,end,init

\ ******************************************************************
\ *    Memory Info
\ ******************************************************************

PRINT "-----------------------"
PRINT " 1-BIT TRITONE PLAYER  "
PRINT "-----------------------"
PRINT "CODE size       = ", ~end-start
PRINT "-----------------------"
PRINT "HIGH WATERMARK  = ", ~P%
PRINT "FREE            = ", ~start+end
PRINT "-----------------------"

\ ******************************************************************
\ * Supporting Files
\ ******************************************************************

PUTBASIC "loader.bas","LOADER"
PUTFILE  "screens\title.bin","TITLE",DISPLAY_START
PUTFILE  "BOOT","!BOOT",$ffff

15

Re: 1-bit sound on BBC Micro / Acorn Electron

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.

Re: 1-bit sound on BBC Micro / Acorn Electron

utz wrote:

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.

Thanks - I've corrected that.  I'm still getting the same high pitched output though.

17

Re: 1-bit sound on BBC Micro / Acorn Electron

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.

Re: 1-bit sound on BBC Micro / Acorn Electron

For standard Tritone there only appears to be a global speed setting? - Tritone Digi has per-row.

Is the speed the value that should be plugged into the row length counter?… and if so should that be bne play_note rather than beq play_note?

I’ve tried this but still just getting a high pitched note currently.

Thanks

19

Re: 1-bit sound on BBC Micro / Acorn Electron

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.

Re: 1-bit sound on BBC Micro / Acorn Electron

Thank you utz.  Once I have something working I may take another look at the data format.  Here's the revised code - I still suspect something wrong with the duty or frequency parsing

;Tritone beeper music engine
;Original Z80 code by Shiru 03'2011, released as Public Domain
;1tracker version by Shiru 03'2018
;6502 engine port by utz 03'2024
;Ported to the BBC Micro by Negative Charge 03'2024

; Constants
OSBYTE                  = $FFF4
OSWRCH                  = $FFEE
OSNEWL                  = $FFE7
SHEILABASE              = $FE00             ; System peripherals
SYSVIA_DDRA             = SHEILABASE + $43  ; Data direction register A
SYSVIA_ORAS             = SHEILABASE + $4F  ; Same as REGA but with no handshake I/O
SYSVIA_REGB             = SHEILABASE + $40  ; Port B I/O

DISPLAY_START           = $7c00
DEBUG                   = TRUE
ROW_DEBUG               = FALSE

OP_NOP                  = $EA
OP_ROL_A                = $2A

; Zero Page
ORG     $5f
GUARD   $8f

.vars_start

.loop_ptr           SKIP 2
.pattern_ptr        SKIP 2
.speed              SKIP 2

.drum               SKIP 1

.vars_end

ORG     &1100
GUARD   DISPLAY_START

.start

INCLUDE "lib\os.s.6502" 

; Write data to sound chip then add processing delay
MACRO sound_write_slow
    sta     SYSVIA_ORAS        ;4 Write reg/data to SN76489

    lda     #%00000000         ;2
    sta     SYSVIA_REGB        ;4 
    nop                        ;2
    nop                        ;2
    nop                        ;2
    lda     #%00001000         ;2
    sta     SYSVIA_REGB        ;4
ENDMACRO

MACRO RESET_SOUND_CHIP
    ; Zero volumes on all SN76489 channels, just in case anything already playing
    lda     #%11111111
    sound_write_slow                                ; Channel 3 (Noise)
    lda     #%11011111
    sound_write_slow                                ; Channel 2
    lda     #%10111111
    sound_write_slow                                ; Channel 1
    lda     #%10011111
    sound_write_slow                                ; Channel 0
ENDMACRO

.init
    ; Print Track Title
    ldx     #1
    ldy     #22
    jsr     moveTextCursor

    jsr     printString
    equs    "The Liberty Bell",0

    ; Print Track Artist
    ldx     #1
    ldy     #24
    jsr     moveTextCursor

    jsr     printString
    equs    "John Philip Sousa",0

    ; Set up audio
    
    ; System VIA port A to all outputs
    lda     #%11111111
    sta     SYSVIA_DDRA

    sei

    RESET_SOUND_CHIP

    ; Period to 1 on tone channel 0
    lda     #%10000001
    sound_write_slow                                ; Channel 0
    lda     #%00000000
    sound_write_slow

    ; System VIA Port A, place accumulator on wires, no handshake
    lda     #%00000000         
    sta     SYSVIA_REGB

    lda     #LO(music_data)
    ldx     #HI(music_data)

.play

    pha
    txa
    pha
    
    lda     #0
    tax
.zero_page_reset_loop
    sta     vars_start,x
    inx
    cpx     #vars_end-vars_start
    bne     zero_page_reset_loop
    
    pla
    sta     pattern_ptr+1
    pla
    sta     pattern_ptr+0

IF DEBUG
        pha:txa:pha:tya:pha

        ldx     #1
        ldy     #1
        jsr     moveTextCursor
        jsr     printString
        equs    "Pattern Pointer: ",0

        lda     pattern_ptr+1
        jsr     s_print_hex
        lda     pattern_ptr+0
        jsr     s_print_hex
        jsr     OSNEWL

        pla:tay:pla:tax:pla
ENDIF

    ldy     #0
    lda     (pattern_ptr),y
    sta     loop_ptr+0

    iny
    lda     (pattern_ptr),y
    sta     loop_ptr+1
    
IF DEBUG
        pha:txa:pha:tya:pha

        ldx     #1
        ldy     #2
        jsr     moveTextCursor

        jsr     printString
        equs    "Loop Pointer   : ",0

        lda     loop_ptr+1
        jsr     s_print_hex
        lda     loop_ptr+0
        jsr     s_print_hex
        jsr     OSNEWL

        pla:tay:pla:tax:pla
ENDIF

    lda     pattern_ptr+0
    clc
    adc     #6
    sta     pattern_ptr+0
    bcc     play_loop
    inc     pattern_ptr+1

.play_loop

    ldy     #1
    lda     (pattern_ptr),y
    bne     no_loop
    
.return_loop

    lda     loop_ptr+0
    sta     pattern_ptr+0
    lda     loop_ptr+1
    sta     pattern_ptr+1
    jmp     play_loop

.no_loop
    iny
    lda     (pattern_ptr),y
    iny
    sta     speed+0
    sta     row_length_lo
    lda     (pattern_ptr),y
    iny
    sta     speed+1
    sta     row_length_hi

IF DEBUG
        pha:txa:pha:tya:pha

        ldx     #1
        ldy     #4
        jsr     moveTextCursor

        jsr     printString
        equs    "Speed          : ",0

        lda     speed+1
        jsr     s_print_hex
        lda     speed+0
        jsr     s_print_hex
        jsr     OSNEWL

        pla:tay:pla:tax:pla
ENDIF

jmp     row

.pattern_end
    cli                             ; Enable interrupts

    RESET_SOUND_CHIP

    rts

.row
    ldy     #0
    lda     (pattern_ptr),y
    bne     ch0

    sta     drum                    ; Silent
    sta     ch0_duty
    sta     ch0_div_lo
    sta     ch0_div_hi
    jmp     skip_ch0

.ch0
    cmp     #$ff
    beq     pattern_end
    cmp     #$01
    beq     skip_ch0                ; Same as previous ch0 entry
    cmp     #$80
    bcc     skip_drum
    sta     drum

    iny
    lda     (pattern_ptr),y

.skip_drum
    pha
    lsr     a
    lsr     a
    lsr     a
    lsr     a
    sta     ch0_duty
    pla

    ora     #%00001111
    sta     ch0_div_hi

    iny
    lda     (pattern_ptr),y
    sta     ch0_div_lo

.skip_ch0
    iny
    lda     (pattern_ptr),y
    bne     ch1             

    sta     ch1_duty
    sta     ch1_div_lo
    sta     ch1_div_hi
    jmp     skip_ch1

.ch1
    cmp     #$01
    beq     skip_ch1                ; Same as previous ch0 entry
    pha
    lsr     a
    lsr     a
    lsr     a
    lsr     a
    sta     ch1_duty
    pla

    ora     #%00001111
    sta     ch1_div_hi

    iny
    lda     (pattern_ptr),y
    sta     ch1_div_lo

.skip_ch1
    iny
    lda     (pattern_ptr),y
    bne     ch2
    
    sta     ch2_duty
    sta     ch2_div_lo
    sta     ch2_div_hi
    jmp     skip_ch2

.ch2
    cmp     #$01
    beq     skip_ch2                ; Same as previous ch0 entry
    pha
    lsr     a
    lsr     a
    lsr     a
    lsr     a
    sta     ch2_duty
    pla

    ora     #%00001111
    sta     ch2_div_hi

    iny
    lda     (pattern_ptr),y
    sta     ch2_div_lo

.skip_ch2
    tya

row_length_lo=*+1
    ldx     #0                     ;   reset row length counter lo byte
row_length_hi=*+1
    ldy     #0

IF ROW_DEBUG
    jsr     printRowInfo
ENDIF

    clc
    adc     pattern_ptr+0
    sta     pattern_ptr+0
    bcc     play_note
    inc     pattern_ptr+1

.play_note

    clc                             ;2  update osc

ch0_div_lo=*+1
    lda     #$0                     ;2
ch0_acc_lo=*+1
    adc     #$0                     ;2
    sta ch0_acc_lo                  ;4
ch0_div_hi=*+1
    lda     #$0                     ;2
ch0_acc_hi=*+1
    adc     #$0                     ;2
    sta     ch0_acc_hi              ;4
ch0_duty=*+1
    cmp     #$0                     ;2  compare against duty threshold
    sbc     ch0_acc_hi              ;4  A = 0 on low half-cycle, FF on hi half-cycle
    ora     #%10010000              ;2
    sta     SYSVIA_ORAS             ;4

    clc                             ;2
ch1_div_lo=*+1
    lda     #$0                     ;2
ch1_acc_lo=*+1
    adc     #$0                     ;2
    sta     ch1_acc_lo              ;4
ch1_div_hi=*+1
    lda     #$0                     ;2
ch1_acc_hi=*+1
    adc     #$0                     ;2
    sta     ch1_acc_hi              ;4
ch1_duty=*+1
    cmp     #$0                     ;2
    sbc     ch1_acc_hi              ;4
    ora     #%10010000              ;2
    sta     SYSVIA_ORAS             ;4

    clc                             ;2
ch2_div_lo=*+1
    lda     #$0                     ;2
ch2_acc_lo=*+1
    adc     #$0                     ;2
    sta     ch2_acc_lo              ;4
ch2_div_hi=*+1
    lda     #$0                     ;2
ch2_acc_hi=*+1
    adc     #$0                     ;2
    sta     ch2_acc_hi              ;4
ch2_duty=*+1
    cmp     #$0                     ;2
    sbc     ch2_acc_hi              ;4
    ora     #%10010000              ;2
    sta     SYSVIA_ORAS             ;2

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

    dey                             ; row length hi byte
    bne     play_note

    jmp     row

.s_print_hex
        pha                            ; Save A
        lsr     a
        lsr     a
        lsr     a
        lsr     a                      ; Move top nybble to bottom nybble
        jsr     printNybble
        pla
        and     #&0f                   ; Mask out original bottom nybble
.printNybble
        sed
        clc
        adc     #&90                   ; Produce &90-&99 or &00-&05
        adc     #&40                   ; Produce &30-&39 or &41-&46
        cld
        jmp     OSWRCH                 ; Print it

.printRowInfo

        pha:txa:pha:tya:pha

        ldx     #1
        ldy     #8
        jsr     moveTextCursor

        jsr     printString
        equs    "Row: ",0

        lda     ch0_div_hi
        jsr     s_print_hex
        lda     ch0_div_lo
        jsr     s_print_hex

        ldx     #11
        ldy     #8
        jsr     moveTextCursor
        
        lda     ch1_div_hi
        jsr     s_print_hex
        lda     ch1_div_lo
        jsr     s_print_hex
        
        ldx     #16
        ldy     #8
        jsr     moveTextCursor

        lda     ch2_div_hi
        jsr     s_print_hex
        lda     ch2_div_lo
        jsr     s_print_hex

        pla:tay:pla:tax:pla

        rts

INCLUDE "tracks\liberty_bell_tritone.6502" 

.end

SAVE "MAIN",start,end,init

\ ******************************************************************
\ *    Memory Info
\ ******************************************************************

PRINT "-----------------------"
PRINT " 1-BIT TRITONE PLAYER  "
PRINT "-----------------------"
PRINT "CODE size       = ", ~end-start
PRINT "-----------------------"
PRINT "HIGH WATERMARK  = ", ~P%
PRINT "FREE            = ", ~start+end
PRINT "-----------------------"

\ ******************************************************************
\ * Supporting Files
\ ******************************************************************

PUTBASIC "loader.bas","LOADER"
PUTFILE  "screens\title.bin","TITLE",DISPLAY_START
PUTFILE  "BOOT","!BOOT",$ffff

Re: 1-bit sound on BBC Micro / Acorn Electron

A quick update - I’ve now got this partially working… it’s playing back too slow, is fairly quiet and there’s a high pitched whine in the background.  However, it’s making a recognisable sound now :-)  Thanks utz!

The issue was that the SN volume value needs to flip between 0x0 and 0xf rather than 0xff.  I added an AND 0xf before each ORA for each channel.

I now need to alter the speed and optimize the data read somehow.

22

Re: 1-bit sound on BBC Micro / Acorn Electron

Ah, good. I'm really too tired to dig into this today.

I wonder what causes that high-pitched whine. This and the slow speed makes me think that the engine isn't running at the 2MHz I was assuming. Are there any interrupts still active? Are there any additional delays when writing to the SN, or is it possible that video is causing some blockage?

Optimizing the data read will only get you so far. Since the overall volume is low, row transitions will always be noticable. You might gain more by writing to all 3 tone channels instead of just one, provided you can make the inner sound loop run fast enough.

You might also consider mapping each Tritone channel to it's own SN channel. That would make things less timing critical.

Re: 1-bit sound on BBC Micro / Acorn Electron

Thanks - I’ll try outputting to three channels simultaneously next.  I think some of the slowness is probably the values I’m using in the custom .1te engine to output the music data.  I’m not sure I have the right value for CPU time.

I’ve put the code I have so far on GitHub: https://github.com/NegativeCharge/Beeb-Tritone-Player

24

Re: 1-bit sound on BBC Micro / Acorn Electron

Do you happen to know if setting the frequency of an SN channel will reset its phase?

Re: 1-bit sound on BBC Micro / Acorn Electron

utz wrote:

Do you happen to know if setting the frequency of an SN channel will reset its phase?

I may be wrong, but I believe this is only true of the LFSR (which I’m not using here). However, noise phase is reset by frequency writes, volume writes have no effect.