desc: Interactive Tonnetz MIDI
// version: 2.7.1 (Chord Display Fix)
// author: assistant
// about: Классический ромбовидный Tonnetz с легато, сменой октав и отображением аккордов

slider1:note_len=250<50,2000,1>Base quarter length (ms)
slider2:velocity=100<1,127,1>Velocity
slider3:midi_ch=1<1,16,1>MIDI channel
slider4:octave=4<0,8,1>Base octave
slider5:show_labels=1<0,1,1{Off,On}>Show note names

@init
// ---------- константы сетки ----------
COLS = 14;
ROWS = 10;

// ---------- состояние ----------
last_cap = 0;
gfx_mouse_down = 0;
trigger_play = 0;
trigger_release = 0;
trigger_pc1 = 0;
trigger_pc2 = 0;
trigger_pc3 = 0;
trigger_c = 0;
trigger_r = 0;
trigger_is_lower = 0;
note_off_timer = 0;

// Состояние для drag-legato
hover_c = -1;
hover_r = -1;
hover_is_lower = -1;

// Состояние секвенсора
input_active = 0;
#seq = ""; 
seq_active = 0;
seq_pos = 0;
seq_timer = 0;
seq_c = 0;
seq_r = 0;
seq_is_lower = 0;
seq_length = 0;

// Фильтр по корневой ноте (-1 = выключен)
selected_root_pc = -1;

// Массивы для хранения шагов секвенсора
seq_ops = 2000;
seq_durs = 2100;

// Массив активных нот (от 1000 до 1127)
active_notes = 1000; 
i = 0; loop(128, active_notes[i] = 0; i += 1;);

// --- Состояние для подсветки ---
is_playing = 0;
active_c = 0;
active_r = 0;
active_is_lower = 0;

// ---------- функции ----------
function mod12(x)
  local(r)
  (
    r = x % 12;
    r < 0 ? r += 12;
    r;
  );

function node_pc(col, row)
  local(v)
  (
    v = mod12(col * 7 + row * 4);
    v;
  );

function point_in_tri(px, py, x1, y1, x2, y2, x3, y3)
  local(d1, d2, d3, has_neg, has_pos)
  (
    d1 = (px - x2) * (y1 - y2) - (x1 - x2) * (py - y2);
    d2 = (px - x3) * (y2 - y3) - (x2 - x3) * (py - y3);
    d3 = (px - x1) * (y3 - y1) - (x3 - x1) * (py - y1);
    has_neg = (d1 < 0) || (d2 < 0) || (d3 < 0);
    has_pos = (d1 > 0) || (d2 > 0) || (d3 > 0);
    !(has_neg && has_pos);
  );

function stop_active_notes()
  local(i)
  (
    i = 0;
    loop(128,
      active_notes[i] ? (
        midisend(0, $x80 | (midi_ch - 1), i, 0);
        active_notes[i] = 0;
      );
      i += 1;
    );
    is_playing = 0;
  );

function apply_lpr(op)
  local(nc, nr, nl)
  (
    nc = seq_c; nr = seq_r; nl = seq_is_lower;
    
    (op == 1) ? ( // L
      nl = 1 - nl;
    ) : (op == 2) ? ( // P
      nl == 0 ? (nr -= 1; nl = 1) : (nr += 1; nl = 0);
    ) : (op == 3) ? ( // R
      nl == 0 ? (nc -= 1; nl = 1) : (nc += 1; nl = 0);
    );
    
    seq_c = nc; seq_r = nr; seq_is_lower = nl;
  );

function parse_seq()
  local(i, len, c, is_upper, dur, op, c2, c3)
  (
    len = strlen(#seq);
    i = 0;
    seq_length = 0;
    while(i < len && seq_length < 128) (
      c = str_getchar(#seq, i);
      (c == ' ' || c == ',' || c == '.') ? (
        i += 1;
      ) : (
        is_upper = (c >= 'A' && c <= 'Z');
        c2 = (c >= 'A' && c <= 'Z') ? c + 32 : c;
        
        (c2 == 'l' || c2 == 'p' || c2 == 'r') ? (
          op = (c2 == 'l') ? 1 : (c2 == 'p') ? 2 : 3;
          dur = is_upper ? 2 : 1;
          
          i += 1;
          (i < len) ? (
            c3 = str_getchar(#seq, i);
            (c3 == '+' && is_upper) ? (
              dur = 4; i += 1;
            ) : (c3 == '-' && !is_upper) ? (
              dur = 0.5; i += 1;
              (i < len && str_getchar(#seq, i) == '-') ? ( dur = 0.25; i += 1; );
            ) : (c3 == '+' && !is_upper) ? (
              dur = 0.25; i += 1;
              (i < len && str_getchar(#seq, i) == '+') ? ( i += 1; );
            );
          );
          
          seq_ops[seq_length] = op;
          seq_durs[seq_length] = dur;
          seq_length += 1;
        ) : (
          i += 1;
        );
      );
    );
  );

function gx(c, r) ( cx + ((c - off_c) - (r - off_r)) * cell_size );
function gy(c, r) ( cy + ((c - off_c) + (r - off_r)) * cell_size );

@block
seq_active ? (
  seq_timer -= (samplesblock / srate) * 1000;
  seq_timer <= 0 ? (
    stop_active_notes();
    
    seq_pos < seq_length ? (
      active_c = seq_c;
      active_r = seq_r;
      active_is_lower = seq_is_lower;
      is_playing = 1;
      
      seq_is_lower == 0 ? (
        pc1 = node_pc(seq_c, seq_r);
        pc2 = node_pc(seq_c + 1, seq_r);
        pc3 = node_pc(seq_c, seq_r + 1);
      ) : (
        pc1 = node_pc(seq_c + 1, seq_r);
        pc2 = node_pc(seq_c + 1, seq_r + 1);
        pc3 = node_pc(seq_c, seq_r + 1);
      );
      
      n = (octave + 1) * 12 + pc1;
      (n >= 0 && n <= 127) ? ( midisend(0, $x90 | (midi_ch - 1), n, velocity); active_notes[n] = 1; );
      n = (octave + 1) * 12 + pc2;
      (n >= 0 && n <= 127) ? ( midisend(0, $x90 | (midi_ch - 1), n, velocity); active_notes[n] = 1; );
      n = (octave + 1) * 12 + pc3;
      (n >= 0 && n <= 127) ? ( midisend(0, $x90 | (midi_ch - 1), n, velocity); active_notes[n] = 1; );

      seq_timer = note_len * seq_durs[seq_pos];
      op = seq_ops[seq_pos];
      apply_lpr(op);
      seq_pos += 1;
    ) : (
      seq_active = 0;
      note_off_timer = note_len * seq_durs[seq_pos - 1];
    );
  );
);

!seq_active ? (
  note_off_timer > 0 ? (
    note_off_timer -= (samplesblock / srate) * 1000;
    (note_off_timer <= 0 && !gfx_mouse_down) ? (
      stop_active_notes();
      note_off_timer = 0;
    );
  );
  
  trigger_release ? (
    note_off_timer <= 0 ? stop_active_notes();
    trigger_release = 0;
  );

  trigger_play ? (
    stop_active_notes();
    active_c = trigger_c;
    active_r = trigger_r;
    active_is_lower = trigger_is_lower;
    is_playing = 1;
    
    n = (octave + 1) * 12 + trigger_pc1;
    (n >= 0 && n <= 127) ? ( midisend(0, $x90 | (midi_ch - 1), n, velocity); active_notes[n] = 1; );
    n = (octave + 1) * 12 + trigger_pc2;
    (n >= 0 && n <= 127) ? ( midisend(0, $x90 | (midi_ch - 1), n, velocity); active_notes[n] = 1; );
    n = (octave + 1) * 12 + trigger_pc3;
    (n >= 0 && n <= 127) ? ( midisend(0, $x90 | (midi_ch - 1), n, velocity); active_notes[n] = 1; );
    
    note_off_timer = max(note_len, 50); 
    trigger_play = 0;
  );
);

@gfx 640 480
gfx_clear = 30; 

ui_h = 75; 
left_margin = 60; 
right_margin = 160; 
avail_w = gfx_w - left_margin - right_margin - 20;
avail_h = gfx_h - ui_h - 20;
span = COLS + ROWS - 2;
cell_size = min(avail_w / span, avail_h / span);
cell_size = min(cell_size, 60); 
cell_size = max(cell_size, 10); 

cx = left_margin + avail_w / 2;
cy = ui_h + (gfx_h - ui_h) / 2;
off_c = (COLS - 1) / 2.0;
off_r = (ROWS - 1) / 2.0;

// ---------- Отрисовка панели нот (Слева) ----------
note_x = 10;
note_w = 40;
note_y = ui_h;
note_h = (gfx_h - ui_h - 10) / 12;

pc = 0;
loop(12,
  ny1 = note_y + pc * note_h;
  
  selected_root_pc == pc ? gfx_set(0.3, 0.6, 1, 0.8) : gfx_set(0.2, 0.2, 0.2, 0.8);
  gfx_rect(note_x, ny1, note_w, note_h, 1);
  
  gfx_set(0.1, 0.1, 0.1, 1);
  gfx_rect(note_x, ny1, note_w, note_h, 0);
  
  gfx_setfont(1, "Arial", max(10, floor(note_h * 0.5)));
  strcpy_substr(#name, "C C#D D#E F F#G G#A A#B ", pc * 2, 2);
  
  w_tmp = 0; h_tmp = 0;
  gfx_measurestr(#name, w_tmp, h_tmp);
  
  gfx_set(1, 1, 1, 1);
  gfx_x = note_x + (note_w - w_tmp) / 2;
  gfx_y = ny1 + (note_h - h_tmp) / 2;
  gfx_drawstr(#name);
  
  pc += 1;
);

// ---------- Отрисовка панели октав (Справа) ----------
oct_x = gfx_w - 45;
oct_w = 35;
oct_y = ui_h + 100; 
oct_h = gfx_h - oct_y - 20;
btn_h = 30;

// Фон
gfx_set(0.15, 0.15, 0.15, 0.8);
gfx_rect(oct_x, oct_y, oct_w, oct_h, 1);
gfx_set(0.3, 0.3, 0.3, 1);
gfx_rect(oct_x, oct_y, oct_w, oct_h, 0);

// Кнопка вверх
gfx_set(0.25, 0.25, 0.25, 1);
gfx_rect(oct_x, oct_y, oct_w, btn_h, 1);
gfx_set(0.8, 0.8, 0.8, 1);
gfx_triangle(oct_x + oct_w/2, oct_y + 10, oct_x + 10, oct_y + 22, oct_x + oct_w - 10, oct_y + 22, 1);

// Кнопка вниз
gfx_set(0.25, 0.25, 0.25, 1);
gfx_rect(oct_x, oct_y + oct_h - btn_h, oct_w, btn_h, 1);
gfx_set(0.8, 0.8, 0.8, 1);
gfx_triangle(oct_x + oct_w/2, oct_y + oct_h - 10, oct_x + 10, oct_y + oct_h - 22, oct_x + oct_w - 10, oct_y + oct_h - 22, 1);

// Значение октавы
gfx_setfont(1, "Arial", 18);
sprintf(#oct_str, "%d", octave);
w_tmp = 0; h_tmp = 0;
gfx_measurestr(#oct_str, w_tmp, h_tmp);
gfx_set(1, 1, 0, 1);
gfx_x = oct_x + (oct_w - w_tmp) / 2;
gfx_y = oct_y + (oct_h - h_tmp) / 2;
gfx_drawstr(#oct_str);

gfx_setfont(1, "Arial", 10);
gfx_x = oct_x + 5; gfx_y = oct_y + oct_h - btn_h + 40;
gfx_set(0.5, 0.5, 0.5, 1);
gfx_drawstr("OCT");

// ---------- Отрисовка блока текущего аккорда ----------
chord_x = gfx_w - 160;
chord_y = ui_h + 20;
chord_w = 110;
chord_h = 60;

// Фон аккорда
gfx_set(0.15, 0.15, 0.15, 0.8);
gfx_rect(chord_x, chord_y, chord_w, chord_h, 1);
gfx_set(0.3, 0.3, 0.3, 1);
gfx_rect(chord_x, chord_y, chord_w, chord_h, 0);

is_playing ? (
  // Вычисляем тонику
  active_is_lower == 0 ? (
    root_pc = node_pc(active_c, active_r);
    chord_suffix = "maj";
  ) : (
    root_pc = node_pc(active_c, active_r + 1); // ИСПРАВЛЕНО: корень минора здесь
    chord_suffix = "min";
  );
  
  strcpy_substr(#root_str, "C C#D D#E F F#G G#A A#B ", root_pc * 2, 2);
  sprintf(#chord_name, "%s%s", #root_str, chord_suffix);
  
  // Крупный шрифт
  gfx_setfont(1, "Arial", 26);
  active_is_lower == 0 ? gfx_set(1, 0.6, 0.2, 1) : gfx_set(0.3, 0.7, 1, 1); // Оранжевый / Голубой
  
  w_tmp = 0; h_tmp = 0;
  gfx_measurestr(#chord_name, w_tmp, h_tmp);
  gfx_x = chord_x + (chord_w - w_tmp) / 2;
  gfx_y = chord_y + (chord_h - h_tmp) / 2;
  gfx_drawstr(#chord_name);
) : (
  // Заглушка, если ничего не играет
  gfx_setfont(1, "Arial", 24);
  gfx_set(0.4, 0.4, 0.4, 1);
  gfx_x = chord_x + 38; gfx_y = chord_y + 15;
  gfx_drawstr("---");
);
gfx_setfont(1, "Arial", 14);

// ---------- Фон треугольников ----------
c = 0;
loop(COLS - 1,
  r = 0;
  loop(ROWS - 1,
    root_maj = node_pc(c, r);
    root_min = node_pc(c, r+1);
    
    x1 = gx(c, r);   y1 = gy(c, r);
    x2 = gx(c+1, r); y2 = gy(c+1, r);
    x3 = gx(c, r+1); y3 = gy(c, r+1);
    
    (selected_root_pc >= 0 && root_maj == selected_root_pc) ? gfx_set(1, 1, 0, 0.25) : gfx_set(1, 0.4, 0.1, 0.15);
    gfx_triangle(x1, y1, x2, y2, x3, y3, 1);
    
    x1 = gx(c+1, r);   y1 = gy(c+1, r);
    x2 = gx(c+1, r+1); y2 = gy(c+1, r+1);
    x3 = gx(c, r+1);   y3 = gy(c, r+1);
    
    (selected_root_pc >= 0 && root_min == selected_root_pc) ? gfx_set(1, 1, 0, 0.25) : gfx_set(0.1, 0.4, 0.9, 0.15);
    gfx_triangle(x1, y1, x2, y2, x3, y3, 1);
    
    r += 1;
  );
  c += 1;
);

// ---------- Линии сетки ----------
gfx_set(0.35, 0.35, 0.45, 1);
c = 0;
loop(COLS,
  r = 0;
  loop(ROWS,
    nx = gx(c, r); ny = gy(c, r);
    c + 1 < COLS ? gfx_line(nx, ny, gx(c+1, r), gy(c+1, r));
    r + 1 < ROWS ? gfx_line(nx, ny, gx(c, r+1), gy(c, r+1));
    (c + 1 < COLS && r + 1 < ROWS) ? gfx_line(gx(c+1, r), gy(c+1, r), gx(c, r+1), gy(c, r+1));
    r += 1;
  );
  c += 1;
);

// ---------- Выделение корневых треугольников ----------
selected_root_pc >= 0 ? (
  c = 0;
  loop(COLS - 1,
    r = 0;
    loop(ROWS - 1,
      root_maj = node_pc(c, r);
      root_min = node_pc(c, r+1);
      gfx_set(1, 1, 0, 0.8);
      
      (root_maj == selected_root_pc) ? (
        x1 = gx(c, r);   y1 = gy(c, r);
        x2 = gx(c+1, r); y2 = gy(c+1, r);
        x3 = gx(c, r+1); y3 = gy(c, r+1);
        gfx_line(x1, y1, x2, y2); gfx_line(x2, y2, x3, y3); gfx_line(x3, y3, x1, y1);
      );
      
      (root_min == selected_root_pc) ? (
        x1 = gx(c+1, r);   y1 = gy(c+1, r);
        x2 = gx(c+1, r+1); y2 = gy(c+1, r+1);
        x3 = gx(c, r+1);   y3 = gy(c, r+1);
        gfx_line(x1, y1, x2, y2); gfx_line(x2, y2, x3, y3); gfx_line(x3, y3, x1, y1);
      );
      r += 1;
    );
    c += 1;
  );
);

// ---------- Активный треугольник ----------
is_playing ? (
  active_is_lower == 0 ? (
    x1 = gx(active_c, active_r);     y1 = gy(active_c, active_r);
    x2 = gx(active_c+1, active_r);   y2 = gy(active_c+1, active_r);
    x3 = gx(active_c, active_r+1);   y3 = gy(active_c, active_r+1);
  ) : (
    x1 = gx(active_c+1, active_r);   y1 = gy(active_c+1, active_r);
    x2 = gx(active_c+1, active_r+1); y2 = gy(active_c+1, active_r+1);
    x3 = gx(active_c, active_r+1);   y3 = gy(active_c, active_r+1);
  );
  
  gfx_set(1, 1, 1, 0.25); 
  gfx_triangle(x1, y1, x2, y2, x3, y3, 1);
  gfx_set(1, 1, 1, 0.9);
  gfx_line(x1, y1, x2, y2); gfx_line(x2, y2, x3, y3); gfx_line(x3, y3, x1, y1);
);

// ---------- Узлы ----------
c = 0;
loop(COLS,
  r = 0;
  loop(ROWS,
    nx = gx(c, r); ny = gy(c, r);
    pc = node_pc(c, r);
    
    gfx_set(0.9, 0.85, 0.7, 1);
    gfx_circle(nx, ny, max(2, cell_size * 0.15), 1);
    
    show_labels ? (
      gfx_setfont(1, "Arial", max(8, floor(cell_size * 0.35))); 
      gfx_set(0.8, 0.8, 0.8, 0.9);
      strcpy_substr(#name, "C C#D D#E F F#G G#A A#B ", pc * 2, 2);
      w_tmp = 0; h_tmp = 0;
      gfx_measurestr(#name, w_tmp, h_tmp); 
      gfx_x = nx - w_tmp / 2;
      gfx_y = ny + cell_size * 0.2; 
      gfx_drawstr(#name);
    );
    r += 1;
  );
  c += 1;
);

gfx_setfont(1, "Arial", 14);

// ---------- UI ----------
input_active ? gfx_set(0.3, 0.6, 1, 1) : gfx_set(0.3, 0.3, 0.3, 1);
gfx_rect(10, 10, 350, 26, 0);
input_active ? gfx_set(0.2, 0.2, 0.25, 1) : gfx_set(0.15, 0.15, 0.15, 1);
gfx_rect(11, 11, 348, 24, 1);

gfx_set(0.8, 0.8, 0.8, 1);
gfx_x = 15; gfx_y = 18; gfx_drawstr("Seq: ");

gfx_set(1, 1, 1, 1);
gfx_x = 60; gfx_y = 18; gfx_drawstr(#seq);
text_end_x = gfx_x; 

input_active ? ( (time_precise() * 2) % 1 > 0.5 ? gfx_rect(text_end_x, 14, 2, 18); );

gfx_set(0.25, 0.25, 0.25, 1);
gfx_rect(370, 10, 50, 26);
gfx_set(0.9, 0.9, 0.9, 1);
gfx_x = 380; gfx_y = 18; gfx_drawstr("Clear");

gfx_set(0.6, 0.6, 0.6, 1);
gfx_x = 10; gfx_y = 45;
gfx_drawstr("Syntax: L+ (Whole) | L (Half) | l (Quarter) | l- (Eighth) | l++ (16th)");

// ---------- Мышь и клавиатура ----------
mouse_down = (mouse_cap & 1) != 0;
clicked = mouse_down && !(last_cap & 1);
released = !mouse_down && (last_cap & 1);

over_input = (mouse_x >= 10 && mouse_x <= 360 && mouse_y >= 10 && mouse_y <= 36);
over_clear = (mouse_x > 370 && mouse_x < 420 && mouse_y > 10 && mouse_y < 36);
over_notes = (mouse_x >= note_x && mouse_x <= note_x + note_w && mouse_y >= note_y && mouse_y <= gfx_h);

over_oct_up = (mouse_x >= oct_x && mouse_x <= oct_x + oct_w && mouse_y >= oct_y && mouse_y <= oct_y + btn_h);
over_oct_down = (mouse_x >= oct_x && mouse_x <= oct_x + oct_w && mouse_y >= oct_y + oct_h - btn_h && mouse_y <= oct_y + oct_h);

over_grid = !over_input && !over_clear && !over_notes && !over_oct_up && !over_oct_down;

clicked ? (
  over_clear ? ( #seq = ""; input_active = 0; ) : 
  over_input ? ( input_active = 1; ) : 
  over_notes ? ( 
    clicked_pc = floor((mouse_y - note_y) / note_h);
    (clicked_pc >= 0 && clicked_pc <= 11) ? (
      selected_root_pc == clicked_pc ? ( selected_root_pc = -1; ) : ( selected_root_pc = clicked_pc; );
    );
    input_active = 0;
  ) : 
  over_oct_up ? (
    octave = min(8, octave + 1);
    input_active = 0;
  ) :
  over_oct_down ? (
    octave = max(0, octave - 1);
    input_active = 0;
  ) : (
    input_active = 0;
  );
);

mouse_down && over_grid && !input_active ? (
  found = 0;
  c = 0;
  loop(COLS - 1,
    r = 0;
    loop(ROWS - 1,
      x1 = gx(c, r);   y1 = gy(c, r);
      x2 = gx(c+1, r); y2 = gy(c+1, r);
      x3 = gx(c, r+1); y3 = gy(c, r+1);
      point_in_tri(mouse_x, mouse_y, x1, y1, x2, y2, x3, y3) ? (
        best_c = c; best_r = r; best_is_lower = 0; found = 1;
      );
      
      x1 = gx(c+1, r);   y1 = gy(c+1, r);
      x2 = gx(c+1, r+1); y2 = gy(c+1, r+1);
      x3 = gx(c, r+1);   y3 = gy(c, r+1);
      point_in_tri(mouse_x, mouse_y, x1, y1, x2, y2, x3, y3) ? (
        best_c = c; best_r = r; best_is_lower = 1; found = 1;
      );
      r += 1;
    );
    c += 1;
  );
  
  found ? (
    (best_c != hover_c || best_r != hover_r || best_is_lower != hover_is_lower) ? (
      hover_c = best_c; hover_r = best_r; hover_is_lower = best_is_lower;
      
      parse_seq();
      
      seq_length > 0 ? (
        seq_c = best_c;
        seq_r = best_r;
        seq_is_lower = best_is_lower;
        seq_pos = 0;
        seq_active = 1;
        seq_timer = 1;
      ) : (
        trigger_c = best_c;
        trigger_r = best_r;
        trigger_is_lower = best_is_lower;
        
        best_is_lower == 0 ? (
          trigger_pc1 = node_pc(best_c, best_r);
          trigger_pc2 = node_pc(best_c + 1, best_r);
          trigger_pc3 = node_pc(best_c, best_r + 1);
        ) : (
          trigger_pc1 = node_pc(best_c + 1, best_r);
          trigger_pc2 = node_pc(best_c + 1, best_r + 1);
          trigger_pc3 = node_pc(best_c, best_r + 1);
        );
        trigger_play = 1;
      );
    );
  ) : (
    hover_c = -1; 
  );
);

released ? ( 
  trigger_release = 1; 
  hover_c = -1;
);

gfx_mouse_down = mouse_down;
last_cap = mouse_cap;

c = 0;
while((c = gfx_getchar()) > 0) (
  input_active ? (
    (c == 8) ? ( 
      len = strlen(#seq);
      len > 0 ? ( str_setchar(#seq, len - 1, 0); );
    ) : (c == 13) ? ( input_active = 0; ) : (c == 27) ? ( input_active = 0; ) : (
      c2 = (c >= 'A' && c <= 'Z') ? c + 32 : c; 
      (c2 == 'l' || c2 == 'p' || c2 == 'r' || c == '+' || c == '-' || c == ' ') ? (
        sprintf(#tmp, "%c", c); 
        strcat(#seq, #tmp);
      );
    );
  );
);
