Jump to content
Why become a member? ×

Trampa v3 - DIY MIDI Controller


SamIAm
 Share

Recommended Posts

Time to share the sophistication (not!) of the wiring so far.

image.thumb.png.f2652db81b202544b95a3951420ba67e.png

 

  • Pushbutton at the top is used in conjunction with the onboard BOOTSEL button to reset the PICO into USB Flash mode, to flash the PICO one simply copies a file to the USB drive, it then reboots running the code.
  • The four pushbuttons at the bottom are the footswitches.
  • A USB cable plugged into the connector on the left carries the MIDI over USB to the host device (Laptop/Dwarf multi-fx unit/etc) as well as supplying power to the PICO.  It also carries serial output from the PICO to a terminal emulator I use for simple debugging.

 

S'manth x

Edited by Smanth
  • Like 2
Link to comment
Share on other sites

Hi

 

Just found this topic and was interested in how it's doing. I had a couple of questions:

 

1. The Mod Dwarf has three buttons already, so is this four button pedal board in addition to three or to replace the three?

 

2. Wouldn't it be easier to simply buy something like a Line 6 pedalboard with USB. I have a 2nd hand one that I spent £60 on. Admittedly it doesn't have the little OLEDS for displaying whats going on which is really, really nice, but it does have an expression pedal. I say this as someone who has gone down that rabbit hole far, far too many times. Sometimes I have to slap myself and say is this sensible. To demonstrate how stupid I can be, I was interested in making some hand carved house signs, I ended up building a whole sodding CNC machine, starting with a 3d printer and some plywoood, and even started writing my own design program (in JavaScript) to get the output I wanted. Eventually I came to my senses and donated the CNC machine to the local school who were very grateful and brought some wooden ones for £100.

 

3. Have you validated that the output of the Mod Dwarf contains enough information for you to keep your pedals in sync? When I first read this, my immediate thoughts were keeping the state of the Mod Dwarf and your pedal board in sync. If you changed the Mod Dwarf, e.g pressed a button to change an effect, then does your pedal board need to have it's state changed to reflect that?

 

4. Logging on low level hardware is tricky, you don't have all the nice output devices to write to, you can have all the sprintf stuff but you've got to get it out to somewhere you can actualy read it. One thing I thought of (and haven't done anything to validate if this would work), would be embed the debug data in a Midi SysEx mesage and read it on your midi monitor on your laptop. That means you don't need two channels. I looked at SysEx when I was debugging the Line 6 pedalboard to work with Amplitube on a Mac. (Basically the Midi CC message value need to be inverted to keep the lights on pedal board synced with Amplitube, so a 1 becomes a 127 and vice versa, oddly enough Amplitube on an iPad works fine).

 

I was a C/Unix/network developer for too many years (note the lack of C++ in that statement) and have written a lot of low level stuff for Microchip PIC16/18 and Arduino's for astrophotograpy. I have played with FreeRTOS as well, but not for a looong time. You might have inspired me to go back and pull the manuals out and have a play.

 

This is a great thread, please keep it going.

 

Thanks

 

Rob

  • Like 3
Link to comment
Share on other sites

20 hours ago, Smanth said:

Yes, it's part of my button class :)

/*
--.--
  |  ,---.,---.,-.-.,---.,---.
  |  |    ,---|| | ||   |,---|
  `  `    `---^` ' '|---'`---^
                    |
(C) 2023 Samantha-uk
https://github.com/samantha-uk/trampa

Adapted from debounce.c https://www.kennethkuhn.com/electronics/debounce.c
/******************************************************************************
debounce.c
written by Kenneth A. Kuhn
version 1.00

This is an algorithm that debounces or removes random or spurious
transistions of a digital signal read as an input by a computer.  This is
particularly applicable when the input is from a mechanical contact.  An
integrator is used to perform a time hysterisis so that the signal must
persistantly be in a logical state (0 or 1) in order for the output to change
to that state.  Random transitions of the input will not affect the output
except in the rare case where statistical clustering is longer than the
specified integration time.

The following example illustrates how this algorithm works.  The sequence
labeled, real signal, represents the real intended signal with no noise.  The
sequence labeled, corrupted, has significant random transitions added to the
real signal.  The sequence labled, integrator, represents the algorithm
integrator which is constrained to be between 0 and 3.  The sequence labeled,
output, only makes a transition when the integrator reaches either 0 or 3.
Note that the output signal lags the input signal by the integration time but
is free of spurious transitions.

real signal 0000111111110000000111111100000000011111111110000000000111111100000
corrupted   0100111011011001000011011010001001011100101111000100010111011100010
integrator  0100123233233212100012123232101001012321212333210100010123233321010
output      0000001111111111100000001111100000000111111111110000000001111111000

I have been using this algorithm for years and I show it here as a code
fragment in C.  The algorithm has been around for many years but does not seem
to be widely known.  Once in a rare while it is published in a tech note.  It
is notable that the algorithm uses integration as opposed to edge logic
(differentiation).  It is the integration that makes this algorithm so robust
in the presence of noise.
******************************************************************************/

#include "button.h"

Button::Button(int id, ButtonConfig* buttonConfig, uint pin) {
  _id = id;
  _buttonConfig = buttonConfig;
  _pin = pin;
}

void Button::init(void) {
  gpio_init(_pin);
  gpio_set_dir(_pin, GPIO_IN);
  gpio_pull_up(_pin);
}

bool Button::pressed() {
  /*
  Step 1: Update the integrator based on the input signal.  Note that the
  integrator follows the input, decreasing or increasing towards the limits as
  determined by the input state (0 or 1).
  */
  if (gpio_get(_pin) == 1) {
    if (_integrator > 0) _integrator--;
  } else if (_integrator < INTEGRATOR_MAX)
    _integrator++;

  /*
  Step 2: Update the output state based on the integrator.  Note that the
  output will only change states if the integrator has reached a limit, either
  0 or MAXIMUM.
   */
  if (_integrator == 0)
    _pressed = false;
  else if (_integrator >= INTEGRATOR_MAX) {
    _integrator =
        INTEGRATOR_MAX; /* defensive code if integrator got corrupted */
    _pressed = true;
  }
  return _pressed;
}
void Button::setState(ButtonState state) {
  // write_serial("Button[%d] from [%d]->[%d]\r\n", _id, _state, state);
  _state = state;
}

void Button::check(void) {
  // Get the debounced current debounced pressed state of the button
  bool isPressed = pressed();
  // bool isPressed = gpio_get(_pin);

  // Process the state machine for the button
  switch (_state) {
    case ButtonState::IDLE:
      if (isPressed) {
        // Capture the time
        _buttonPressedTime = get_absolute_time();

        // Reset _clickCount
        _clickCount = 0;

        // should we send a PRESS event
        if (!_buttonConfig->_clickDetect) fireEvent(ButtonEvent::PRESS);

        // Change to PRESSED state
        setState(ButtonState::PRESSED);
      }
      break;

    case ButtonState::PRESSED:
      if (!isPressed) {                     // The button is now released
        if (_buttonConfig->_clickDetect) {  // We ARE detecting clicks
          // Capture the time
          _buttonReleasedTime = get_absolute_time();

          // Change to RELEASED state
          setState(ButtonState::RELEASED);
        } else {  // We ARE NOT detecting clicks
          fireEvent(ButtonEvent::RELEASE);
          // Reset _clickCount
          _clickCount = 0;

          // Change to IDLE state
          setState(ButtonState::IDLE);
        }
      } else {  // The button is still pressed
        // If we are detecting clicks and the time since _buttonDownTime is
        // longer than _holdDelay
        if (_buttonConfig->_clickDetect &&
            absolute_time_diff_us(_buttonPressedTime, get_absolute_time()) >=
                _buttonConfig->_holdDelay) {
          fireEvent(ButtonEvent::HOLD);

          // Capture the time
          _buttonRepeatTime = get_absolute_time();

          // Change to HOLD state
          setState(ButtonState::HOLD);
        }
      }
      break;

    case ButtonState::RELEASED:
      if (isPressed) {  // The button is pressed
        // Capture the time
        _buttonPressedTime = get_absolute_time();

        // Increment _clickCount
        _clickCount++;

        // Change to PRESSED state
        setState(ButtonState::PRESSED);
      } else {  // The button is still releasaed
        // check to see if _clickDelay has passed since we entered RELEASED
        // state
        if (absolute_time_diff_us(_buttonReleasedTime, get_absolute_time()) >=
            _buttonConfig->_clickDelay) {
          // _clickDelay time has passed so we report the number of clicks
          fireEvent(ButtonEvent::CLICK);

          // Reset _clickCount
          _clickCount = 0;

          // Change to IDLE state
          setState(ButtonState::IDLE);
        }
      }
      break;

    case ButtonState::HOLD:
      // If the button is now released
      // _clickCount is used to indicate if any single clicks preceeded the hold
      // These are used as "shifts" to potentially alter the behaviour of
      // hold/repeat events.
      if (!isPressed) {  // The button is now released
        fireEvent(ButtonEvent::HOLD_RELEASE);

        // Reset _clickCount
        _clickCount = 0;

        // Change to IDLE state
        setState(ButtonState::IDLE);
      } else {  // The button is still pressed
        // If time down is longer than _repeatDelay
        if (absolute_time_diff_us(_buttonRepeatTime, get_absolute_time()) >=
            _buttonConfig->_repeatDelay) {
          fireEvent(ButtonEvent::REPEAT);

          // Reset _buttonHoldTime
          _buttonRepeatTime = get_absolute_time();
        }
      }
      break;
  }
}

void Button::fireEvent(ButtonEvent event) {
  // Check to make sure that we have a cbFunction
  if (_buttonConfig->cbButtonEvent) {
    _buttonConfig->cbButtonEvent(_id, event, _clickCount, _latched);
  }
  if (_buttonConfig->_latching) _latched = !_latched;
}

ButtonConfig::ButtonConfig() {}

S'manth x

Slight problem - it's impossible to read this on a phone in dark mode.

 

Screenshot_20230508_135142_Edge.thumb.jpg.9669205258eb1bde615cd502178c38fe.jpg

 

Bigger problem - it's been a couple of decades since I programmed anything and so if even I could read it I would just be depressed.

 

The cabbages are doing fine though.

  • Like 1
Link to comment
Share on other sites

36 minutes ago, Richard R said:

The cabbages are doing fine though.

Excellent!

I guess BC dark mode does not display the code well ... but not to worry (nor about coding) Once I've got a basic version operational I'll be sharing it via github (Including compiled code) so that others can hopefully enjoy Trampa too. I'll be sharing the github link here when it's ready.

 

S'manth x

  • Thanks 1
Link to comment
Share on other sites

48 minutes ago, rwillett said:

3. Have you validated that the output of the Mod Dwarf contains enough information for you to keep your pedals in sync? When I first read this, my immediate thoughts were keeping the state of the Mod Dwarf and your pedal board in sync. If you changed the Mod Dwarf, e.g pressed a button to change an effect, then does your pedal board need to have it's state changed to reflect that?

 

By default it doesn't, other than on the CC interface, but the beauty of it is that you can do anything you want in your own patches, so you can have a midi generator that just sends a message when you switch to a patch. It does mean knowing what is on the patch and setting the button to the same as the patch, but that was mentioned at the beginning of this

  • Like 1
Link to comment
Share on other sites

43 minutes ago, rwillett said:

1. The Mod Dwarf has three buttons already, so is this four button pedal board in addition to three or to replace the three?

 

My plan is to use in in addition to the three footswitches on the Dwarf.

 

44 minutes ago, rwillett said:

2. Wouldn't it be easier to simply buy something like a Line 6 pedalboard with USB

I'm not sure the Line6 would do the trick.  Trampa will have a TRS input for an expression pedal (I've a SONICAKE passive device), after I've got the footswitches sending MIDI (Hopefully just a few hours of playing away) I'll turn to getting that working.  In my POC I got it to work so I don;t imagine it will be a lot of effort, even if I incorporate a mapping algorithm (like Morningstar) to allow selection of lin.log mapping of the ADC input to the MIDI value I send.

 

48 minutes ago, rwillett said:

3. Have you validated that the output of the Mod Dwarf contains enough information for you to keep your pedals in sync? When I first read this, my immediate thoughts were keeping the state of the Mod Dwarf and your pedal board in sync. If you changed the Mod Dwarf, e.g pressed a button to change an effect, then does your pedal board need to have it's state changed to reflect that?

 

Definately an issue! It is possible to send a MIDI message when the Dwarf loads a new configuration or settings snapshot and I intend to capture this to change the Trampa setup. Trampa will display the state it thinks is the case on the OLEDs, but this may fall out of sync with the Dwarf ... I am exploring if it is possible (either via midi message or other API to determine the actual state of a Dwarf plugin, to allow 100% accurate representation)

Ther is a recent new plugin for the Dwarf, which can display on the LCD display state change (based on MIDI messages) that will ease this problem a bit.

 

52 minutes ago, rwillett said:

4. Logging on low level hardware is tricky, you don't have all the nice output devices to write to, you can have all the sprintf stuff but you've got to get it out to somewhere you can actualy read it. One thing I thought of (and haven't done anything to validate if this would work), would be embed the debug data in a Midi SysEx mesage and read it on your midi monitor on your laptop. That means you don't need two channels. I looked at SysEx when I was debugging the Line 6 pedalboard to work with Amplitube on a Mac. (Basically the Midi CC message value need to be inverted to keep the lights on pedal board synced with Amplitube, so a 1 becomes a 127 and vice versa, oddly enough Amplitube on an iPad works fine).

 

COOL!!!  At present, I managed to get USB and MIDI working over the same USB connection and thus far the simple debugging I've needed works.  It is possible (I gather) to run a proper debugger using a second PICO to allow stepping/breakpoints/etc but whilst I've got a PICO wired up to allow this I've not yet plunged into getting it working.

 

55 minutes ago, rwillett said:

I was a C/Unix/network developer for too many years (note the lack of C++ in that statement) and have written a lot of low level stuff for Microchip PIC16/18 and Arduino's for astrophotograpy. I have played with FreeRTOS as well, but not for a looong time. You might have inspired me to go back and pull the manuals out and have a play

Excellent!  I cut my teeth on multithreaded C++ in the early 90s (And even got to work with one of the gang of four in 1997!) I'm not doing anything too sophisticated from an OO perspective, but I find it helps me encapsulate/decouple things nicely.

Go on ... FreeRTOS FTW! lol

57 minutes ago, rwillett said:

This is a great thread, please keep it going.

 

Thank you! I will :)

 

S'manth x

  • Like 1
Link to comment
Share on other sites

23 minutes ago, Woodinblack said:

 

By default it doesn't, other than on the CC interface, but the beauty of it is that you can do anything you want in your own patches, so you can have a midi generator that just sends a message when you switch to a patch. It does mean knowing what is on the patch and setting the button to the same as the patch, but that was mentioned at the beginning of this

Sadly yes! I'm wondering if a CC interface is worth incorporating.  I've also been browsing the MOD codebase to see if it might be possible to interrogate plugins, it seems that this is only possible at present via the MOD-UI, but I think I could do some http API calls from Trampa to achieve this ...

 

S'manth x

Link to comment
Share on other sites

4 minutes ago, Smanth said:

Sadly yes! I'm wondering if a CC interface is worth incorporating. 

 

I think it may be an either / or - when I do my pedalboard it will just be CC, as I am not worried about controlling anything else, and that way I can set it up from the mod.

Link to comment
Share on other sites

So ...

Achieved button presses->button events->MIDI messages such that I could have ditched my MVave chocolate ... result!

 

BUT ... refactoring (Restructuring it to a better design) broke it all :(

 

I've now got a compiling but non-operational, codebase based on a thread per button, nicely encapsulated ... but not working!

 

I fear it is time for me to bite the bullet of getting the proper debugging tool working ... thankfully a week off work (mostly to attend the RCN nursing congress in Brighton) will let me spend some time sorting this (I hope!)

S'manth x

  • Sad 1
Link to comment
Share on other sites

So today I will be eating that frog

image.thumb.png.8fa1f6cf0a8047413ee53a08c40ba9c2.png

My frog is getting debugging working on my mac/pico development system.

 

My initial dev setup was achieved by following this very useful post

https://blog.smittytone.net/2021/02/02/program-raspberry-pi-pico-c-mac/

 

And my frog will be eaten using a similar approach

https://blog.smittytone.net/2021/02/05/how-to-debug-a-raspberry-pi-pico-with-a-mac-swd/

 

S'manth x

 

Link to comment
Share on other sites

So that was not straightforward!

Following the second post did not achieve the desired effect, I had to scavange the net for tweaks to actually get the debugger configured correctly.

And then ...  it kept crashing, after more digging I found that I needed to be using a slightly different version of the FreeRTOS kernel ... one that has thread safe breakpoint support.

However, debugging is now working (and pretty funky!)  and I was able to fix my refactored code so can now successfully detect button presses and fire button events.  I have a button class that instantiates a thread to process just that button (this will make it super easy to add more buttons if I want).

Next step will be to get the different button objects to dump requests to send a midi message into a thread safe queue which I will process in a single midi send thread.

S'manth x

  • Like 1
Link to comment
Share on other sites

Welcome to the wonderful world of embedded code with all it's limitations :) We're so used to just using high level libraries and seeing the output immediatly.

 

I did look to see if there was a Pico simulator and saw this https://wokwi.com/projects/new/pi-pico. It has the ability to add hardware such as buttons and displays. Looks simple but I didn't actually try and use it. It looks like you can upload libraries so I wonder if you could get the FreeRTOS libraries up? This might help, thought it might be a waste of time.

 

Rob

  • Like 2
Link to comment
Share on other sites

1 hour ago, rwillett said:

Welcome to the wonderful world of embedded code with all it's limitations :) We're so used to just using high level libraries and seeing the output immediatly.

 

I did look to see if there was a Pico simulator and saw this https://wokwi.com/projects/new/pi-pico. It has the ability to add hardware such as buttons and displays. Looks simple but I didn't actually try and use it. It looks like you can upload libraries so I wonder if you could get the FreeRTOS libraries up? This might help, thought it might be a waste of time.

 

Rob

The level of support,apps,libraries, compilers etc. that have emerged from OSS never ceases to amaze me even though I have been an OSS advocate from the days of FreeBSD 2.2.5. 

  • Like 1
Link to comment
Share on other sites

17 minutes ago, 3below said:

The level of support,apps,libraries, compilers etc. that have emerged from OSS never ceases to amaze me even though I have been an OSS advocate from the days of FreeBSD 2.2.5. 

I still remember downloading Yyradsil (?) Linux via UUCP in the early 90's. It's come on along way since then. I have some verison of BSD on our FreeNAS server, no idea what it is as it just works and has never gone wrong.

 

It sounds like you're now using n threads in the RTOS kernel to handle n buttons so letting the kernel handle the threads (and hence the buttons), so once you get one button thread right, the kernel will manage all the rest of the buttons on your behalf. Rather than solve a difficult problem, solve two simpler ones. Let the kernel do what it does best and you manage the details of the buttons.

 

It sounds like you're then putting the output all those nice isolated threads into a single threaded queue, I was thinking why are you doing that? I was thinking "If the MIDI is ready then just send it". Does MIDI require headers and footers, so you need to know the packet contents before you send it, perhaps the number of midi commands or length, or does it have a start and end limiters, so you have to know what the start and end is? Therefore you have to have it all ready to package it up and then send it? Clearly you know what you're doing, so I'm assuming that you can't just send MIDi as-is directly, you need to know a bit more about it and prepare what needs to be sent, hence the thread safe queue.  I'll read up on this.

 

Thanks

 

Rob

 

 

 

Link to comment
Share on other sites

@rwillett I'm not sure the midi actually needs a seperate thread ... however:

  • Even printf seems to be thread unsafe.
  • midi is provided by TinyUSB and I'm not sure what goes on in its depths.
  • Gut! (lol) My first proper tooth cutting on multithreading was in the early 90s, a DOS based, TUI C application (financial trading system) to use a pre-emptive multi-threading library ... lots of issues to deal with, but it worked in the end!  I just feel that having midi in its own thread is safer.

I have also rethought things and I'm going to get the buttons to just add events to the Q and let an event processor figure out what to do (OSC ... maybe one day), this seems to lend itself to dealing with interactions between multiple buttons (Two pressed at once).

 

That online PICO emulator looks pretty cool!

 

S'manth x

Edited by Smanth
Link to comment
Share on other sites

14 hours ago, rwillett said:

It sounds like you're then putting the output all those nice isolated threads into a single threaded queue, I was thinking why are you doing that? I was thinking "If the MIDI is ready then just send it". Does MIDI require headers and footers, so you need to know the packet contents before you send it, perhaps the number of midi commands or length, or does it have a start and end limiters, so you have to know what the start and end is? Therefore you have to have it all ready to package it up and then send it? Clearly you know what you're doing, so I'm assuming that you can't just send MIDi as-is directly, you need to know a bit more about it and prepare what needs to be sent, hence the thread safe queue.  I'll read up on this.

 

MIDI messages are pretty simple, each message consists of an initial byte which defines the message type, generally followed by either one or two bytes which are the parameter values for that message. For example, a note on message will have the channel number (1-16, which IIRC is actually encoded as 0-15) in the message type byte, a second byte defining the note number, and a third defining the velocity. There's going to have to be some sort of queuing involved, as sending two MIDI messages simultaneously would be a recipe for disaster.

 

Unfortunately I can't be much help as my programming career has been 95% COBOL, with tiny bits of exposure to C, C++, C#, JavaScript, and Python.

  • Like 1
Link to comment
Share on other sites

2 hours ago, tauzero said:

Unfortunately I can't be much help as my programming career has been 95% COBOL, with tiny bits of exposure to C, C++, C#, JavaScript, and Python.

Thanks @tauzero, COBOL & FORTRAN are two I've never really used; LISP et al are well outside my experience. PICK is my most esoteric! Pascal/C/C++/C#/TS is my area of ... playing.

S'manth x

Link to comment
Share on other sites

10 minutes ago, Richard R said:

Fortan was pretty damn' impressive back in '77, and was still in use in 1990 for academic work when I had to do some analysis with it. Quite glad to have left it behind.

This low-level stuff is super-impressive though. 

:hi:

 

Just to blow your mind, apparently there is an Object Orientated Fortran. that’s way beyond my skill level as I’ve never used Fortran at all. Happy about that and will keep it that way. 
 

just downloaded FreeRTOS and will see if the pick simulator will handle it

  • Like 1
Link to comment
Share on other sites

Not wishing to derail the thread, the pico (and others e.g.ESP32) simulator  http:// https://wokwi.com/projects/new/pi-pico that @rwillett pointed out is pretty damn amazing.  I have only played with the online version with virtual hardware. Highly impressed so far.  It apparently interfaces with VS CODE as well.

Edited by 3below
  • Like 2
Link to comment
Share on other sites

On 14/05/2023 at 22:20, 3below said:

Not wishing to derail the thread, the pico (and others e.g.ESP32) simulator  http:// https://wokwi.com/projects/new/pi-pico that @rwillett pointed out is pretty damn amazing.  I have only played with the online version with virtual hardware. Highly impressed so far.  It apparently interfaces with VS CODE as well.

 

Broken link - https://wokwi.com/projects/new/pi-pico is what you want.

  • Like 1
Link to comment
Share on other sites

  • 2 weeks later...

So some progress with the codebase to add a task to act as an eventManager.  This will be sent Button/Expression and MIDI events (via a thread safe queue) and then decide what needs doing, such as sending a MIDI message, updating a display (eventually), changing the button configuration bank etc.

 

Off work today and I'm taking the opportunity to do some design for the enclosure for the Trampa.  I was finally inducted on my makerspace 3D printers and I am using them to:

  • Print replacement parts for my own 3D printer which melted in the fire. Hopefully when I get these done (Many more hours of printing sadly) I will be able to get Patty the Printer running again ... the drive to the makerspace, trying to find a place to park in Brighton and the drive home all consume time that I could actually be printing! lol
  • Produce some Trampa test prints to hone the enclosure design.

A fun day with a working 3D printer (two in this case) and other bits of techie joy.

IMG_1003.thumb.png.2401821ad78313c03a26c9e4aa105376.png

S'manth x

  • Like 3
Link to comment
Share on other sites

On 14/05/2023 at 18:26, rwillett said:

Just to blow your mind, apparently there is an Object Orientated Fortran.

 

That reminds me of when I did some work at a flight simulator company, a lot of the engineers had started in Fortran and migrated to C.

They migrated syntax, but some never mastered the new idioms, and were writing what became known as C-tran

  • Haha 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...