Description

Cubo: An unorthodox 3D box game-like design tool.

  • Simple to use and navigate in its dome space.
  • 20x20x20 cube volume.
  • Paint cubes with hex RGB color wheel.
  • 3D Animation with desired speed.
  • Save/load your project to retrieve whenever you want.
  • Save as .obj file.
  • Additional instructions are provided in its menu.
Cubo Retro GIF

Instagram: cubo

download it from Itch.io: itch.io/achibaba

Pencere: Simple Png editor.

  • Create png files with RGBA colors.
  • Undo for every operation.
  • Save / load png files.

download it from Itch.io: itch.io/achibaba

ESP32 Drum Step Sequencer: Here is my ESP32 project — a 16-step drum sequencer. It's easy to get the parts: 4x4 matrix buttons, a rotary encoder, SD card reader, and a 128x64 OLED display. You can download the full .ino zip file below.

  • Kick, Snare, Hi-Hat samples
  • Bluetooth A2DP output
  • Real-time step sequencing via matrix buttons
  • SD card support for loading custom samples
  • Rotary encoder UI with OLED feedback
  • Adjustable BPM
ESP32 Drum Sequencer Front ESP32 Drum Sequencer Display

📂 Main ino code is below , view Full Code on GitHub


#include <Arduino.h>
#include <BluetoothA2DPSource.h>

#include <U8g2lib.h>
#include <Wire.h>

#include "kick.h"
#include "snare.h"
#include "hihat.h"

#include <SPI.h>

#include "SDCard.h"
#include "Tus.h"
#include "RotaryEncoder.h"

// --- Keypad Setup
Tus tus;
// --- Rotary Encoder Pins
RotaryEncoder encoder; // uses default pins: 32, 33, 25

enum UIState { NAVIGATE, BPM_EDIT, SUB_SD_KICK ,SUB_SD_SNARE,SUB_SD_HIHAT};
UIState uiState = NAVIGATE;

enum Instrument { KICK, SNARE, HIHAT };
Instrument currentInstrument = KICK;

enum MenuItem { MENU_KICK, MENU_SNARE, MENU_HIHAT, MENU_BPM, MENU_COUNT };
MenuItem rotatedItem = MENU_KICK;

enum KickType { STANDARD_KICK, SD_DATA_SAMPLE_KICK };
KickType currentKick = STANDARD_KICK;

enum SnareType { STANDARD_SNARE, SD_DATA_SAMPLE_SNARE };
SnareType currentSnare = STANDARD_SNARE;

enum HihatType { STANDARD_HIHAT, SD_DATA_SAMPLE__HIHAT };
HihatType currentHihat = STANDARD_HIHAT;

SDCard sd;
#define SAMPLE_SIZE (13 * 1024 / 2)

int sampleLength1_sd_kick = 0;
int16_t buffer1_sd_kick[SAMPLE_SIZE];
int index_sd_kick = 0;

int sampleLength2_sd_snare = 0;
int16_t buffer2_sd_snare [SAMPLE_SIZE];
int index_sd_snare = 0;

int sampleLength3_sd_hihat= 0;
int16_t buffer3_sd_hihat [SAMPLE_SIZE];
int index_sd_hihat = 0;

#define OLED_SDA 4
#define OLED_SCL 15
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

BluetoothA2DPSource a2dp_source;
const int sampleRate = 44100;
float bpm = 60.0f;
int step_duration_samples = (int)((60.0f / bpm / 4.0f) * sampleRate);

const int NUM_STEPS = 16;
bool kick_steps[NUM_STEPS] = {false};
bool snare_steps[NUM_STEPS] = {false};
bool hihat_steps[NUM_STEPS] = {false};
int current_step = 0;
size_t sample_in_step = 0;

bool playing_kick = false, playing_snare = false, playing_hihat = false;
size_t kick_sample_index = 0, snare_sample_index = 0, hihat_sample_index = 0;

int32_t audio_data_callback(uint8_t *data, int32_t len) {
  int16_t *buffer = (int16_t *)data;
  int samples = len / 2;

  for (int i = 0; i < samples; i++) {
    if (sample_in_step == 0) {
      if (kick_steps[current_step]) { playing_kick = true; kick_sample_index = 0; }
      if (snare_steps[current_step]) { playing_snare = true; snare_sample_index = 0; }
      if (hihat_steps[current_step]) { playing_hihat = true; hihat_sample_index = 0; }
    }

    int32_t sample = 0;

    if (playing_kick) {
      if (currentKick == STANDARD_KICK && kick_sample_index < kick_data_len) {
        sample += kick_data[kick_sample_index++];
      } else if (currentKick == SD_DATA_SAMPLE_KICK && kick_sample_index < sampleLength1_sd_kick) {
        sample += buffer1_sd_kick[kick_sample_index++];
      } else playing_kick = false;
    }

    if (playing_snare) {
      if (currentSnare == STANDARD_SNARE && snare_sample_index < snare_data_len) {
        sample += snare_data[snare_sample_index++];
      } else if (currentSnare == SD_DATA_SAMPLE_SNARE && snare_sample_index < sampleLength2_sd_snare) {
        sample += buffer2_sd_snare[snare_sample_index++];
      } else playing_snare = false;
    }

    if (playing_hihat) {
      if (currentHihat == STANDARD_HIHAT && hihat_sample_index < hihat_data_len) {
        sample += hihat_data[hihat_sample_index++];
      } else if (currentHihat == SD_DATA_SAMPLE__HIHAT && hihat_sample_index < sampleLength3_sd_hihat) {
        sample += buffer3_sd_hihat[hihat_sample_index++];
      } else playing_hihat = false;
    }

    sample = constrain(sample, -32768, 32767);
    buffer[i] = (int16_t)sample;

    sample_in_step++;
    if (sample_in_step >= step_duration_samples) {
      sample_in_step = 0;
      current_step = (current_step + 1) % NUM_STEPS;
    }
  }

  return len;
}

void drawSequencerState() {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_5x7_tr);
  char status[32] = "";

  u8g2.drawStr(0, 7, "Kck");
  u8g2.drawStr(0, 23, "Snr");
  u8g2.drawStr(0, 39, "Hht");

  for (int i = 0; i < NUM_STEPS; i++) {
    int x = 20 + i * 6;
    if (kick_steps[i]) u8g2.drawBox(x, 0, 5, 5); else u8g2.drawFrame(x, 0, 5, 5);
    if (snare_steps[i]) u8g2.drawBox(x, 16, 5, 5); else u8g2.drawFrame(x, 16, 5, 5);
    if (hihat_steps[i]) u8g2.drawBox(x, 32, 5, 5); else u8g2.drawFrame(x, 32, 5, 5);
  }

  int cursorX = 20 + current_step * 6;
  u8g2.drawLine(cursorX, 0, cursorX, 38);

  if (uiState == NAVIGATE) {
    switch (rotatedItem) {
      case MENU_KICK: strcpy(status, "SELECT: KICK"); break;
      case MENU_SNARE: strcpy(status, "SELECT: SNARE"); break;
      case MENU_HIHAT: strcpy(status, "SELECT: HIHAT"); break;
      case MENU_BPM: sprintf(status, "SELECT: BPM (%.0f)", bpm); break;
    }
  } else if (uiState == BPM_EDIT) {
    sprintf(status, "EDIT BPM: %.0f", bpm);
  } else if (uiState == SUB_SD_KICK) {
    strcpy(status, sd.sampleNames[index_sd_kick].c_str());
  } else if (uiState == SUB_SD_SNARE) {
    strcpy(status, sd.sampleNames[index_sd_snare].c_str());
  } else if (uiState == SUB_SD_HIHAT) {
    strcpy(status, sd.sampleNames[index_sd_hihat].c_str());
  }

  u8g2.drawStr(0, 60, status);
  u8g2.sendBuffer();
}

void setup() {
  Serial.begin(115200);
  sd.begin();
  sd.saveSampleNames("/seq");
  sd.printStoredFileNames();

  Wire.begin(OLED_SDA, OLED_SCL);
  u8g2.begin();

  encoder.begin();

  a2dp_source.set_local_name("ESP32_DrumSeq");
  a2dp_source.set_data_callback(audio_data_callback);
  a2dp_source.start("NOVA MINI");
}

void loop() {
  encoder.loop();

  if (encoder.wasButtonPressed()) {
    switch (uiState) {
      case NAVIGATE:
        if (rotatedItem == MENU_KICK) uiState = SUB_SD_KICK;
        else if (rotatedItem == MENU_SNARE) uiState = SUB_SD_SNARE;
        else if (rotatedItem == MENU_HIHAT) uiState = SUB_SD_HIHAT;
        else if (rotatedItem == MENU_BPM) uiState = BPM_EDIT;
        break;

      case BPM_EDIT:
        uiState = NAVIGATE;
        break;

      case SUB_SD_KICK:
        if (sd.readSampleToBuffer16Bit(index_sd_kick, buffer1_sd_kick, SAMPLE_SIZE, sampleLength1_sd_kick)) {
          currentKick = SD_DATA_SAMPLE_KICK;
        } else currentKick = STANDARD_KICK;
        uiState = NAVIGATE;
        break;

      case SUB_SD_SNARE:
        if (sd.readSampleToBuffer16Bit(index_sd_snare, buffer2_sd_snare, SAMPLE_SIZE, sampleLength2_sd_snare)) {
          currentSnare = SD_DATA_SAMPLE_SNARE;
        } else currentSnare = STANDARD_SNARE;
        uiState = NAVIGATE;
        break;

      case SUB_SD_HIHAT:
        if (sd.readSampleToBuffer16Bit(index_sd_hihat, buffer3_sd_hihat, SAMPLE_SIZE, sampleLength3_sd_hihat)) {
          currentHihat = SD_DATA_SAMPLE__HIHAT;
        } else currentHihat = STANDARD_HIHAT;
        uiState = NAVIGATE;
        break;
    }
  }

  int delta = encoder.getDelta();
  if (delta != 0) {
    switch (uiState) {
      case NAVIGATE:
        rotatedItem = (MenuItem)((rotatedItem + delta + MENU_COUNT) % MENU_COUNT);
        if (rotatedItem == MENU_KICK) currentInstrument = KICK;
        else if (rotatedItem == MENU_SNARE) currentInstrument = SNARE;
        else if (rotatedItem == MENU_HIHAT) currentInstrument = HIHAT;
        break;

      case BPM_EDIT:
        bpm += delta;
        bpm = constrain(bpm, 40.0f, 300.0f);
        step_duration_samples = (int)((60.0f / bpm / 4.0f) * sampleRate);
        break;

      case SUB_SD_KICK:
        index_sd_kick = constrain(index_sd_kick + delta, 0, sd.fileCount - 1);
        break;

      case SUB_SD_SNARE:
        index_sd_snare = constrain(index_sd_snare + delta, 0, sd.fileCount - 1);
        break;

      case SUB_SD_HIHAT:
        index_sd_hihat = constrain(index_sd_hihat + delta, 0, sd.fileCount - 1);
        break;
    }
  }

  char key = tus.keypad.getKey();
  if (key) {
    int step = tus.mapKeyToStep(key);
    if (step >= 0 && step < NUM_STEPS) {
      if (currentInstrument == KICK) kick_steps[step] = !kick_steps[step];
      else if (currentInstrument == SNARE) snare_steps[step] = !snare_steps[step];
      else if (currentInstrument == HIHAT) hihat_steps[step] = !hihat_steps[step];
    }
  }

  static unsigned long lastDraw = 0;
  if (millis() - lastDraw > 100) {
    drawSequencerState();
    lastDraw = millis();
  }
}
                

📂 View Full Code on GitHub

About Me

I am Gökalp, a mechanical engineer, and I love to develop softwares, primarily in Java. After years of working on Cad/Cam softwares, I decided to focus on developing desktop applications. I prefer to code my own libraries instead of using pre-existing ones. The reason behind this is my motivation to have good control over my code and to be more innovative. By the way, I usually find myself playing basketball or electric guitar.

Contact

Email: socialgokalp@gmail.com

Phone: +90 551 190 9784