mirror of
https://github.com/rdolbeau/NuBusFPGA.git
synced 2024-06-09 21:29:31 +00:00
8-bits support
This commit is contained in:
parent
8ecda4c1d8
commit
ca779f3d26
|
@ -1,10 +1,13 @@
|
|||
/*
|
||||
File: NuBusFPGAHDMIAudio.c
|
||||
Contains: NuBusFGPA HDMI sound output component
|
||||
Written by: Romain Dolbeau
|
||||
Copyright: © 2023 by Romain Dolbeau
|
||||
|
||||
Based upon the following reference code from 'Develop Magazine, issue 20':
|
||||
File: NoiseMaker.c
|
||||
|
||||
Contains: Sample sound output component
|
||||
|
||||
Written by: Kip Olson
|
||||
|
||||
Copyright: © 1994 by Apple Computer, Inc.
|
||||
*/
|
||||
|
||||
|
@ -18,7 +21,7 @@
|
|||
#include "SoundComponents.h"
|
||||
|
||||
#include "Slots.h"
|
||||
#include "RomDefs.h"
|
||||
#include "ROMDefs.h"
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Hardware
|
||||
|
@ -28,6 +31,7 @@
|
|||
|
||||
#define u_int32_t volatile unsigned long
|
||||
|
||||
//#define BT_DEBUG 1
|
||||
// this is the DAC registers, not needed for audio except 'debug' is handy
|
||||
struct goblin_bt_regs {
|
||||
u_int32_t mode;
|
||||
|
@ -60,6 +64,7 @@ struct goblin_csr {
|
|||
u_int32_t goblin_goblin_audio_buf0_size;
|
||||
u_int32_t goblin_goblin_audio_buf1_addr;
|
||||
u_int32_t goblin_goblin_audio_buf1_size;
|
||||
u_int32_t goblin_goblin_audio_buf_desc;
|
||||
};
|
||||
|
||||
#define GOBLIN_AUDIOBUFFER_OFFSET 0x00920000
|
||||
|
@ -81,7 +86,7 @@ static inline unsigned long brev(const unsigned long r) {
|
|||
|
||||
// hardware settings
|
||||
|
||||
#define kSampleSizesCount 1 // 2
|
||||
#define kSampleSizesCount 2
|
||||
#define k8BitSamples 8 // 8-bit samples
|
||||
#define k16BitSamples 16 // 16-bit samples
|
||||
|
||||
|
@ -125,7 +130,7 @@ typedef struct {
|
|||
|
||||
typedef struct {
|
||||
|
||||
// these are general purpose variables that every sound component will need
|
||||
// these are general purpose variables that every sound component will need
|
||||
ComponentInstance self; // ourselves
|
||||
ComponentInstance sourceComponent; // component to call when hardware needs more data
|
||||
SoundComponentDataPtr sourceDataPtr; // pointer to source data structure
|
||||
|
@ -134,7 +139,7 @@ typedef struct {
|
|||
Boolean prefsChanged; // true if preferences have changed
|
||||
PreferencesHandle prefsHandle; // global preferences
|
||||
|
||||
// these are variables specific to this implementation
|
||||
// these are variables specific to this implementation
|
||||
SoundComponentData hwSettings; // current hardware settings
|
||||
unsigned long hwVolume; // current hardware volume
|
||||
Boolean hwInterruptsOn; // true if sound is playing
|
||||
|
@ -153,7 +158,7 @@ typedef struct {
|
|||
// Compatibility with old names (?)
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
typedef ComponentFunctionUPP ComponentRoutine;
|
||||
typedef ComponentFunctionUPP ComponentRoutine;
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Prototypes
|
||||
|
@ -208,6 +213,7 @@ pascal ComponentResult main(ComponentParameters *params, GlobalsPtr globals)
|
|||
ComponentRoutine theRtn;
|
||||
ComponentResult result;
|
||||
|
||||
#ifdef BT_DEBUG
|
||||
if (globals) {
|
||||
if ((*globals).bt) {
|
||||
(*(*globals).bt).debug = 'MAIN';
|
||||
|
@ -220,6 +226,7 @@ pascal ComponentResult main(ComponentParameters *params, GlobalsPtr globals)
|
|||
(*(struct goblin_bt_regs*)0xfc900000).debug = 'main';
|
||||
(*(struct goblin_bt_regs*)0xfc900000).debug = params->what;
|
||||
}
|
||||
#endif
|
||||
|
||||
theRtn = GetComponentRoutine(params->what); // get address of component routine
|
||||
|
||||
|
@ -377,18 +384,21 @@ pascal ComponentResult __ComponentRegister(GlobalsPtr globals)
|
|||
((unsigned long)mySpBlock.spSlot)<<24 |
|
||||
GOBLIN_BT_OFFSET);
|
||||
|
||||
|
||||
#ifdef BT_DEBUG
|
||||
(*(*globals).bt).debug = 'REGI';
|
||||
|
||||
(*(*globals).bt).debug = 'slot';
|
||||
(*(*globals).bt).debug = mySpBlock.spSlot;
|
||||
#endif
|
||||
|
||||
(*globals).csr = (struct goblin_csr*)(0xF0000000 |
|
||||
((unsigned long)mySpBlock.spSlot)<<24 |
|
||||
GOBLIN_CSR_OFFSET);
|
||||
|
||||
#ifdef BT_DEBUG
|
||||
(*(*globals).bt).debug = 'csr ';
|
||||
(*(*globals).bt).debug = (unsigned long)(*globals).csr;
|
||||
#endif
|
||||
|
||||
return (0); // install this sound component
|
||||
}
|
||||
|
@ -593,17 +603,21 @@ pascal ComponentResult __InitOutputDevice(GlobalsPtr globals, long actions)
|
|||
((unsigned long)mySpBlock.spSlot)<<24 |
|
||||
GOBLIN_BT_OFFSET);
|
||||
|
||||
#ifdef BT_DEBUG
|
||||
(*(*globals).bt).debug = 'INIT';
|
||||
|
||||
(*(*globals).bt).debug = 'SLOT';
|
||||
(*(*globals).bt).debug = mySpBlock.spSlot;
|
||||
#endif
|
||||
|
||||
(*globals).csr = (struct goblin_csr*)(0xF0000000 |
|
||||
((unsigned long)mySpBlock.spSlot)<<24 |
|
||||
GOBLIN_CSR_OFFSET);
|
||||
|
||||
#ifdef BT_DEBUG
|
||||
(*(*globals).bt).debug = 'CSR ';
|
||||
(*(*globals).bt).debug = (unsigned long)(*globals).csr;
|
||||
#endif
|
||||
|
||||
// Open the mixer and tell it the type of data it should output. The
|
||||
// description includes sample format, sample rate, sample size, number of channels
|
||||
|
@ -615,7 +629,7 @@ pascal ComponentResult __InitOutputDevice(GlobalsPtr globals, long actions)
|
|||
// set to hardware defaults
|
||||
|
||||
globals->hwSettings.flags = 0;
|
||||
globals->hwSettings.format = (prefsPtr->sampleSize == 8) ? kOffsetBinary : kTwosComplement;
|
||||
globals->hwSettings.format = (prefsPtr->sampleSize == k8BitSamples) ? kOffsetBinary : kTwosComplement;
|
||||
globals->hwSettings.sampleRate = prefsPtr->sampleRate;
|
||||
globals->hwSettings.sampleSize = prefsPtr->sampleSize;
|
||||
globals->hwSettings.numChannels = prefsPtr->numChannels;
|
||||
|
@ -678,7 +692,7 @@ pascal ComponentResult __GetInfo(GlobalsPtr globals, SoundSource sourceID,
|
|||
listPtr->handle = h; // handle to be returned
|
||||
|
||||
sp = (short *) *h; // store sample sizes in handle
|
||||
//*sp++ = k8BitSamples;
|
||||
*sp++ = k8BitSamples;
|
||||
*sp++ = k16BitSamples;
|
||||
break;
|
||||
|
||||
|
@ -776,7 +790,7 @@ pascal ComponentResult __SetInfo(GlobalsPtr globals, SoundSource sourceID,
|
|||
{
|
||||
short sampleSize = (short) infoPtr;
|
||||
|
||||
if (/*(sampleSize == k8BitSamples) ||*/ // make sure it is a valid sample size
|
||||
if ((sampleSize == k8BitSamples) || // make sure it is a valid sample size
|
||||
(sampleSize == k16BitSamples))
|
||||
{
|
||||
prefsPtr->sampleSize = sampleSize; // save new size in prefs
|
||||
|
@ -887,7 +901,9 @@ pascal ComponentResult __PlaySourceBuffer(GlobalsPtr globals, SoundSource source
|
|||
{
|
||||
ComponentResult result;
|
||||
|
||||
#ifdef BT_DEBUG
|
||||
(*(*globals).bt).debug = 'PLAY';
|
||||
#endif
|
||||
|
||||
// tell mixer to start playing this new buffer
|
||||
result = SoundComponentPlaySourceBuffer(globals->sourceComponent, sourceID, pb, actions);
|
||||
|
@ -901,9 +917,10 @@ pascal ComponentResult __PlaySourceBuffer(GlobalsPtr globals, SoundSource source
|
|||
if (!(actions & kSourcePaused))
|
||||
result = StartHardware(globals);
|
||||
|
||||
|
||||
#ifdef BT_DEBUG
|
||||
(*(*globals).bt).debug = 'YALP';
|
||||
(*(*globals).bt).debug = result;
|
||||
#endif
|
||||
|
||||
return (result);
|
||||
}
|
||||
|
@ -977,12 +994,12 @@ PreferencesHandle GetPreferences(ComponentInstance self, Boolean inSystemHeap)
|
|||
/* If we end up here, it means that the preferences could not be loaded out of the
|
||||
sound preferences file for some reason, so we need to generate some default preferences. */
|
||||
|
||||
GetPrefsFailed:
|
||||
GetPrefsFailed:
|
||||
DisposeHandle(prefsHandle);
|
||||
NewPrefsHandleFailed:
|
||||
InfoFailed:
|
||||
NewPrefsHandleFailed:
|
||||
InfoFailed:
|
||||
DisposeHandle(componentName);
|
||||
NewNameHandleFailed:
|
||||
NewNameHandleFailed:
|
||||
|
||||
prefsHandle = NewHandleLockClear(sizeof(PreferencesRecord), inSystemHeap); // create space for prefs handle
|
||||
if (prefsHandle == nil)
|
||||
|
@ -1021,9 +1038,9 @@ void SavePreferences(ComponentInstance self, PreferencesHandle prefsHandle)
|
|||
HLock(componentName);
|
||||
err = SetSoundPreference(componentDesc.componentSubType, (StringPtr) *componentName, (Handle) prefsHandle);
|
||||
|
||||
InfoFailed:
|
||||
InfoFailed:
|
||||
DisposeHandle(componentName);
|
||||
NewNameHandleFailed:
|
||||
NewNameHandleFailed:
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -1060,11 +1077,13 @@ OSErr SetupHardware(GlobalsPtr globals)
|
|||
GOBLIN_AUDIOBUFFER_OFFSET |
|
||||
(GOBLIN_AUDIOBUFFER_SIZE >> 1)); // offset by 4 KiB
|
||||
|
||||
#ifdef BT_DEBUG
|
||||
(*(*globals).bt).debug = 'bufX';
|
||||
(*(*globals).bt).debug = (unsigned long)(*globals).buf0;
|
||||
(*(*globals).bt).debug = (unsigned long)(*globals).buf1;
|
||||
(*(*globals).bt).debug = brev((unsigned long)&(*(*globals).csr).goblin_goblin_audio_buf0_addr);
|
||||
(*(*globals).bt).debug = brev((unsigned long)&(*(*globals).csr).goblin_goblin_audio_buf1_addr);
|
||||
#endif
|
||||
|
||||
// HW view (internal Wishbone addresses, so w/o the slot number)
|
||||
(*(*globals).csr).goblin_goblin_audio_buf0_addr = brev((unsigned long)(0xF0000000 |
|
||||
|
@ -1073,7 +1092,6 @@ OSErr SetupHardware(GlobalsPtr globals)
|
|||
GOBLIN_AUDIOBUFFER_OFFSET |
|
||||
(GOBLIN_AUDIOBUFFER_SIZE >> 1)));
|
||||
|
||||
// FIXME TODO: irq
|
||||
(*(*globals).siqel).sqType = sIQType;
|
||||
(*(*globals).siqel).sqPrio = 6;
|
||||
(*(*globals).siqel).sqAddr = irqTrampoline;
|
||||
|
@ -1181,9 +1199,9 @@ OSErr SetHardwareVolume(GlobalsPtr globals, unsigned long volume)
|
|||
// and copy the data to the hardware. On the way out, if all data was copied,
|
||||
// try to get some more so it will be available immediately next interrupt.
|
||||
/*
|
||||
pascal void InterruptRoutine(SndChannelPtr chan, SndCommand *cmd)
|
||||
{
|
||||
#pragma unused (chan)
|
||||
pascal void InterruptRoutine(SndChannelPtr chan, SndCommand *cmd)
|
||||
{
|
||||
#pragma unused (chan)
|
||||
|
||||
GlobalsPtr globals;
|
||||
SoundComponentDataPtr sourceDataPtr;
|
||||
|
@ -1201,17 +1219,17 @@ pascal void InterruptRoutine(SndChannelPtr chan, SndCommand *cmd)
|
|||
|
||||
CopySamplesToHardware(globals, sourceDataPtr); // fulfill hardware request
|
||||
|
||||
// Normally, you will want to check to see if you have run out
|
||||
// of data here and get more right away so you will be ready for
|
||||
// the next interrupt. This example does not have any hardware
|
||||
// to copy the data to, so it leaves the data in the mixer buffer and
|
||||
// thus cannot call for more until it has been played.
|
||||
// Normally, you will want to check to see if you have run out
|
||||
// of data here and get more right away so you will be ready for
|
||||
// the next interrupt. This example does not have any hardware
|
||||
// to copy the data to, so it leaves the data in the mixer buffer and
|
||||
// thus cannot call for more until it has been played.
|
||||
|
||||
// if (sourceDataPtr->sampleCount == 0) // exhausted the source
|
||||
// sourceDataPtr = GetMoreSource(globals); // get more for next time
|
||||
// if (sourceDataPtr->sampleCount == 0) // exhausted the source
|
||||
// sourceDataPtr = GetMoreSource(globals); // get more for next time
|
||||
|
||||
ResumeHardware(globals); // resume interrupts
|
||||
}
|
||||
}
|
||||
*/
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// This routine returns the data pointer to your mixer source. If there
|
||||
|
@ -1235,13 +1253,13 @@ SoundComponentDataPtr GetMoreSource(GlobalsPtr globals)
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#ifdef BT_DEBUG
|
||||
(*(*globals).bt).debug = 'srcd';
|
||||
(*(*globals).bt).debug = sourceDataPtr->numChannels;
|
||||
(*(*globals).bt).debug = sourceDataPtr->sampleSize;
|
||||
(*(*globals).bt).debug = sourceDataPtr->sampleRate;
|
||||
(*(*globals).bt).debug = sourceDataPtr->sampleCount;
|
||||
#endif
|
||||
|
||||
return (sourceDataPtr); // return pointer to source
|
||||
}
|
||||
|
@ -1254,29 +1272,68 @@ SoundComponentDataPtr GetMoreSource(GlobalsPtr globals)
|
|||
// used in your sound component.
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
static void StereoBlockMove(const void *in, void* out, const int sampleBytes, const int sampleCount) {
|
||||
int i;
|
||||
unsigned long data;
|
||||
|
||||
switch(sampleBytes) {
|
||||
case 1: {
|
||||
for (i = 0 ; i < sampleCount/2; i++) {
|
||||
data = ((unsigned short*)in)[i];
|
||||
data = ((data & 0x0000FF00) << 8) | (data & 0x000000FF);
|
||||
data |= (data << 8);
|
||||
((unsigned long*)out)[i] = data;
|
||||
}
|
||||
if (sampleCount & 1) {
|
||||
((unsigned char*)out)[2 * sampleCount - 2] = ((unsigned char*)in)[sampleCount - 1];
|
||||
((unsigned char*)out)[2 * sampleCount - 1] = ((unsigned char*)in)[sampleCount - 1];
|
||||
}
|
||||
} break;
|
||||
|
||||
case 2: {
|
||||
for (i = 0 ; i < sampleCount; i++) {
|
||||
data = ((unsigned short*)in)[i];
|
||||
data |= (data << 16);
|
||||
((unsigned long*)out)[i] = data;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// This routine copies the data returned by the mixer to the hardware.
|
||||
|
||||
void CopySamplesToHardware(GlobalsPtr globals, SoundComponentDataPtr sourceDataPtr)
|
||||
static void CopySamplesToHardware(GlobalsPtr globals, SoundComponentDataPtr sourceDataPtr)
|
||||
{
|
||||
OSErr err = noErr;
|
||||
unsigned long status = brev((*(*globals).csr).goblin_goblin_audio_ctrl);
|
||||
const unsigned long status = brev((*(*globals).csr).goblin_goblin_audio_ctrl);
|
||||
const int sampleBytes = sourceDataPtr->sampleSize >> 3;
|
||||
const int sampleChannel = sourceDataPtr->numChannels;
|
||||
|
||||
if (status & 0x0100) {
|
||||
#ifdef BT_DEBUG
|
||||
(*(*globals).bt).debug = 'cont';
|
||||
#endif
|
||||
// already playing, so the 'lastbuf' is empty
|
||||
// and we are playing lastbuf xor 1
|
||||
switch ((*globals).lastbuf) {
|
||||
case 0:
|
||||
BlockMove(sourceDataPtr->buffer, (void*)((*globals).buf0), 4 * sourceDataPtr->sampleCount);
|
||||
if (sampleChannel == 2) {
|
||||
BlockMove(sourceDataPtr->buffer, (void*)((*globals).buf0), sampleBytes * 2 * sourceDataPtr->sampleCount);
|
||||
} else if (sampleChannel == 1) {
|
||||
StereoBlockMove(sourceDataPtr->buffer, (void*)((*globals).buf0), sampleBytes, sourceDataPtr->sampleCount);
|
||||
}
|
||||
(*(*globals).csr).goblin_goblin_audio_buf0_size = brev(sourceDataPtr->sampleCount);
|
||||
(*globals).lastbuf = 1;
|
||||
//(*(*globals).csr).goblin_goblin_audio_ctrl = brev(0x0100); // play buf0 // redundant ?
|
||||
break;
|
||||
case 1:
|
||||
default:
|
||||
BlockMove(sourceDataPtr->buffer, (void*)((*globals).buf1), 4 * sourceDataPtr->sampleCount);
|
||||
if (sampleChannel == 2) {
|
||||
BlockMove(sourceDataPtr->buffer, (void*)((*globals).buf1), sampleBytes * 2 * sourceDataPtr->sampleCount);
|
||||
} else if (sampleChannel == 1) {
|
||||
StereoBlockMove(sourceDataPtr->buffer, (void*)((*globals).buf1), sampleBytes, sourceDataPtr->sampleCount);
|
||||
}
|
||||
(*(*globals).csr).goblin_goblin_audio_buf1_size = brev(sourceDataPtr->sampleCount);
|
||||
(*globals).lastbuf = 0;
|
||||
//(*(*globals).csr).goblin_goblin_audio_ctrl = brev(0x0101); // play buf1 // redundant ?
|
||||
|
@ -1286,18 +1343,27 @@ void CopySamplesToHardware(GlobalsPtr globals, SoundComponentDataPtr sourceDataP
|
|||
} else {
|
||||
// not yet playing, put half of the samples in each buffer
|
||||
// so that when the first buffer is empty, we'll be playing the second while reloading
|
||||
long buf0count = sourceDataPtr->sampleCount / 2;
|
||||
long buf1count = sourceDataPtr->sampleCount - buf0count;
|
||||
const long buf0count = sourceDataPtr->sampleCount / 2;
|
||||
const long buf1count = sourceDataPtr->sampleCount - buf0count;
|
||||
#ifdef BT_DEBUG
|
||||
(*(*globals).bt).debug = 'new ';
|
||||
(*(*globals).bt).debug = buf0count;
|
||||
(*(*globals).bt).debug = buf1count;
|
||||
BlockMove(sourceDataPtr->buffer, (void*)((*globals).buf0), 4 * buf0count);
|
||||
BlockMove(sourceDataPtr->buffer + 4*buf0count, (void*)((*globals).buf1), 4 * buf1count);
|
||||
#endif
|
||||
if (sampleChannel == 2) {
|
||||
BlockMove(sourceDataPtr->buffer, (void*)((*globals).buf0), sampleBytes * 2 * buf0count);
|
||||
BlockMove(sourceDataPtr->buffer + sampleBytes * 2 * buf0count, (void*)((*globals).buf1), sampleBytes * 2 * buf1count);
|
||||
} else if (sampleChannel == 1) {
|
||||
StereoBlockMove(sourceDataPtr->buffer, (void*)((*globals).buf0), sampleBytes, buf0count);
|
||||
StereoBlockMove(sourceDataPtr->buffer + sampleBytes * buf0count, (void*)((*globals).buf1), sampleBytes, buf1count);
|
||||
}
|
||||
(*(*globals).csr).goblin_goblin_audio_buf0_size = brev(buf0count);
|
||||
(*(*globals).csr).goblin_goblin_audio_buf1_size = brev(buf1count);
|
||||
(*globals).lastbuf = 0;
|
||||
sourceDataPtr->sampleCount = 0; // sound has been played
|
||||
// now play for real
|
||||
(*(*globals).csr).goblin_goblin_audio_buf_desc = brev((sourceDataPtr->sampleSize == k8BitSamples ? 0x1 : 0x0) |
|
||||
(sourceDataPtr->format == kOffsetBinary ? 0x0080 : 0x0000)); // otherwise assume kTwosComplement
|
||||
(*(*globals).csr).goblin_goblin_audio_ctrl = brev(0x0100); // play buf0
|
||||
}
|
||||
}
|
||||
|
@ -1312,7 +1378,9 @@ short irqFct(GlobalsPtr globals)
|
|||
SoundComponentDataPtr sourceDataPtr;
|
||||
long irqstatus;
|
||||
|
||||
#ifdef BT_DEBUG
|
||||
(*(*globals).bt).debug = 'irq ';
|
||||
#endif
|
||||
|
||||
// first, is that one of ours ?
|
||||
irqstatus = brev((*(*globals).csr).goblin_goblin_audio_irqstatus);
|
||||
|
@ -1321,7 +1389,9 @@ short irqFct(GlobalsPtr globals)
|
|||
return 0;
|
||||
}
|
||||
|
||||
#ifdef BT_DEBUG
|
||||
(*(*globals).bt).debug = 'irqA';
|
||||
#endif
|
||||
|
||||
// yes, ours, clear & suspend it before dealing with it
|
||||
(*(*globals).csr).goblin_goblin_audio_irqctrl = brev(0x2);
|
||||
|
@ -1347,4 +1417,4 @@ asm pascal void irqTrampoline(void)
|
|||
BSR irqFct
|
||||
MOVEM.L (A7)+,D1-D6/A0/A2-A5
|
||||
RTS
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit c18819f169c556ada9fc0d6358c3965aef09ee48
|
||||
Subproject commit eea4e2aab7fa7a048a92f283ce8163206e9449d6
|
Loading…
Reference in New Issue
Block a user