Skip to content

Add a wider range of FabGL sound capability#31

Closed
HeathenUK wants to merge 16 commits intobreakintoprogram:mainfrom
HeathenUK:main
Closed

Add a wider range of FabGL sound capability#31
HeathenUK wants to merge 16 commits intobreakintoprogram:mainfrom
HeathenUK:main

Conversation

@HeathenUK
Copy link
Copy Markdown

These commits do the following:

  • Re-purpose the currently unused waveform byte in the VDU sequence into an indicator of the requested sound complexity.
  • If complexity is 0 (which is what Agon's BBC BASIC hard sets it to) then it carries on as before with a sawtooth wave and no envelope for the given duration at the given frequency.
  • If complexity is 1 the VDP expects one additional byte specifying the waveform to be used (square, triangle, sine, saw, noise).
  • Finally if complexity is 2 then VDP expects 13 (!) additional bytes, providing both a waveform and the specifics of an ASDR envelope for the sound.
  • For complexities 1 and 2 the system asumes time-based parameters are given in millis, and uses the appropriate esp macro to convert to ticks. This is left alone for complexity 0/BBC Basic mode.

This is designed to be backward-compatible with Agon's BBC Basic implementation (you could absolutely add additional advanced sound commands if you wanted, of course). But for anyone wanting to do something a bit more advanced (but still reasonably period-accurate for the Z80) via, say, C, this allow that fairly easily.

Here's some examples done via raw VDU commands in MOS (ignore and do not type the square brackets, they're just there to keep 16-bit words together):

  • VDU 23 0 &85 1 0 10 0 33 0 12 //Complexity 0: Default saw-tooth wave over channel 1, backward-compatible with BASIC.
  • VDU 23 0 &85 2 1 4 10 0 33 0 12 //Complexity 1: White noise over channel 2 - one additional parameter
  • VDU 23 0 &85 0 2 2 [127] [&B0 &04] [154 00] [05 00] [127] [00 00] [39 00] [&B8 01] [01] //Complexity 2: Space invaders style pew pew noise over channel zero

Finally - although I've worked to fold this into the way channel-based audio is done already in the VDP the "new" code pretty much just adapts the standard approach in FabGL reflected in various demo code. I'm not sure how you're flagging that up for attribution elsewhere where you've drawn on Fabrizio's code but TBA worth including an attribution here too!

Multi-complexity sound, backward compatible with BBC Basic single-shape, flat sound.
@HeathenUK
Copy link
Copy Markdown
Author

I recognise this has a minor conflict with the slight refactoring of both audio dispatch into its own function from vdu_sys_video and your introduction of timeouts (excellent addition!), but hopefully straightforward to resolve?

G

This change expands the use of the audio packet to set the nth bit in a global byte (channel_status) when audio is queued up, and then reset that byte when the audio is done playing. This means that "getsysvar_audioSuccess" now becomes a status register for all audio channels, not just the most recently played and you can query this on the ez80 side rather than guessing when a channel is clear based on delays.
The audio channel resets the appropriate bit when it's done playing its current audio.
@HeathenUK
Copy link
Copy Markdown
Author

As described above, I've gone a bit further now to co-opt the existing "last channel and its status" packet to give MOS a fuller picture of audio status. As before, this is invisible to MOS and BASIC and is backward compatible with the existing code.

And of course, it's compatible with up to 8 audio channels, if we ever go there.

…le simple audio

As with prior commits both are written to be backward compatible (i.e. invisible to pre-existing software not interested in new features).
Previous commit refers.
@HeathenUK
Copy link
Copy Markdown
Author

Probably the final additions to this PR now other than any potential bugs.

I've added the ability to upload PCM audio samples compliant with FabGL's constraints (8 bit, signed, 16KHz). These samples are housed in PSRAM so don't conflict with the use of SRAM for sprites.

I've also added an additional variation of the simple (pre-existing) sound and the version that allows you to specify the wave shape. These variants (complexities 3 and 4 respectively) cue audio without setting a blocking flag or duration. This means that similar to most period sound chips you create music by changing the frequency and volume of a tone channel and leaving it to play until you want to change it, rather than setting a duration - it will simply keep playing the same note at the same volume until you send a replacement instruction to the same channel.

On working this through it's striking how similar the constraints are between what's necessitated by the Agon's design and what was required for vintage audio chips back in the 80s and 90s. We're still playing with the optimal way to push data to the chip, even if it's via UART rather than DMA or register writes.

HeathenUK added 10 commits April 7, 2023 15:18
Now includes bmp loading that supports 8, 24 and 32bit bitmaps and an (unavoidably slow until we get access to fseek) tile crop from set function.
Single parameter VDU instructions for altering either just volume or just frequency. Useful for VGM playback coupled with infinite duration audio (complexity 3 or 4).
Need to switch back from RAM to file now we have fseek.
NB this still depends on at least infinite duration notes/change vol/freq and BMP bitmap support in VDP (see wider code for these).
@breakintoprogram
Copy link
Copy Markdown
Owner

Thanks for this! I'll be looking at integrating it into the next version of the VDP.

@HeathenUK
Copy link
Copy Markdown
Author

Hope it's helpful. You'll see the one thing I rowed back on (but never really removed the code for) was the concept of "forcing" versions of normal sound modes. Instead I eventually just implemented individual commands to alter the frequency or volume of a channel.

@stevesims stevesims mentioned this pull request Aug 10, 2023
Comment thread video.ino
Comment on lines +289 to 299
void sendChannelStatus(int channel, int status) {

if (status == 1) channel_status |= 1UL << channel;
if (status == 0) channel_status &= ~(1UL << channel);;

byte packet[] = {
channel,
success,
channel_status,
};
send_packet(PACKET_AUDIO, sizeof packet, packet);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @HeathenUK - when we met at The Cave and chatted about this audio stuff, I mentioned that I liked your status approach, and that I planned on adopting it in my code.

If you look thru my PR you'll see that I ended up going in a different direction, so I thought I'd explain why.

Long story short, this changes the meaning of the second response byte so existing code that's checking it after attempting to play a note would not be compatible.

Specifically the current BBC BASIC SOUND command implementation would need a fairly complex rewrite. Without that there would be circumstances where BASIC would fail to correctly work out whether an attempt to play a note was successful or not. I couldn't quite work out how to make BASIC work correctly with your approach.

I also felt that it's slightly too limited, as there's more state than just "on/off" that it would be useful to convey.

The original meaning of this second byte is, essentially, to give an indicator of the status of the last audio command, with the only supported command being "play note". With that in mind, I decided to just keep that meaning - so existing code can be compatible.

The underlying notion though that it'd be good to let code on the z80 side of the system be able to know the status of each audio channel is a great one though, hence why I implemented a "status" command.

Comment thread agon_audio.h
Comment on lines +15 to +25
struct audio_sample
{

bool written = false; // has this sample been written yet? If note, ignore efforts to play.
uint16_t length; // sample length
uint16_t rate; // sample rate
int8_t sample_buffer[262144]; // Max 256KB of raw PCM

};

struct audio_sample* samples[SAMPLE_SLOTS]; //6 Samples = ~1.5MB of PSRAM
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kinda liked your simplistic sample storage... but it's obviously rather restrictive with its fixed buffers, and is rather limited.

how to best handle loading and storing samples was my biggest headache - and indeed forced me to learn quite a lot about dynamic memory management in C++.

possibly the most frustrating aspect of dealing with samples tho is how slow BASIC can be reading in a file and sending it over to the VDP. 😁 my test code used a 160kb sample file, which took about 90s to send across. oof!

(I think with appropriate z80 assembly code that transfer could be cut down to about 2s)

@HeathenUK
Copy link
Copy Markdown
Author

@breakintoprogram just to close the loop on this and make sure you've clocked that Steve (above) has done the heavy lifting to bring this in line with modern VDP while fixing many of my hacks and errors in his own PR. Might save effort!

@breakintoprogram
Copy link
Copy Markdown
Owner

Hi @HeathenUK apologies for not getting round to this - it's been superseded by changes made in #76 - thanks for the contribution though, it looks like it formed the basis of the ideas in that change.

@breakintoprogram breakintoprogram added the duplicate This issue or pull request already exists label Sep 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

duplicate This issue or pull request already exists enhancement New feature or request

Projects

Status: Won't Do

Development

Successfully merging this pull request may close these issues.

3 participants