Add SPU readback example

This commit is contained in:
ABelliqueux 2021-11-13 19:56:58 +01:00
parent 6e168368f6
commit 2dfc68d6ca
5 changed files with 613 additions and 0 deletions

View File

@ -0,0 +1,12 @@
.PHONY: all cleansub
all:
mkpsxiso -y ./isoconfig.xml
cleansub:
$(MAKE) clean
rm -f hello_spu_readback.cue hello_spu_readback.bin
TARGET = hello_spu_readback
SRCS = hello_spu_readback.c \
include ../common.mk

View File

@ -0,0 +1,58 @@
This example is adapted from PsyQ's sample : `psyq/psx/sample/sound/CDVOL, main.c,v 1.14 1997/05/02 13:05:21 by ayako`.
It was edited to fix typos, have a hopefully better code organization with hopefully more usefull variable names.
What it does is demonstrate how to transfer data from the PSX 's SPU to main memory in order to analyze / process the audio signal and dostuff accordingly.
In this instance, it's used to determine the coordinates of a few primitives to display a [VU-meter](https://en.wikipedia.org/wiki/VU_meter).
This technique is known to be used in certain games for lipsynching or audio visualization ( Crash team racing, Hercules, Vib Ribbon ...).
## PsyQ's SpuReadDecodedData() doc errata
The main function for transferring data from the SPU to the RAM is `SpuReadDecodedData()`, and is documented in **LibRef47.pdf, p1054**.
The table on this page (Table 15-2) contains erroneous data and should read :
![Spu addresses range](https://wiki.arthus.net/assets/spureaddecodeddata_errata.png)
The correct address ranges for the SPU buffer is :
| Map (bytes) | Data contents |
|-------------|---------------|
| 0x000 - 0x3ff | CD Left channel |
| 0x400 - 0x7ff | CD Right channel |
| 0x600 - 0xbff | Voice 1 |
| 0x800 - 0xfff | Voice 3 |
## Compiling
You need [mkpsxiso](https://github.com/Lameguy64/mkpsxiso) in your $PATH to generate a PSX disk image.
Typing
```bash
make
```
in a terminal will compile and generate the bin/cue files.
Typing
```bash
make cleansub
```
will clean the current directory
## More on CDDA
See the [hello_cdda](https://github.com/ABelliqueux/nolibgs_hello_worlds/tree/main/hello_cdda) example in this repo.
## Docs and links
Original psyq example : `psyq/psx/sample/sound/CDVOL, main.c,v 1.14 1997/05/02 13:05:21 by ayako`
## Music credits
Track 1 :
Beach Party by Kevin MacLeod
Link: https://incompetech.filmmusic.io/song/3429-beach-party
License: https://filmmusic.io/standard-license
Track 2:
Funk Game Loop by Kevin MacLeod
Link: https://incompetech.filmmusic.io/song/3787-funk-game-loop
License: https://filmmusic.io/standard-license

View File

@ -0,0 +1,428 @@
// SPU readback example
// adapted from PsyQ's sample : psyq/psx/sample/sound/CDVOL, main.c,v 1.14 1997/05/02 13:05:21 by ayako
// Schnappy 11-2021
#include <sys/types.h>
#include <stdio.h>
#include <stdint.h>
#include <kernel.h>
#include <libgte.h>
#include <libetc.h>
#include <libgpu.h>
// CD library
#include <libcd.h>
// SPU library
#include <libspu.h>
#include "../thirdparty/nugget/common/syscalls/syscalls.h"
#define printf ramsyscall_printf
#define VMODE 0 // Video Mode : 0 : NTSC, 1: PAL
#define SCREENXRES 320 // Screen width
#define SCREENYRES 240 + (VMODE << 4) // Screen height : If VMODE is 0 = 240, if VMODE is 1 = 256
#define CENTERX SCREENXRES/2 // Center of screen on x
#define CENTERY SCREENYRES/2 // Center of screen on y
#define MARGINX 0 // margins for text display
#define MARGINY 32
#define FONTSIZE 8 * 7 // Text Field Height
#define OTLEN 8 // Ordering Table Length
// Number of bars
#define BARNUM 2
// Peak cursor width
#define TSIZE 10
// Bar size / 2
#define BSIZE 128
// Top Y coordinate of Left volume bar
#define BARTOP 100
// Bottom Y coordinates of Left volume bar
#define BARBOTTOM ((BARTOP)+5)
// Vertical spacing
#define MARGIN 40
// Bars left coordinates
#define MINBAR CENTERX - BSIZE
// Bars right coordinates
#define MAXBAR ( CENTERX + BSIZE + TSIZE )
// Bars IDs
#define LEFTBAR 0
#define RIGHTBAR 1
// Return absolute value of a number
#define ABS(x) (((x)<0)?(-(x)):(x))
DISPENV disp[2]; // Double buffered DISPENV and DRAWENV
DRAWENV draw[2];
u_long ot[2][OTLEN]; // double ordering table of length 8 * 32 = 256 bits / 32 bytes
uint8_t primbuff[2][32768]; // double primitive buffer of length 32768 * 8 = 262.144 bits / 32,768 Kbytes
uint8_t *nextpri = primbuff[0]; // pointer to the next primitive in primbuff. Initially, points to the first bit of primbuff[0]
uint8_t db = 0; // index of which buffer is used, values 0, 1
// SPU attributes
SpuCommonAttr spuSettings;
// SPU IRQ address
uint16_t SpuIrqAddr;
// SPU decoded data buffer
SpuDecodedData decodedData;
// CD volume: current sample's max values
ulong leftMax, rightMax;
// Last 2 seconds's peak volume values
ulong leftPeak, rightPeak;
// Primitives for drawing the VU-metre, double buffered
// Blue : background bar
POLY_F4 * bar[BARNUM];
// White : current value
POLY_F4 * current[BARNUM];
// Red : volume peak in the last 3 seconds
POLY_F4 * peak[BARNUM];
// Colors for the VU-metre
CVECTOR bg = {0,90,255};
CVECTOR fg = {255,190,0};
CVECTOR cursor = {255,40,0};
void init(void)
{
ResetGraph(0); // Initialize drawing engine with a complete reset (0)
InitGeom();
SetGeomOffset(CENTERX,CENTERY);
SetGeomScreen(CENTERX);
SetDefDispEnv(&disp[0], 0, 0 , SCREENXRES, SCREENYRES); // Set display area for both &disp[0] and &disp[1]
SetDefDispEnv(&disp[1], 0, SCREENYRES, SCREENXRES, SCREENYRES); // &disp[0] is on top of &disp[1]
SetDefDrawEnv(&draw[0], 0, SCREENYRES, SCREENXRES, SCREENYRES); // Set draw for both &draw[0] and &draw[1]
SetDefDrawEnv(&draw[1], 0, 0 , SCREENXRES, SCREENYRES); // &draw[0] is below &draw[1]
// Set video mode
if (VMODE){ SetVideoMode(MODE_PAL);}
SetDispMask(1); // Display on screen
setRGB0(&draw[0], 50, 50, 50); // set color for first draw area
setRGB0(&draw[1], 50, 50, 50); // set color for second draw area
draw[0].isbg = 1; // set mask for draw areas. 1 means repainting the area with the RGB color each frame
draw[1].isbg = 1;
PutDispEnv(&disp[db]); // set the disp and draw environnments
PutDrawEnv(&draw[db]);
FntLoad(960, 0); // Load font to vram at 960,0(+128)
// Top bar
FntOpen (MINBAR, BARTOP - 10, 200, 150, 0, 64);
// Bottom bar
FntOpen (MINBAR, BARTOP - 10 + (MARGIN), 200, 150, 0, 64);
// Debug
FntOpen (32, SCREENYRES - 74, SCREENXRES - 64, 64, 0, 200);
}
void display(void)
{
DrawSync(0);
VSync(0);
PutDispEnv(&disp[db]);
PutDrawEnv(&draw[db]);
DrawOTag(&ot[db][OTLEN - 1]);
db = !db;
nextpri = primbuff[db];
}
void initPrimitives(void)
{
// Set primitives from primbuff[]
bar[0] = (POLY_F4 *)nextpri;
bar[1] = (POLY_F4 *)nextpri + sizeof(POLY_F4);
current[0] = (POLY_F4 *)nextpri + (sizeof(POLY_F4) * 2);
current[1] = (POLY_F4 *)nextpri + (sizeof(POLY_F4) * 3);
peak[0] = (POLY_F4 *)nextpri + (sizeof(POLY_F4) * 4);
peak[1] = (POLY_F4 *)nextpri + (sizeof(POLY_F4) * 5);
// Set each primitive to their default settings
for (int i = 0; i < BARNUM; i++)
{
// Volume bar background is blue
SetPolyF4 ( bar[i] );
setRGB0 ( bar[i], bg.r,bg.g,bg.b );
setXY4 ( bar[i],
MINBAR, BARTOP + i * MARGIN, /* NW */
MAXBAR, BARTOP + i * MARGIN, /* NE */
MINBAR, BARBOTTOM + i * MARGIN, /* SW */
MAXBAR, BARBOTTOM + i * MARGIN); /* SE */
// Current volume is light purple-ish
SetPolyF4 (current[i]);
setRGB0 ( current[i], fg.r,fg.g,fg.b);
setXY4 ( current[i],
MINBAR, BARTOP + i * MARGIN,
MINBAR + TSIZE, BARTOP + i * MARGIN,
MINBAR, BARBOTTOM + i * MARGIN,
MINBAR + TSIZE, BARBOTTOM + i * MARGIN);
// Initialize peak cursor
SetPolyF4 ( peak[i] );
setRGB0 ( peak[i], cursor.r,cursor.g,cursor.b);
setXY4 ( peak[i],
MINBAR, BARTOP + i * MARGIN,
MINBAR + TSIZE, BARTOP + i * MARGIN,
MINBAR, BARBOTTOM + i * MARGIN,
MINBAR + TSIZE, BARBOTTOM + i * MARGIN);
}
}
// Unused - should be called whenever this madness needs to be ended
void terminate(void)
{
// Turn SPU irq off
SpuSetIRQ (SPU_OFF);
// Clear callback functions
SpuSetIRQCallback ((SpuIRQCallbackProc) NULL);
SpuSetTransferCallback ((SpuTransferCallbackProc) NULL);
// Reset SPU settings
spuSettings.mask = (SPU_COMMON_MVOLL |
SPU_COMMON_MVOLR |
SPU_COMMON_CDVOLL |
SPU_COMMON_CDVOLR |
SPU_COMMON_CDMIX
);
spuSettings.mvol.left = 0;
spuSettings.mvol.right = 0;
spuSettings.cd.volume.left = 0;
spuSettings.cd.volume.right = 0;
spuSettings.cd.mix = SPU_OFF;
SpuSetCommonAttr (&spuSettings);
// Stop CD
CdStop ();
// Stop SPU processing
SpuQuit ();
// Re-init display env
ResetGraph (3);
// stop callback processing
StopCallback ();
}
// Print corresponding data for each volume bar
void printDataInfo(void)
{
// We're using 2 streams
FntPrint (0, "L: %04x peak/%04x\n", leftMax, leftPeak);
FntPrint (1, "R: %04x peak/%04x\n", rightMax, rightPeak);
FntFlush (0);
FntFlush (1);
}
// SPU IRQ calback function
void eachIRQ (void)
{
SpuSetIRQ (SPU_OFF); /**/
SpuReadDecodeData (&decodedData, SPU_CDONLY); /**/
}
// DMA Transfer callback function
void eachDMA (void)
{
if (SpuIrqAddr == 0x0)
SpuIrqAddr = 0x200;
else
SpuIrqAddr = 0x0;
// Change IRQ address
SpuSetIRQAddr (SpuIrqAddr);
// Turn SPU IRQ requests on
SpuSetIRQ (SPU_ON);
}
void findSampleMaxVolume(void)
{
// Search maximum volume value of the SPU decoded data
// SPU buffer data range adresses
long dataLowerAdress, dataUpperAdress;
// Current sample's max and working value
short maxL = 0, tmpL;
short maxR = 0, tmpR;
// Timers for the Peak cursor, reset after 120 iterations.
static long timeCursorL = 0, timeCursorR = 0;
// Find SPU data range according to current half we're working on
if (SpuIrqAddr == 0x0) {
/* 1st part is available */
dataLowerAdress = 0x0;
dataUpperAdress = 0x1ff;
} else {
/* 2nd part is available */
dataLowerAdress = 0x200;
dataUpperAdress = 0x3ff;
}
// Examine and find max volume in the data range
for (long i = dataLowerAdress; i < dataUpperAdress; i ++) {
// Examine SPU decoded data
tmpL = ABS(decodedData.cd_left[i]);
tmpR = ABS(decodedData.cd_right[i]);
// Only keep maximum value for this sample
if (maxL < tmpL ) {
maxL = tmpL ;
}
if (maxR < tmpR ) {
maxR = tmpR;
}
}
leftMax = (long) maxL;
rightMax = (long) maxR;
// Peak level
if (leftPeak < leftMax) {
leftPeak = leftMax;
timeCursorL = 0;
}
if (rightPeak < rightMax) {
rightPeak = rightMax;
timeCursorR = 0;
}
// Peak cursors: hold 2s@60fps.
// Increment counters until 120 is reached, then set cursors position to current leftMax/rightMax values
if (timeCursorL < 120) {
timeCursorL ++;
} else {
timeCursorL = 0;
leftPeak = leftMax;
}
if (timeCursorR < 120) {
timeCursorR ++;
} else {
timeCursorR = 0;
rightPeak = rightMax;
}
}
int main(void)
{
// Values used to switch CD track after
u_int counter = 0;
int8_t flip = 1;
// These will hold the normalised values of leftMax/rightMax, leftPeak/rightPeak
long lMax, rMax, lPeak, rPeak;
// Init display
init();
// Init CD system
CdInit ();
// Init Spu
SpuInit();
// Initialize SPU related variables
leftMax = rightMax = 0;
leftPeak = rightPeak = 0;
// Fill SPU data buffers with 0s
for (int i = 0; i < SPU_DECODEDDATA_SIZE; i ++) {
decodedData.cd_left[i] = 0;
decodedData.cd_right[i] = 0;
}
// SPU setup
// Set master & CD volume to max
spuSettings.mask = (SPU_COMMON_MVOLL |
SPU_COMMON_MVOLR |
SPU_COMMON_CDVOLL |
SPU_COMMON_CDVOLR |
SPU_COMMON_CDMIX);
// Master volume should be in range 0x0000 - 0x3fff
spuSettings.mvol.left = 0x3fff;
spuSettings.mvol.right = 0x3fff;
// Cd volume should be in range 0x0000 - 0x7fff
spuSettings.cd.volume.left = 0x7fff;
spuSettings.cd.volume.right = 0x7fff;
// Enable CD input ON
spuSettings.cd.mix = SPU_ON;
// Apply settings
SpuSetCommonAttr(&spuSettings);
// Set transfer mode
SpuSetTransferMode(SPU_TRANSFER_BY_DMA);
// Callbacks setup
// Set Transfer callback
(void) SpuSetTransferCallback ((SpuTransferCallbackProc) eachDMA);
// set IRQ callback
SpuSetIRQCallback ((SpuIRQCallbackProc) eachIRQ);
// Initialize SPU IRQ address
SpuIrqAddr = 0x200;
// Set IRQ address
SpuSetIRQAddr (SpuIrqAddr);
// Turn interrupt request ON
SpuSetIRQ(SPU_ON);
// CD Playback setup
// Play second audio track
// Get CD TOC
CdlLOC loc[100];
int ntoc;
while ((ntoc = CdGetToc(loc)) == 0) { /* Read TOC */
printf("No TOC found: please use CD-DA disc...\n");
FntPrint(2, "No TOC found: please use CD-DA disc...\n");
}
// Prevent out of bound pos
for (int i = 1; i < ntoc; i++) {
CdIntToPos(CdPosToInt(&loc[i]) - 74, &loc[i]);
}
// Those array will hold the return values of the CD commands
u_char param[4], result[8];
// Set CD parameters ; Report Mode ON, CD-DA ON. See LibeOver47.pdf, p.188
param[0] = CdlModeRept|CdlModeDA;
CdControlB (CdlSetmode, param, 0); /* set mode */
VSync (3); /* wait three vsync times */
// Play second track in toc array
CdControlB (CdlPlay, (u_char *)&loc[3], 0); /* play */
// Graphics setup
initPrimitives();
while (1)
{
counter++;
ClearOTagR(ot[db], OTLEN);
// Normalize volume
lMax = (leftMax * 256) / 0x8000 + MINBAR;
rMax = (rightMax * 256) / 0x8000 + MINBAR;
lPeak = (leftPeak * 256) / 0x8000 + MINBAR;
rPeak = (rightPeak * 256) / 0x8000 + MINBAR;
// Update primitives XY coordinates
setXY4 ( current[LEFTBAR],
MINBAR, BARTOP,
lMax + TSIZE, BARTOP,
MINBAR, BARBOTTOM,
lMax + TSIZE, BARBOTTOM);
setXY4 (current[RIGHTBAR],
MINBAR, BARTOP + MARGIN,
rMax + TSIZE, BARTOP + MARGIN,
MINBAR, BARBOTTOM + MARGIN,
rMax + TSIZE, BARBOTTOM + MARGIN);
setXY4 (peak[LEFTBAR],
lPeak, BARTOP,
lPeak + TSIZE, BARTOP,
lPeak, BARBOTTOM,
lPeak + TSIZE, BARBOTTOM);
setXY4 (peak[RIGHTBAR],
rPeak, BARTOP + MARGIN,
rPeak + TSIZE, BARTOP + MARGIN,
rPeak, BARBOTTOM + MARGIN,
rPeak + TSIZE, BARBOTTOM + MARGIN);
// Add prims to ordering table from bottom to top
for ( int i = 0; i < BARNUM; i++) {
addPrim(ot[db][OTLEN - 1], bar[i]);
addPrim(ot[db][OTLEN - 2], current[i]);
addPrim(ot[db][OTLEN - 3], peak[i]);
}
// Get current track number ~ every second
// See LibeOver47.pdf, p.188
if (counter%50 == 0){
CdReady(1, &result[0]);
// current track number can also be obtained with
// CdControlB (CdlGetlocP, 0, &result[0]);
}
// Switch track after ~ 20 seconds
if (counter%(50*20) == 0){
// Flip can have a value of 1 or -1
flip *= -1;
uint8_t nextTrackIndex = result[1] + flip;
// Send CD command to switch track
CdControlB (CdlPlay, (u_char *)&loc[ nextTrackIndex ], 0);
}
// Update current and peak values
findSampleMaxVolume();
// Print bar's infos
printDataInfo();
// Draw debug stream
FntPrint(2, "Hello SPU readback ! %d\n", counter);
FntPrint(2, "Current track: %d\n", result[1] );
FntPrint(2, "L: %08d, R: %08d\n", leftMax, rightMax);
FntPrint(2, "SPU Addr: 0x%03x ", SpuIrqAddr );
FntFlush(2);
// Update display
display();
}
return 0;
}

View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- MKPSXISO example XML script -->
<!-- <iso_project>
Starts an ISO image project to build. Multiple <iso_project> elements may be
specified within the same xml script which useful for multi-disc projects.
<iso_project> elements must contain at least one <track> element.
Attributes:
image_name - File name of the ISO image file to generate.
cue_sheet - Optional, file name of the cue sheet for the image file
(required if more than one track is specified).
-->
<iso_project image_name="hello_spu_readback.bin" cue_sheet="hello_spu_readback.cue">
<!-- <track>
Specifies a track to the ISO project. This example element creates a data
track for storing data files and CD-XA/STR streams.
Only one data track is allowed and data tracks must only be specified as the
first track in the ISO image and cannot be specified after an audio track.
Attributes:
type - Track type (either data or audio).
source - For audio tracks only, specifies the file name of a wav audio
file to use for the audio track.
-->
<track type="data">
<!-- <identifiers>
Optional, Specifies the identifier strings to use for the data track.
Attributes:
system - Optional, specifies the system identifier (PLAYSTATION if unspecified).
application - Optional, specifies the application identifier (PLAYSTATION if unspecified).
volume - Optional, specifies the volume identifier.
volume_set - Optional, specifies the volume set identifier.
publisher - Optional, specifies the publisher identifier.
data_preparer - Optional, specifies the data preparer identifier. If unspecified, MKPSXISO
will fill it with lengthy text telling that the image file was generated
using MKPSXISO.
-->
<identifiers
system ="PLAYSTATION"
application ="PLAYSTATION"
volume ="HELOCD"
volume_set ="HELOCD"
publisher ="SCHNAPPY"
data_preparer ="MKPSXISO"
/>
<!-- <license>
Optional, specifies the license file to use, the format of the license file must be in
raw 2336 byte sector format, like the ones included with the PsyQ SDK in psyq\cdgen\LCNSFILE.
License data is not included within the MKPSXISO program to avoid possible legal problems
in the open source environment... Better be safe than sorry.
Attributes:
file - Specifies the license file to inject into the ISO image.
-->
<!--
<license file="LICENSEA.DAT"/>
-->
<!-- <directory_tree>
Specifies and contains the directory structure for the data track.
Attributes:
None.
-->
<directory_tree>
<!-- <file>
Specifies a file in the directory tree.
Attributes:
name - File name to use in the directory tree (can be used for renaming).
type - Optional, type of file (data for regular files and is the default, xa for
XA audio and str for MDEC video).
source - File name of the source file.
-->
<!-- Stores system.txt as system.cnf -->
<file name="system.cnf" type="data" source="system.cnf"/>
<file name="SCES_313.37" type="data" source="hello_spu_readback.ps-exe"/>
<dummy sectors="1024"/>
<!-- <dir>
Specifies a directory in the directory tree. <file> and <dir> elements inside the element
will be inside the specified directory.
-->
</directory_tree>
</track>
<!--
Track 1 :
Beach Party by Kevin MacLeod
Link: https://incompetech.filmmusic.io/song/3429-beach-party
License: https://filmmusic.io/standard-license
Track 2:
Funk Game Loop by Kevin MacLeod
Link: https://incompetech.filmmusic.io/song/3787-funk-game-loop
License: https://filmmusic.io/standard-license
-->
<track type="audio" source="../hello_cdda/audio/beach.wav"/>
<track type="audio" source="../hello_cdda/audio/funk.wav"/>
</iso_project>

View File

@ -0,0 +1,4 @@
BOOT=cdrom:\SCES_313.37;1
TCB=4
EVENT=10
STACK=801FFFF0