Thursday, November 15, 2012

Auduino With Delay

This is the first in a series of posts introducing simple audio effects that can be used in micro controller projects.

Next Week - Bit Crushing effects

The Delay effect is one of the simplest and most effective enhancements we can add to our audio projects.

The delay effect works by recording the output as it is being generated and then mixing this sound back in with itself - after a delay. The result should be familiar to anyone who has every played an electric guitar through an amp with reverb.

In the case of the Auduino synthesizer the result is a mild echo effect and slightly smoother, more metallic sound - the effect can be turned on or off through a push button in the code provided below.

How do we create the delay effect
Delay is very simple to add in a microcontroller, all we need is a block of memory to record the output in.

The larger the block of memory, the longer the delay we can record and the deeper the effect.

In this case we are using a 1K block of memory in the array named sDelayBuffer -

// Duane B
// rcarduino.blogspot.com
// 15/11/2012
// Very simple ring buffer delay
// we record the output in this array
// and then mix it back in with the output as the buffer wraps around
// can be switched on and off by a button on DELAY_BUTTON
#define MAX_DELAY 1024
unsigned char sDelayBuffer[MAX_DELAY];
unsigned int nDelayCounter = 0;
unsigned char bDelay;

The other modification is inside the interrupt service routine which generates the Audiuno output, essentially what we are doing is adding the sound we recorded 1/8th of a second ago on top of the current output value -


  // Duane B
  // rcarduino.blogspot.com
  // 15/11/2012
  // add a button to set bDelay true or false to turn delay on and off
  if(bDelay)
  {
    // Output to PWM (this is faster than using analogWrite) 
    // Here we add the delay buffer to the output value, this produces
    // an subtle echo effect, the delay buffer is effectivley replaying the sound from
    // 1/8th of a second ago.
 
    LED_PORT |= 1 << LED_BIT; // Faster than using digitalWrite
    PWM_VALUE = (output + (sDelayBuffer[nDelayCounter]))>>1;
   
    // add the new output to the buffer so we can use it when the buffer next wraps around
    sDelayBuffer[nDelayCounter] = PWM_VALUE;
    nDelayCounter++;
    if(nDelayCounter == MAX_DELAY)
    {
      nDelayCounter = 0;
    }
  }
  else
  {
    LED_PORT &= ~(1 << LED_BIT); // Faster than using digitalWrite
   
    PWM_VALUE = output;
  }

We test whether delay is enabled, if it is we calculate the output value by adding the initial output to the earlier recorded output from our delay buffer. After outputting this combined value we record it in the delay buffer replacing the value we just used. Over time, the code cycles through the delay buffer over and over again, mixing the current output with a sample from 1/8th of a second back - a bit like playing your instrument in a large hall where the distinct sound is the result of the current sound being constantly mixed with its echo.

Thats all there is to generating delay in a micro controller synth engine - exactly the same code is used to create the delay effect in the RCArduino Five Dollar Synthesizer.


The RCArduino Five Dollar Synthesizer is another audio project enhanced with this delay effect -

http://rcarduino.blogspot.com/2012/10/five-dollar-synthesiser.html 


Further Development
The amount of delay we can provide is determined to the memory we use to record the samples. In the Auduino we are using 1K which at an 8K play back rate gives us 125ms of delay. This can be increased by bit crushing the samples - using 4 bits per sample we get 250ms, 2 bits gets us half a second, with 1 bit we can get a whole second. Unfortunately initial experiments suggest that the effect is largely lost when applying these techniques, its a bit like shouting into a cave and getting a different echo back - your ears just don't buy it.

Auduino Accreditation
The Auduino is an original work by Peter Knight, the original project can be found here -
http://code.google.com/p/tinkerit/wiki/Auduino

Auduino with delay

Auduino with delay is a very slight modification by Duane B (rcarduino) to the original work of Peter Knight.

Notes
- This code also include the volatile fix which allows the Auduino to work correctly in Arduino 1.0 and later
- Remember to use a pull up or pull down resistor if you are not using a push button or switch for the delay button or if your more comfortable modifying the code, replace the button code with true or false.
- LED 13, now indicates whether delay is on or off.


// Auduino, the Lo-Fi granular synthesiser
//
// by Peter Knight, Tinker.it http://tinker.it
//
// Help:      http://code.google.com/p/tinkerit/wiki/Auduino
// More help: http://groups.google.com/group/auduino
//
// Analog in 0: Grain 1 pitch
// Analog in 1: Grain 2 decay
// Analog in 2: Grain 1 decay
// Analog in 3: Grain 2 pitch
// Analog in 4: Grain repetition frequency
//
// Digital 3: Audio out (Digital 11 on ATmega8)
//
// Changelog:
// 19 Nov 2008: Added support for ATmega8 boards
// 21 Mar 2009: Added support for ATmega328 boards
// 7 Apr 2009: Fixed interrupt vector for ATmega328 boards
// 8 Apr 2009: Added support for ATmega1280 boards (Arduino Mega)

#include <avr/io.h>
#include <avr/interrupt.h>

uint16_t syncPhaseAcc;
volatile uint16_t syncPhaseInc;
uint16_t grainPhaseAcc;
volatile uint16_t grainPhaseInc;
uint16_t grainAmp;
volatile uint8_t grainDecay;
uint16_t grain2PhaseAcc;
volatile uint16_t grain2PhaseInc;
uint16_t grain2Amp;
volatile uint8_t grain2Decay;

// Map Analogue channels
#define SYNC_CONTROL         (4)
#define GRAIN_FREQ_CONTROL   (0)
#define GRAIN_DECAY_CONTROL  (2)
#define GRAIN2_FREQ_CONTROL  (3)
#define GRAIN2_DECAY_CONTROL (1)

// DB
#define SMOOTH_PIN 8


// Changing these will also requires rewriting audioOn()

#if defined(__AVR_ATmega8__)
//
// On old ATmega8 boards.
//    Output is on pin 11
//
#define LED_PIN       13
#define LED_PORT      PORTB
#define LED_BIT       5
#define PWM_PIN       11
#define PWM_VALUE     OCR2
#define PWM_INTERRUPT TIMER2_OVF_vect



#elif defined(__AVR_ATmega1280__)
//
// On the Arduino Mega
//    Output is on pin 3
//
#define LED_PIN       13
#define LED_PORT      PORTB
#define LED_BIT       7
#define PWM_PIN       3
#define PWM_VALUE     OCR3C
#define PWM_INTERRUPT TIMER3_OVF_vect
#else
//
// For modern ATmega168 and ATmega328 boards
//    Output is on pin 3
//
#define PWM_PIN       3
#define PWM_VALUE     OCR2B
#define LED_PIN       13
#define LED_PORT      PORTB
#define LED_BIT       5
#define PWM_INTERRUPT TIMER2_OVF_vect
#endif

// Duane B
// rcarduino.blogspot.com
// 15/11/2012
// Very simple ring buffer delay
// we record the output in this array
// and then mix it back in with the output as the buffer wraps around
// can be switched on and off by a button on DELAY_BUTTON
#define MAX_DELAY 1024
unsigned char sDelayBuffer[MAX_DELAY];
unsigned int nDelayCounter = 0;
unsigned char bDelay;

#define DELAY_BUTTON 4


// Smooth logarithmic mapping
//
uint16_t antilogTable[] = {
  64830,64132,63441,62757,62081,61413,60751,60097,59449,58809,58176,57549,56929,56316,55709,55109,
  54515,53928,53347,52773,52204,51642,51085,50535,49991,49452,48920,48393,47871,47356,46846,46341,
  45842,45348,44859,44376,43898,43425,42958,42495,42037,41584,41136,40693,40255,39821,39392,38968,
  38548,38133,37722,37316,36914,36516,36123,35734,35349,34968,34591,34219,33850,33486,33125,32768
};
uint16_t mapPhaseInc(uint16_t input) {
  return (antilogTable[input & 0x3f]) >> (input >> 6);
}

// Stepped chromatic mapping
//
uint16_t midiTable[] = {
  17,18,19,20,22,23,24,26,27,29,31,32,34,36,38,41,43,46,48,51,54,58,61,65,69,73,
  77,82,86,92,97,103,109,115,122,129,137,145,154,163,173,183,194,206,218,231,
  244,259,274,291,308,326,346,366,388,411,435,461,489,518,549,581,616,652,691,
  732,776,822,871,923,978,1036,1097,1163,1232,1305,1383,1465,1552,1644,1742,
  1845,1955,2071,2195,2325,2463,2610,2765,2930,3104,3288,3484,3691,3910,4143,
  4389,4650,4927,5220,5530,5859,6207,6577,6968,7382,7821,8286,8779,9301,9854,
  10440,11060,11718,12415,13153,13935,14764,15642,16572,17557,18601,19708,20879,
  22121,23436,24830,26306
};
uint16_t mapMidi(uint16_t input) {
  return (midiTable[(1023-input) >> 3]);
}

// Stepped Pentatonic mapping
//
uint16_t pentatonicTable[54] = {
  0,19,22,26,29,32,38,43,51,58,65,77,86,103,115,129,154,173,206,231,259,308,346,
  411,461,518,616,691,822,923,1036,1232,1383,1644,1845,2071,2463,2765,3288,
  3691,4143,4927,5530,6577,7382,8286,9854,11060,13153,14764,16572,19708,22121,26306
};

uint16_t mapPentatonic(uint16_t input) {
  uint8_t value = (1023-input) / (1024/53);
  return (pentatonicTable[value]);
}


void audioOn() {
#if defined(__AVR_ATmega8__)
  // ATmega8 has different registers
  TCCR2 = _BV(WGM20) | _BV(COM21) | _BV(CS20);
  TIMSK = _BV(TOIE2);
#elif defined(__AVR_ATmega1280__)
  TCCR3A = _BV(COM3C1) | _BV(WGM30);
  TCCR3B = _BV(CS30);
  TIMSK3 = _BV(TOIE3);
#else
  // Set up PWM to 31.25kHz, phase accurate
  TCCR2A = _BV(COM2B1) | _BV(WGM20);
  TCCR2B = _BV(CS20);
  TIMSK2 = _BV(TOIE2);
#endif
}


void setup() {
  pinMode(PWM_PIN,OUTPUT);
  audioOn();
  pinMode(LED_PIN,OUTPUT);

  pinMode(DELAY_BUTTON,INPUT);
 
  // set pin mode and turn on pull up so that default mode
  // is PENTATONIC, pull the pin low to switch to smooth
  pinMode(SMOOTH_PIN,INPUT);
  digitalWrite(SMOOTH_PIN,HIGH);
}

void loop() {
  // The loop is pretty simple - it just updates the parameters for the oscillators.
  //
  // Avoid using any functions that make extensive use of interrupts, or turn interrupts off.
  // They will cause clicks and poops in the audio.
 
  // defaults to pentatonic stepped tones, pull pin low for smooth frequency without distinct tones
  //    syncPhaseInc = mapPhaseInc(analogRead(SYNC_CONTROL)) / 4;

  syncPhaseInc = mapPentatonic(analogRead(SYNC_CONTROL));
 
  // updated 29/01/2013
  // pull the DELAY_BUTTON pin high for delay, low for no delay
  // use either a pull up/pull down resistor
  // or a pull up resistor with a toggle switch between the pin and ground
  bDelay = digitalRead(DELAY_BUTTON);
 
  // Stepped mapping to MIDI notes: C, Db, D, Eb, E, F...
  //syncPhaseInc = mapMidi(analogRead(SYNC_CONTROL));
 
  // Stepped pentatonic mapping: D, E, G, A, B
 

  grainPhaseInc  = mapPhaseInc(analogRead(GRAIN_FREQ_CONTROL)) / 2;
  grainDecay     = analogRead(GRAIN_DECAY_CONTROL) / 8;
  grain2PhaseInc = mapPhaseInc(analogRead(GRAIN2_FREQ_CONTROL)) / 2;
  grain2Decay    = analogRead(GRAIN2_DECAY_CONTROL) / 4;
}


SIGNAL(PWM_INTERRUPT)
{
  uint8_t value;
  uint16_t output;

  syncPhaseAcc += syncPhaseInc;
  if (syncPhaseAcc < syncPhaseInc) {
    // Time to start the next grain
    grainPhaseAcc = 0;
    grainAmp = 0x7fff;
    grain2PhaseAcc = 0;
    grain2Amp = 0x7fff;
//    LED_PORT ^= 1 << LED_BIT; // Faster than using digitalWrite
  }
 
  // Increment the phase of the grain oscillators
  grainPhaseAcc += grainPhaseInc;
  grain2PhaseAcc += grain2PhaseInc;

  // Convert phase into a triangle wave
  value = (grainPhaseAcc >> 7) & 0xff;
  if (grainPhaseAcc & 0x8000) value = ~value;
  // Multiply by current grain amplitude to get sample
  output = value * (grainAmp >> 8);

  // Repeat for second grain
  value = (grain2PhaseAcc >> 7) & 0xff;
  if (grain2PhaseAcc & 0x8000) value = ~value;
  output += value * (grain2Amp >> 8);

  // Make the grain amplitudes decay by a factor every sample (exponential decay)
  grainAmp -= (grainAmp >> 8) * grainDecay;
  grain2Amp -= (grain2Amp >> 8) * grain2Decay;

  // Scale output to the available range, clipping if necessary
  output >>= 9;
  if (output > 255) output = 255;

  // Duane B
  // rcarduino.blogspot.com
  // 15/11/2012
  // add a button to set bDelay true or false to turn delay on and off
  if(bDelay)
  {
    // Output to PWM (this is faster than using analogWrite) 
    // Here we add the delay buffer to the output value, this produces
    // an subtle echo effect, the delay buffer is effectivley replaying the sound from
    // 1/8th of a second ago.
 
    LED_PORT |= 1 << LED_BIT; // Faster than using digitalWrite
    PWM_VALUE = (output + (sDelayBuffer[nDelayCounter]))>>1;
   
    // add the new output to the buffer so we can use it when the buffer next wraps around
    sDelayBuffer[nDelayCounter] = PWM_VALUE;
    nDelayCounter++;
    if(nDelayCounter == MAX_DELAY)
    {
      nDelayCounter = 0;
    }
  }
  else
  {
    LED_PORT &= ~(1 << LED_BIT); // Faster than using digitalWrite
   
    PWM_VALUE = output;
  }
}


24 comments:

  1. This is gonna sound silly, but how should the button for the delay be wired?

    Thank you so much for updating this code!

    ReplyDelete
    Replies
    1. LEGIT FULLZ & TOOLS STORE

      Hello to All !

      We are offering all types of tools & Fullz on discounted price.
      If you are in search of anything regarding fullz, tools, tutorials, Hack Pack, etc
      Feel Free to contact

      ***CONTACT 24/7***
      **Telegram > @leadsupplier
      **ICQ > 752822040
      **Skype > Peeterhacks
      **Wicker me > peeterhacks

      "SSN LEADS/FULLZ AVAILABLE"
      "TOOLS & TUTORIALS AVAILABLE FOR HACKING, SPAMMING,
      CARDING, CASHOUT, CLONING, SCRIPTING ETC"

      **************************************
      "Fresh Spammed SSN Fullz info included"
      >>SSN FULLZ with complete info
      >>CC With CVV (vbv & non vbv) Fullz USA
      >>FULLZ FOR SBA, PUA & TAX RETURN FILLING
      >>USA I.D Photos Front & Back
      >>High Credit Score fullz (700+ Scores)
      >>DL number, Employee Details, Bank Details Included
      >>Complete Premium Info with Relative Info

      ***************************************
      COMPLETE GUIDE FOR TUTORIALS & TOOLS

      "SPAMMING" "HACKING" "CARDING" "CASH OUT"
      "KALI LINUX" "BLOCKCHAIN BLUE PRINTS" "SCRIPTING"
      "FRAUD BIBLE"

      "TOOLS & TUTORIALS LIST"
      =>Ethical Hacking Ebooks, Tools & Tutorials
      =>Bitcoin Hacking
      =>Kali Linux
      =>Fraud Bible
      =>RAT
      =>Keylogger & Keystroke Logger
      =>Whatsapp Hacking & Hacked Version of Whatsapp
      =>Facebook & Google Hacking
      =>Bitcoin Flasher
      =>SQL Injector
      =>Premium Logs (PayPal/Amazon/Coinbase/Netflix/FedEx/Banks)
      =>Bitcoin Cracker
      =>SMTP Linux Root
      =>Shell Scripting
      =>DUMPS with pins track 1 and 2 with & without pin
      =>SMTP's, Safe Socks, Rdp's brute
      =>PHP mailer
      =>SMS Sender & Email Blaster
      =>Cpanel
      =>Server I.P's & Proxies
      =>Viruses & VPN's
      =>HQ Email Combo (Gmail, Yahoo, Hotmail, MSN, AOL, etc.)

      *Serious buyers will always welcome
      *Price will be reduce in bulk order
      *Discount offers will gives to serious buyers
      *Hope we do a great business together

      ===>Contact 24/7<===
      ==>Telegram > @leadsupplier
      ==>ICQ > 752822040
      ==>Skype > Peeterhacks
      ==>Wicker me > peeterhacks

      Delete
  2. Like this - http://arduino.cc/en/Tutorial/Button but connected to digital pin 4

    Duane B

    ReplyDelete
  3. Hi Duane, I've built my Auduino in a breadboard with a china made Arduino Duemilanove (Not official that is), could that be the reason it doesn't work? Wiring is OK, but only the first potentiometer seems to work. Wiring is not the problem, as if I switch cables on the breadboard strange noises happen :)

    Regards,

    Pancho

    ReplyDelete
  4. Hi,
    If only one pot seems to work, this is usually because the 'volatile' fix is not being used in the code.

    Are you using the exact code above or the code from the original tinkerkit link, the tinkerkit code has the bug and does not look as if anyone is every going to update it, the code above has the fix.

    Duane B

    ReplyDelete
  5. Hi Duane,

    im just wondering what the smooth pin 8 is for is something supposed to be connected to that pin?

    thanks

    ReplyDelete
  6. Hi,
    At one point it was possible to choose which scale the Auduino would use by changing this line based on user input - syncPhaseInc = mapPentatonic(analogRead(SYNC_CONTROL)); however having tried many different scales and smooth transitions without scales - this option gives the best results. You can try adding other scales by adding a new map function, the chromatic scale is still in the code so an easy place to start. Also check the blog in a few days, I have finished a really nice resonant low pass filter for Arduino audio projects that I will write up and post over the next few days. Duane.

    ReplyDelete
  7. Hi Duane
    I am trying to upload this code to arduino Micro. But there is problem - TCCR2A was not declared as scope - and higlighted part of the code is TCCR2A = _BV(COM2B1) | _BV(WGM20);. Do you have any solutions or maybe code for arduino micro.

    pls REPLAY
    thanks

    ReplyDelete
    Replies
    1. Try TCCR2 without the A for the Micro.

      Duane B

      Delete
    2. Update, try this as well, change the audioOn function from this -

      void audioOn() {
      #if defined(__AVR_ATmega8__)
      // ATmega8 has different registers
      TCCR2 = _BV(WGM20) | _BV(COM21) | _BV(CS20);
      TIMSK = _BV(TOIE2);
      #elif defined(__AVR_ATmega1280__)
      TCCR3A = _BV(COM3C1) | _BV(WGM30);
      TCCR3B = _BV(CS30);
      TIMSK3 = _BV(TOIE3);
      #else
      // Set up PWM to 31.25kHz, phase accurate
      TCCR2A = _BV(COM2B1) | _BV(WGM20);
      TCCR2B = _BV(CS20);
      TIMSK2 = _BV(TOIE2);
      #endif
      }

      to this -

      void audioOn() {
      TCCR2 = _BV(WGM20) | _BV(COM21) | _BV(CS20);
      TIMSK = _BV(TOIE2);
      }

      Duane B

      Delete
    3. still same problem...

      error copy:

      auduino_v5.ino: In function 'void audioOn()':
      auduino_v5:147: error: 'TCCR2' was not declared in this scope
      auduino_v5:147: error: 'WGM20' was not declared in this scope
      auduino_v5:147: error: 'COM21' was not declared in this scope
      auduino_v5:147: error: 'CS20' was not declared in this scope
      auduino_v5:148: error: 'TIMSK' was not declared in this scope
      auduino_v5.ino: In function 'void TIMER2_OVF_vect()':
      auduino_v5:245: error: 'OCR2B' was not declared in this scope
      auduino_v5:259: error: 'OCR2B' was not declared in this scope

      Delete
  8. The micro uses a different chip, an ATMega32u4, its very similar to the ATMega328 in the Arduino UNO however if your accessing the chip registers directly some of them are named differently. To convert the timer counter registers from ATMega328 to ATMega32u4, refer to page 89 onwards of the datasheet here - http://www.atmel.com/Images/doc7766.pdf

    Duane B

    ReplyDelete
  9. hi i'm new at this, wich libraries should i download? or they come with the arduino software by default?

    ReplyDelete
  10. Hi DuaneB I was wondering how is your code licensed to the public, as gnu licensed or what else?

    ReplyDelete

  11. I found this is an informative blog and also very useful and knowledgeable. I would like to thank you for the efforts you have made in writing this blog


    buy modalert online

    buy artvigil online
    buy waklert online
    buy modvigil online

    buy vilafinil online


    ReplyDelete
  12. LEGIT FULLZ & TOOLS STORE

    Hello to All !

    We are offering all types of tools & Fullz on discounted price.
    If you are in search of anything regarding fullz, tools, tutorials, Hack Pack, etc
    Feel Free to contact

    ***CONTACT 24/7***
    **Telegram > @leadsupplier
    **ICQ > 752822040
    **Skype > Peeterhacks
    **Wicker me > peeterhacks

    "SSN LEADS/FULLZ AVAILABLE"
    "TOOLS & TUTORIALS AVAILABLE FOR HACKING, SPAMMING,
    CARDING, CASHOUT, CLONING, SCRIPTING ETC"

    **************************************
    "Fresh Spammed SSN Fullz info included"
    >>SSN FULLZ with complete info
    >>CC With CVV (vbv & non vbv) Fullz USA
    >>FULLZ FOR SBA, PUA & TAX RETURN FILLING
    >>USA I.D Photos Front & Back
    >>High Credit Score fullz (700+ Scores)
    >>DL number, Employee Details, Bank Details Included
    >>Complete Premium Info with Relative Info

    ***************************************
    COMPLETE GUIDE FOR TUTORIALS & TOOLS

    "SPAMMING" "HACKING" "CARDING" "CASH OUT"
    "KALI LINUX" "BLOCKCHAIN BLUE PRINTS" "SCRIPTING"
    "FRAUD BIBLE"

    "TOOLS & TUTORIALS LIST"
    =>Ethical Hacking Ebooks, Tools & Tutorials
    =>Bitcoin Hacking
    =>Kali Linux
    =>Fraud Bible
    =>RAT
    =>Keylogger & Keystroke Logger
    =>Whatsapp Hacking & Hacked Version of Whatsapp
    =>Facebook & Google Hacking
    =>Bitcoin Flasher
    =>SQL Injector
    =>Premium Logs (PayPal/Amazon/Coinbase/Netflix/FedEx/Banks)
    =>Bitcoin Cracker
    =>SMTP Linux Root
    =>Shell Scripting
    =>DUMPS with pins track 1 and 2 with & without pin
    =>SMTP's, Safe Socks, Rdp's brute
    =>PHP mailer
    =>SMS Sender & Email Blaster
    =>Cpanel
    =>Server I.P's & Proxies
    =>Viruses & VPN's
    =>HQ Email Combo (Gmail, Yahoo, Hotmail, MSN, AOL, etc.)

    *Serious buyers will always welcome
    *Price will be reduce in bulk order
    *Discount offers will gives to serious buyers
    *Hope we do a great business together

    ===>Contact 24/7<===
    ==>Telegram > @leadsupplier
    ==>ICQ > 752822040
    ==>Skype > Peeterhacks
    ==>Wicker me > peeterhacks

    ReplyDelete
  13. The increasing popularity of ready-to-drink products is also increasing the demand for coffee concentrates in the market. The high paced lifestyles of consumers are shifting the choice of people from traditional coffee to the concentrated form. Additionally, the use of instant coffee in homes and offices is likely to create opportunities for the coffee concentrate market.


    Also Read: acquired disease testing market, Latin America Generic Injectables Market.

    ReplyDelete
  14. Good day everyone, San Wilson here! If you are wondering how many pages is 1000 words, contact me any time and I will provide you with full guidance.

    ReplyDelete
  15. Hi Everyone! My name is Michael and I am an experienced author working at an independent tech-support firm

    ReplyDelete
  16. allbet casino online gambling service provider that meets international standards and meets all gambler needs.

    ReplyDelete
  17. naza8k We are number 1 in total entertainment.

    ReplyDelete
  18. Oh my goodness, this is so exciting! I absolutely have to investigate further!

    ReplyDelete
  19. CMOLDS leads as the premier mobile app development company uae, offering cutting-edge solutions tailored to your business needs. Elevate your digital presence with our expert team crafting innovative mobile experiences.

    ReplyDelete