vgrowhouse commited on
Commit
a24dd09
·
verified ·
1 Parent(s): 5211083

ADD <div id="appCtn" style="display:flex; flex-direction:column; height:100vh; background:#000; color:#0f0; font-family:'Press Start 2P', sans-serif; font-size:10px;">

Browse files

<!-- Font Import -->
<style>@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');</style>

<!-- Header -->
<header style="padding:10px; background:#111; border-bottom:2px solid #0f0; display:flex; justify-content:space-between; align-items:center;">
<h1 style="margin:0; font-size:14px;">8-BIT CHIPTUNE GENERATOR</h1>
<div>
<button id="generateBtn" onclick="generateSong()" style="margin:0 5px; padding:5px 10px; background:#0f0; color:#000; border:none; cursor:pointer;">GENERATE AI SONG</button>
<button id="playBtn" onclick="playSong()" style="margin:0 5px; padding:5px 10px; background:#0f0; color:#000; border:none; cursor:pointer;">PLAY</button>
<button id="stopBtn" onclick="stopSong()" style="margin:0 5px; padding:5px 10px; background:#f00; color:#fff; border:none; cursor:pointer;">STOP</button>
<button onclick="saveProject()" style="margin:0 5px; padding:5px 10px; background:#ff0; color:#000; border:none; cursor:pointer;">SAVE</button>
<label for="loadInput" style="margin:0 5px; padding:5px 10px; background:#ff0; color:#000; border:none; cursor:pointer;">LOAD</label>
<input id="loadInput" type="file" style="display:none;" onchange="loadProject(event)">
</div>
</header>

<!-- Main Content -->
<div style="display:flex; flex:1; overflow:hidden;">
<!-- Sidebar -->
<aside style="width:200px; background:#111; border-right:2px solid #0f0; padding:10px; overflow-y:auto;">
<div style="margin-bottom:20px;">
<h2 style="font-size:12px; margin-bottom:10px;">CHANNELS</h2>
<div id="channelList" style="margin-bottom:10px;"></div>
<button id="addChannelBtn" onclick="addChannel()" style="width:100%; padding:5px; background:#0f0; color:#000; border:none; cursor:pointer;">+ ADD CHANNEL</button>
</div>

<div>
<h2 style="font-size:12px; margin-bottom:10px;">INSTRUMENTS</h2>
<div id="instrumentList" style="margin-bottom:10px;"></div>
<button id="addInstrumentBtn" onclick="addInstrument()" style="width:100%; padding:5px; background:#0f0; color:#000; border:none; cursor:pointer;">+ ADD INSTRUMENT</button>
</div>

<div style="margin-top:20px;">
<h2 style="font-size:12px; margin-bottom:10px;">PATTERN</h2>
<div style="margin-bottom:10px;">
<label>STEPS: </label>
<input id="patternLengthInput" type="number" min="4" max="64" value="16" style="width:50px; margin-left:5px; padding:2px; background:#000; color:#0f0; border:1px solid #0f0;">
<button onclick="updatePatternLength()" style="margin-left:5px; padding:2px 5px; background:#0f0; color:#000; border:none; cursor:pointer;">UPDATE</button>
</div>
<div style="margin-bottom:10px;">
<label>VERSE: </label>
<select id="verseSelect" onchange="switchVerse()" style="margin-left:5px; padding:2px; background:#000; color:#0f0; border:1px solid #0f0;">
<option value="0">Verse 1</option>
</select>
<button onclick="addVerse()" style="margin-left:5px; padding:2px 5px; background:#0f0; color:#000; border:none; cursor:pointer;">+</button>
<button onclick="copyVerse()" style="margin-left:5px; padding:2px 5px; background:#ff0; color:#000; border:none; cursor:pointer;">COPY</button>
</div>
</div>
</aside>

<!-- Pattern Editor -->
<main style="flex:1; display:flex; flex-direction:column; padding:10px; overflow:auto;">
<div style="margin-bottom:10px;">
<h2 style="font-size:12px;">PATTERN EDITOR</h2>
<div id="patternControls" style="margin:5px 0;">
<span>TEMPO: </span>
<input id="tempoInput" type="range" min="60" max="240" value="120" style="width:100px;">
<span id="tempoValue">120</span> BPM
</div>
</div>

<div id="patternGrid" style="border:1px solid #0f0; background:#0a0a0a; overflow:auto; flex:1;">
<!-- Grid will be generated here -->
</div>

<div id="noteInput" style="margin-top:10px; padding:10px; background:#111; border:1px solid #0f0; overflow-x:auto;">
<h3 style="font-size:10px; margin-bottom:5px;">NOTE INPUT</h3>
<div id="piano" style="display:flex; height:40px; min-width:600px;">
<!-- Piano keys will be generated here -->
</div>
</div>
</main>
</div>

<!-- Loading Indicator -->
<div id="loadingIndicator" hidden style="position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:#111; padding:20px; border:2px solid #0f0; text-align:center;">
<div>GENERATING...</div>
<div style="margin-top:10px;">⏳</div>
</div>

<!-- Edit Channel Modal -->
<div id="editChannelModal" hidden style="position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:#111; padding:20px; border:2px solid #0f0; z-index:1000;">
<h3 style="margin-top:0;">EDIT CHANNEL</h3>
<div style="margin:10px 0;">
<label>NAME:</label>
<input id="editChannelName" type="text" style="width:100%; margin-top:5px; padding:5px; background:#000; color:#0f0; border:1px solid #0f0;">
</div>
<div style="margin:10px 0;">
<label>INSTRUMENT:</label>
<select id="editChannelInstrument" style="width:100%; margin-top:5px; padding:5px; background:#000; color:#0f0; border:1px solid #0f0;">
<!-- Options will be populated -->
</select>
</div>
<div style="display:flex; justify-content:space-between; margin-top:20px;">
<button onclick="cancelEditChannel()" style="padding:5px 10px; background:#f00; color:#fff; border:none; cursor:pointer;">CANCEL</button>
<button onclick="saveEditChannel()" style="padding:5px 10px; background:#0f0; color:#000; border:none; cursor:pointer;">SAVE</button>
</div>
</div>

<!-- Edit Instrument Modal -->
<div id="editInstrumentModal" hidden style="position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:#111; padding:20px; border:2px solid #0f0; z-index:1000;">
<h3 style="margin-top:0;">EDIT INSTRUMENT</h3>
<div style="margin:10px 0;">
<label>NAME:</label>
<input id="editInstrumentName" type="text" style="width:100%; margin-top:5px; padding:5px; background:#000; color:#0f0; border:1px solid #0f0;">
</div>
<div style="margin:10px 0;">
<label>TYPE:</label>
<select id="editInstrumentType" style="width:100%; margin-top:5px; padding:5px; background:#000; color:#0f0; border:1px solid #0f0;">
<option value="square">SQUARE</option>
<option value="triangle">TRIANGLE</option>
<option value="sawtooth">SAWTOOTH</option>
<option value="sine">SINE</option>
<option value="noise">NOISE</option>
</select>
</div>
<div style="margin:10px 0;">
<label>COLOR:</label>
<input id="editInstrumentColor" type="color" style="width:100%; margin-top:5px; padding:5px; background:#000; color:#0f0; border:1px solid #0f0;">
</div>
<div style="margin:10px 0;">
<label>ATTACK (ms):</label>
<input id="editInstrumentAttack" type="number" min="0" max="1000" value="0" style="width:100%; margin-top:5px; padding:5px; background:#000; color:#0f0; border:1px solid #0f0;">
</div>
<div style="margin:10px 0;">
<label>DECAY (ms):</label>
<input id="editInstrumentDecay" type="number" min="0" max="1000" value="100" style="width:100%; margin-top:5px; padding:5px; background:#000; color:#0f0; border:1px solid #0f0;">
</div>
<div style="margin:10px 0;">
<label>SUSTAIN:</label>
<input id="editInstrumentSustain" type="number" min="0" max="1" step="0.1" value="0.7" style="width:100%; margin-top:5px; padding:5px; background:#000; color:#0f0; border:1px solid #0f0;">
</div>
<div style="margin:10px 0;">
<label>RELEASE (ms):</label>
<input id="editInstrumentRelease" type="number" min="0" max="2000" value="300" style="width:100%; margin-top:5px; padding:5px; background:#000; color:#0f0; border:1px solid #0f0;">
</div>
<div style="margin:10px 0;">
<label>VIBRATO (Hz):</label>
<input id="editInstrumentVibrato" type="number" min="0" max="20" step="0.5" value="0" style="width:100%; margin-top:5px; padding:5px; background:#000; color:#0f0; border:1px solid #0f0;">
</div>
<div style="display:flex; justify-content:space-between; margin-top:20px;">
<button onclick="cancelEditInstrument()" style="padding:5px 10px; background:#f00; color:#fff; border:none; cursor:pointer;">CANCEL</button>
<button onclick="saveEditInstrument()" style="padding:5px 10px; background:#0f0; color:#000; border:none; cursor:pointer;">SAVE</button>
</div>
</div>

<script>
// State management
let channels = [];
let instruments = [];
let pattern = [];
let verses = []; // Store multiple patterns (verses)
let currentVerse = 0;
let currentChannel = 0;
let currentStep = 0;
let isPlaying = false;
let audioContext;
let gainNode;
let oscillators = [];
let tempo = 120;
let selectedNote = null; // Track the currently selected note
let editingChannelId = null; // Track which channel is being edited
let editingInstrumentId = null; // Track which instrument is being edited
let isDrawing = false; // For drag drawing

// Initialize app
function init() {
// Create default instruments with ADSR and vibrato
instruments = [
{ id: 1, name: 'SQUARE', type: 'square', color: '#0ff', attack: 0, decay: 100, sustain: 0.7, release: 300, vibrato: 0 },
{ id: 2, name: 'TRIANGLE', type: 'triangle', color: '#f0f', attack: 50, decay: 150, sustain: 0.6, release: 400, vibrato: 2 },
{ id: 3, name: 'NOISE', type: 'noise', color: '#ff0', attack: 10, decay: 50, sustain: 0.5, release: 200, vibrato: 0 },
{ id: 4, name: 'SINE', type: 'sine', color: '#f80', attack: 100, decay: 200, sustain: 0.8, release: 500, vibrato: 5 }
];

// Create default cha

Files changed (2) hide show
  1. chiptune.html +379 -203
  2. index.html +2 -2
chiptune.html CHANGED
@@ -4,21 +4,27 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>8-Bit Chiptune Generator</title>
7
- <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
8
  <style>
 
 
9
  body {
10
  margin: 0;
11
  padding: 0;
12
- background: #000;
 
13
  color: #0f0;
14
- font-family: 'Press Start 2P', sans-serif;
15
  overflow: hidden;
16
  }
 
17
  #appCtn {
18
  display: flex;
19
  flex-direction: column;
20
  height: 100vh;
 
 
 
21
  }
 
22
  header {
23
  padding: 10px;
24
  background: #111;
@@ -27,10 +33,12 @@
27
  justify-content: space-between;
28
  align-items: center;
29
  }
 
30
  h1 {
31
- font-size: 14px;
32
  margin: 0;
 
33
  }
 
34
  button {
35
  margin: 0 5px;
36
  padding: 5px 10px;
@@ -38,24 +46,21 @@
38
  color: #000;
39
  border: none;
40
  cursor: pointer;
41
- font-family: 'Press Start 2P', sans-serif;
42
  font-size: 10px;
43
  }
44
- #stopBtn {
45
- background: #f00;
46
- color: #fff;
47
- }
48
- label {
49
- cursor: pointer;
50
- }
51
- input[type="file"] {
52
- display: none;
53
- }
54
- main {
55
  display: flex;
56
  flex: 1;
57
  overflow: hidden;
58
  }
 
59
  aside {
60
  width: 200px;
61
  background: #111;
@@ -63,12 +68,14 @@
63
  padding: 10px;
64
  overflow-y: auto;
65
  }
 
66
  #patternGrid {
67
  flex: 1;
68
  border: 1px solid #0f0;
69
  background: #0a0a0a;
70
  overflow: auto;
71
  }
 
72
  #noteInput {
73
  margin-top: 10px;
74
  padding: 10px;
@@ -76,42 +83,23 @@
76
  border: 1px solid #0f0;
77
  overflow-x: auto;
78
  }
 
79
  #piano {
80
  display: flex;
81
  height: 40px;
82
  min-width: 600px;
83
  }
84
- .loader {
85
- border: 4px solid #f3f3f3;
86
- border-top: 4px solid #0f0;
87
- border-radius: 50%;
88
- width: 40px;
89
- height: 40px;
90
- animation: spin 1s linear infinite;
91
- margin: 0 auto;
92
- }
93
- @keyframes spin {
94
- 0% { transform: rotate(0deg); }
95
- 100% { transform: rotate(360deg); }
96
- }
97
- table {
98
- width: 100%;
99
- border-collapse: collapse;
100
- }
101
- th, td {
102
- border: 1px solid #0f0;
103
- padding: 5px;
104
- text-align: center;
105
- }
106
- th {
107
  background: #111;
108
- }
109
- td {
110
- cursor: pointer;
111
- background: #000;
112
- }
113
- td:hover {
114
- background: #0a0a0a;
115
  }
116
  </style>
117
  </head>
@@ -120,74 +108,70 @@
120
  <header>
121
  <h1>8-BIT CHIPTUNE GENERATOR</h1>
122
  <div>
123
- <button id="generateBtn">GENERATE AI SONG</button>
124
- <button id="playBtn">PLAY</button>
125
- <button id="stopBtn">STOP</button>
126
- <button id="saveBtn">SAVE</button>
127
- <label for="loadInput">LOAD</label>
128
- <input id="loadInput" type="file">
129
  </div>
130
  </header>
131
- <main>
 
132
  <aside>
133
- <div>
134
- <h2>CHANNELS</h2>
135
- <div id="channelList"></div>
136
- <button id="addChannelBtn">+ ADD CHANNEL</button>
137
  </div>
 
138
  <div>
139
- <h2>INSTRUMENTS</h2>
140
- <div id="instrumentList"></div>
141
- <button id="addInstrumentBtn">+ ADD INSTRUMENT</button>
142
  </div>
143
- <div>
144
- <h2>PATTERN</h2>
145
- <div>
 
146
  <label>STEPS: </label>
147
- <input id="patternLengthInput" type="number" min="4" max="64" value="16">
148
- <button id="updatePatternBtn">UPDATE</button>
149
  </div>
150
- <div>
151
  <label>VERSE: </label>
152
- <select id="verseSelect">
153
  <option value="0">Verse 1</option>
154
  </select>
155
- <button id="addVerseBtn">+</button>
156
- <button id="copyVerseBtn">COPY</button>
157
  </div>
158
  </div>
159
  </aside>
160
- <div id="centerPanel">
161
- <div>
162
- <h2>PATTERN EDITOR</h2>
163
- <div>
 
164
  <span>TEMPO: </span>
165
- <input id="tempoInput" type="range" min="60" max="240" value="120">
166
  <span id="tempoValue">120</span> BPM
167
  </div>
168
  </div>
169
- <div id="patternGrid">
170
- <table>
171
- <thead>
172
- <tr>
173
- <th>STEP</th>
174
- <th>CH1</th>
175
- <th>CH2</th>
176
- <th>CH3</th>
177
- <th>CH4</th>
178
- </tr>
179
- </thead>
180
- <tbody>
181
- <!-- Rows will be generated by JavaScript -->
182
- </tbody>
183
- </table>
184
- </div>
185
  <div id="noteInput">
186
- <h3>NOTE INPUT</h3>
187
  <div id="piano"></div>
188
  </div>
189
  </div>
190
- </main>
 
 
 
 
 
191
  </div>
192
 
193
  <script>
@@ -197,93 +181,133 @@
197
  let pattern = [];
198
  let verses = [];
199
  let currentVerse = 0;
 
 
200
  let isPlaying = false;
201
  let audioContext;
 
 
202
  let tempo = 120;
203
-
 
 
 
 
204
  // Initialize app
205
  function init() {
206
  // Create default instruments
207
  instruments = [
208
- { id: 1, name: 'SQUARE', type: 'square', color: '#0ff' },
209
- { id: 2, name: 'TRIANGLE', type: 'triangle', color: '#f0f' },
210
- { id: 3, name: 'NOISE', type: 'noise', color: '#ff0' }
 
211
  ];
212
 
213
  // Create default channels
214
  channels = [
215
  { id: 1, name: 'CH1', instrumentId: 1, muted: false },
216
  { id: 2, name: 'CH2', instrumentId: 2, muted: false },
217
- { id: 3, name: 'CH3', instrumentId: 3, muted: false }
 
218
  ];
219
 
220
  // Initialize empty pattern
221
- initPattern();
 
222
 
223
  // Setup audio context
224
  audioContext = new (window.AudioContext || window.webkitAudioContext)();
 
 
 
225
 
226
  // Render UI
227
  renderChannels();
228
  renderInstruments();
229
  renderPatternGrid();
230
  renderPiano();
 
231
 
232
- // Setup event listeners
233
- setupEventListeners();
234
- }
235
-
236
- function initPattern() {
237
- pattern = Array(16).fill().map(() => Array(channels.length).fill(null));
238
- verses = [JSON.parse(JSON.stringify(pattern))];
239
  }
240
-
 
241
  function renderChannels() {
242
  const channelList = document.getElementById('channelList');
243
  channelList.innerHTML = channels.map(ch => `
244
- <div style="display:flex; justify-content:space-between; margin:5px 0; padding:5px; background:#0a0a0a; border:1px solid #0f0;">
245
  <span>${ch.name}</span>
246
  <div>
247
- <button onclick="toggleMute(${ch.id})" style="margin-right:5px; background:${ch.muted ? '#f00' : '#0f0'}; color:#000;">${ch.muted ? 'UNMUTE' : 'MUTE'}</button>
248
- <button onclick="removeChannel(${ch.id})" style="background:#f00; color:#fff;">X</button>
 
249
  </div>
250
  </div>
251
  `).join('');
252
  }
253
-
254
  function renderInstruments() {
255
  const instrumentList = document.getElementById('instrumentList');
256
  instrumentList.innerHTML = instruments.map(inst => `
257
- <div style="display:flex; justify-content:space-between; margin:5px 0; padding:5px; background:#0a0a0a; border:1px solid ${inst.color};">
258
- <span style="color:${inst.color}">${inst.name}</span>
259
  <div>
260
- <button onclick="removeInstrument(${inst.id})" style="background:#f00; color:#fff;">X</button>
 
261
  </div>
262
  </div>
263
  `).join('');
264
  }
265
-
266
  function renderPatternGrid() {
267
- const tbody = document.querySelector('#patternGrid tbody');
268
- tbody.innerHTML = pattern.map((step, stepIndex) => `
269
- <tr>
270
- <td>${stepIndex + 1}</td>
271
- ${step.map((note, chIndex) => `
272
- <td onclick="setNote(${stepIndex}, ${chIndex})">${note || '-'}</td>
273
- `).join('')}
274
- </tr>
275
- `).join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  }
277
-
278
  function renderPiano() {
279
- const piano = document.getElementById('piano');
280
  const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
281
  const octaves = [3, 4, 5];
282
 
 
283
  piano.innerHTML = octaves.map(oct =>
284
  notes.map(note => {
285
  const isSharp = note.includes('#');
286
  const noteName = `${note}${oct}`;
 
287
  return `
288
  <button
289
  onclick="playNote('${noteName}')"
@@ -292,8 +316,11 @@
292
  height:100%;
293
  border:1px solid #0f0;
294
  background:${isSharp ? '#333' : '#111'};
295
- color:#0f0;
 
 
296
  ${isSharp ? 'margin:0 -2px; z-index:1;' : ''}
 
297
  "
298
  >
299
  ${noteName}
@@ -302,71 +329,111 @@
302
  }).join('')
303
  ).join('');
304
  }
305
-
306
- function setupEventListeners() {
307
- // Button event listeners
308
- document.getElementById('addChannelBtn').addEventListener('click', addChannel);
309
- document.getElementById('addInstrumentBtn').addEventListener('click', addInstrument);
310
- document.getElementById('updatePatternBtn').addEventListener('click', updatePatternLength);
311
- document.getElementById('addVerseBtn').addEventListener('click', addVerse);
312
- document.getElementById('copyVerseBtn').addEventListener('click', copyVerse);
313
- document.getElementById('playBtn').addEventListener('click', playSong);
314
- document.getElementById('stopBtn').addEventListener('click', stopSong);
315
- document.getElementById('generateBtn').addEventListener('click', generateSong);
316
- document.getElementById('saveBtn').addEventListener('click', saveProject);
317
-
318
- // File input
319
- document.getElementById('loadInput').addEventListener('change', loadProject);
320
-
321
- // Tempo control
322
- document.getElementById('tempoInput').addEventListener('input', function() {
323
- tempo = parseInt(this.value);
324
- document.getElementById('tempoValue').textContent = tempo;
325
- });
326
  }
327
-
 
328
  function playNote(note) {
329
- const oscillator = audioContext.createOscillator();
330
- oscillator.type = 'square';
331
- oscillator.frequency.value = noteToFrequency(note);
332
- oscillator.connect(audioContext.destination);
333
- oscillator.start();
334
- oscillator.stop(audioContext.currentTime + 0.5);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  }
336
-
337
  function noteToFrequency(note) {
338
  const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
339
  const octave = parseInt(note.slice(-1));
340
  const key = notes.indexOf(note.slice(0, -1));
341
  return 440 * Math.pow(2, (octave - 4) + (key - 9) / 12);
342
  }
343
-
344
- function setNote(step, channel) {
345
- // In a real implementation, this would set the note for the step/channel
346
- console.log(`Set note for step ${step+1}, channel ${channel+1}`);
347
- }
348
-
349
  function addChannel() {
350
- const newId = channels.length + 1;
351
  channels.push({
352
  id: newId,
353
  name: `CH${newId}`,
354
- instrumentId: instruments[0].id,
355
  muted: false
356
  });
357
 
358
- // Add new column to pattern
359
  pattern.forEach(step => step.push(null));
360
 
361
  renderChannels();
362
  renderPatternGrid();
363
  }
364
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  function addInstrument() {
366
  const types = ['square', 'triangle', 'sawtooth', 'sine', 'noise'];
367
  const colors = ['#0ff', '#f0f', '#ff0', '#f80', '#0f0', '#00f'];
368
 
369
- const newId = instruments.length + 1;
370
  const type = types[Math.floor(Math.random() * types.length)];
371
  const color = colors[Math.floor(Math.random() * colors.length)];
372
 
@@ -374,51 +441,107 @@
374
  id: newId,
375
  name: `INST${newId}`,
376
  type: type,
377
- color: color
 
 
 
 
 
378
  });
379
 
380
  renderInstruments();
381
  }
382
-
 
 
 
 
 
 
 
 
 
 
 
383
  function updatePatternLength() {
384
  const newLength = parseInt(document.getElementById('patternLengthInput').value);
385
  if (newLength < 4 || newLength > 64) return;
386
 
387
  if (newLength > pattern.length) {
388
- // Add steps
389
  for (let i = pattern.length; i < newLength; i++) {
390
  pattern.push(Array(channels.length).fill(null));
391
  }
392
  } else {
393
- // Remove steps
394
  pattern = pattern.slice(0, newLength);
395
  }
396
 
397
  verses[currentVerse] = JSON.parse(JSON.stringify(pattern));
398
  renderPatternGrid();
399
  }
400
-
401
  function addVerse() {
402
  const newVerse = Array(pattern.length).fill().map(() => Array(channels.length).fill(null));
403
  verses.push(newVerse);
404
  currentVerse = verses.length - 1;
405
  pattern = JSON.parse(JSON.stringify(newVerse));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
 
407
- // Update verse select
408
- const verseSelect = document.getElementById('verseSelect');
409
- verseSelect.innerHTML = verses.map((_, index) =>
410
- `<option value="${index}" ${index === currentVerse ? 'selected' : ''}>Verse ${index + 1}</option>`
411
- ).join('');
412
 
413
  renderPatternGrid();
414
  }
415
-
 
416
  function playSong() {
417
  if (isPlaying) return;
 
418
  isPlaying = true;
 
 
419
 
420
  let stepIndex = 0;
421
- const stepTime = 60000 / (tempo * 4); // 16th notes
422
 
423
  const playStep = () => {
424
  if (!isPlaying || stepIndex >= pattern.length) {
@@ -426,10 +549,33 @@
426
  return;
427
  }
428
 
429
- // Highlight current step in UI
430
- const rows = document.querySelectorAll('#patternGrid tbody tr');
431
- rows.forEach(row => row.style.backgroundColor = '');
432
- if (rows[stepIndex]) rows[stepIndex].style.backgroundColor = '#0f03';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
 
434
  stepIndex++;
435
  setTimeout(playStep, stepTime);
@@ -437,16 +583,16 @@
437
 
438
  playStep();
439
  }
440
-
441
  function stopSong() {
442
  isPlaying = false;
 
 
 
 
443
  }
444
-
445
- function generateSong() {
446
- // Placeholder for AI generation
447
- alert('AI song generation would happen here');
448
- }
449
-
450
  function saveProject() {
451
  const project = {
452
  channels,
@@ -464,7 +610,7 @@
464
  linkElement.setAttribute('download', 'chiptune-project.json');
465
  linkElement.click();
466
  }
467
-
468
  function loadProject(event) {
469
  const file = event.target.files[0];
470
  if (!file) return;
@@ -474,32 +620,23 @@
474
  try {
475
  const project = JSON.parse(e.target.result);
476
 
477
- // Load project data
478
  channels = project.channels || [];
479
  instruments = project.instruments || [];
480
  verses = project.verses || [Array(16).fill().map(() => Array(channels.length).fill(null))];
481
  tempo = project.tempo || 120;
482
  currentVerse = 0;
483
 
484
- // Update UI
485
  document.getElementById('tempoInput').value = tempo;
486
  document.getElementById('tempoValue').textContent = tempo;
487
  document.getElementById('patternLengthInput').value = project.patternLength || 16;
488
 
489
- // Load current verse pattern
490
  pattern = JSON.parse(JSON.stringify(verses[currentVerse]));
491
 
492
- // Re-render everything
493
  renderChannels();
494
  renderInstruments();
495
  renderPatternGrid();
496
  renderPiano();
497
-
498
- // Update verse select
499
- const verseSelect = document.getElementById('verseSelect');
500
- verseSelect.innerHTML = verses.map((_, index) =>
501
- `<option value="${index}" ${index === currentVerse ? 'selected' : ''}>Verse ${index + 1}</option>`
502
- ).join('');
503
 
504
  } catch (error) {
505
  console.error("Error loading project:", error);
@@ -507,11 +644,50 @@
507
  }
508
  };
509
  reader.readAsText(file);
510
-
511
- // Reset file input
512
  event.target.value = '';
513
  }
514
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
  // Initialize on load
516
  window.onload = init;
517
  </script>
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>8-Bit Chiptune Generator</title>
 
7
  <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
9
+
10
  body {
11
  margin: 0;
12
  padding: 0;
13
+ font-family: 'Press Start 2P', cursive;
14
+ background-color: #000;
15
  color: #0f0;
 
16
  overflow: hidden;
17
  }
18
+
19
  #appCtn {
20
  display: flex;
21
  flex-direction: column;
22
  height: 100vh;
23
+ background: #000;
24
+ color: #0f0;
25
+ font-size: 10px;
26
  }
27
+
28
  header {
29
  padding: 10px;
30
  background: #111;
 
33
  justify-content: space-between;
34
  align-items: center;
35
  }
36
+
37
  h1 {
 
38
  margin: 0;
39
+ font-size: 14px;
40
  }
41
+
42
  button {
43
  margin: 0 5px;
44
  padding: 5px 10px;
 
46
  color: #000;
47
  border: none;
48
  cursor: pointer;
49
+ font-family: 'Press Start 2P', cursive;
50
  font-size: 10px;
51
  }
52
+
53
+ #playBtn { background: #0f0; }
54
+ #stopBtn { background: #f00; color: #fff; }
55
+ #saveBtn { background: #ff0; }
56
+ #loadBtn { background: #ff0; }
57
+
58
+ #mainContent {
 
 
 
 
59
  display: flex;
60
  flex: 1;
61
  overflow: hidden;
62
  }
63
+
64
  aside {
65
  width: 200px;
66
  background: #111;
 
68
  padding: 10px;
69
  overflow-y: auto;
70
  }
71
+
72
  #patternGrid {
73
  flex: 1;
74
  border: 1px solid #0f0;
75
  background: #0a0a0a;
76
  overflow: auto;
77
  }
78
+
79
  #noteInput {
80
  margin-top: 10px;
81
  padding: 10px;
 
83
  border: 1px solid #0f0;
84
  overflow-x: auto;
85
  }
86
+
87
  #piano {
88
  display: flex;
89
  height: 40px;
90
  min-width: 600px;
91
  }
92
+
93
+ /* Loading indicator */
94
+ #loadingIndicator {
95
+ position: fixed;
96
+ top: 50%;
97
+ left: 50%;
98
+ transform: translate(-50%, -50%);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  background: #111;
100
+ padding: 20px;
101
+ border: 2px solid #0f0;
102
+ text-align: center;
 
 
 
 
103
  }
104
  </style>
105
  </head>
 
108
  <header>
109
  <h1>8-BIT CHIPTUNE GENERATOR</h1>
110
  <div>
111
+ <button id="generateBtn" onclick="generateSong()">GENERATE AI SONG</button>
112
+ <button id="playBtn" onclick="playSong()">PLAY</button>
113
+ <button id="stopBtn" onclick="stopSong()">STOP</button>
114
+ <button id="saveBtn" onclick="saveProject()">SAVE</button>
115
+ <label for="loadInput" style="display: inline-block; padding: 5px 10px; background: #ff0; color: #000; cursor: pointer;">LOAD</label>
116
+ <input id="loadInput" type="file" style="display: none;" onchange="loadProject(event)">
117
  </div>
118
  </header>
119
+
120
+ <div id="mainContent">
121
  <aside>
122
+ <div style="margin-bottom: 20px;">
123
+ <h2 style="font-size: 12px; margin-bottom: 10px;">CHANNELS</h2>
124
+ <div id="channelList" style="margin-bottom: 10px;"></div>
125
+ <button id="addChannelBtn" onclick="addChannel()" style="width: 100%; padding: 5px; background: #0f0; color: #000;">+ ADD CHANNEL</button>
126
  </div>
127
+
128
  <div>
129
+ <h2 style="font-size: 12px; margin-bottom: 10px;">INSTRUMENTS</h2>
130
+ <div id="instrumentList" style="margin-bottom: 10px;"></div>
131
+ <button id="addInstrumentBtn" onclick="addInstrument()" style="width: 100%; padding: 5px; background: #0f0; color: #000;">+ ADD INSTRUMENT</button>
132
  </div>
133
+
134
+ <div style="margin-top: 20px;">
135
+ <h2 style="font-size: 12px; margin-bottom: 10px;">PATTERN</h2>
136
+ <div style="margin-bottom: 10px;">
137
  <label>STEPS: </label>
138
+ <input id="patternLengthInput" type="number" min="4" max="64" value="16" style="width: 50px; margin-left: 5px; padding: 2px; background: #000; color: #0f0; border: 1px solid #0f0;">
139
+ <button onclick="updatePatternLength()" style="margin-left: 5px; padding: 2px 5px; background: #0f0; color: #000;">UPDATE</button>
140
  </div>
141
+ <div style="margin-bottom: 10px;">
142
  <label>VERSE: </label>
143
+ <select id="verseSelect" onchange="switchVerse()" style="margin-left: 5px; padding: 2px; background: #000; color: #0f0; border: 1px solid #0f0;">
144
  <option value="0">Verse 1</option>
145
  </select>
146
+ <button onclick="addVerse()" style="margin-left: 5px; padding: 2px 5px; background: #0f0; color: #000;">+</button>
147
+ <button onclick="copyVerse()" style="margin-left: 5px; padding: 2px 5px; background: #ff0; color: #000;">COPY</button>
148
  </div>
149
  </div>
150
  </aside>
151
+
152
+ <div style="flex: 1; display: flex; flex-direction: column;">
153
+ <div style="margin-bottom: 10px; padding: 10px;">
154
+ <h2 style="font-size: 12px;">PATTERN EDITOR</h2>
155
+ <div style="margin: 5px 0;">
156
  <span>TEMPO: </span>
157
+ <input id="tempoInput" type="range" min="60" max="240" value="120" style="width: 100px;">
158
  <span id="tempoValue">120</span> BPM
159
  </div>
160
  </div>
161
+
162
+ <div id="patternGrid"></div>
163
+
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  <div id="noteInput">
165
+ <h3 style="font-size: 10px; margin-bottom: 5px;">NOTE INPUT</h3>
166
  <div id="piano"></div>
167
  </div>
168
  </div>
169
+ </div>
170
+
171
+ <div id="loadingIndicator" hidden>
172
+ <div>GENERATING...</div>
173
+ <div style="margin-top: 10px;">⏳</div>
174
+ </div>
175
  </div>
176
 
177
  <script>
 
181
  let pattern = [];
182
  let verses = [];
183
  let currentVerse = 0;
184
+ let currentChannel = 0;
185
+ let currentStep = 0;
186
  let isPlaying = false;
187
  let audioContext;
188
+ let gainNode;
189
+ let oscillators = [];
190
  let tempo = 120;
191
+ let selectedNote = null;
192
+ let editingChannelId = null;
193
+ let editingInstrumentId = null;
194
+ let isDrawing = false;
195
+
196
  // Initialize app
197
  function init() {
198
  // Create default instruments
199
  instruments = [
200
+ { id: 1, name: 'SQUARE', type: 'square', color: '#0ff', attack: 0, decay: 100, sustain: 0.7, release: 300, vibrato: 0 },
201
+ { id: 2, name: 'TRIANGLE', type: 'triangle', color: '#f0f', attack: 50, decay: 150, sustain: 0.6, release: 400, vibrato: 2 },
202
+ { id: 3, name: 'NOISE', type: 'noise', color: '#ff0', attack: 10, decay: 50, sustain: 0.5, release: 200, vibrato: 0 },
203
+ { id: 4, name: 'SINE', type: 'sine', color: '#f80', attack: 100, decay: 200, sustain: 0.8, release: 500, vibrato: 5 }
204
  ];
205
 
206
  // Create default channels
207
  channels = [
208
  { id: 1, name: 'CH1', instrumentId: 1, muted: false },
209
  { id: 2, name: 'CH2', instrumentId: 2, muted: false },
210
+ { id: 3, name: 'CH3', instrumentId: 3, muted: false },
211
+ { id: 4, name: 'CH4', instrumentId: 4, muted: false }
212
  ];
213
 
214
  // Initialize empty pattern
215
+ pattern = Array(16).fill().map(() => Array(channels.length).fill(null));
216
+ verses = [JSON.parse(JSON.stringify(pattern))];
217
 
218
  // Setup audio context
219
  audioContext = new (window.AudioContext || window.webkitAudioContext)();
220
+ gainNode = audioContext.createGain();
221
+ gainNode.connect(audioContext.destination);
222
+ gainNode.gain.value = 0.2;
223
 
224
  // Render UI
225
  renderChannels();
226
  renderInstruments();
227
  renderPatternGrid();
228
  renderPiano();
229
+ renderVerseControls();
230
 
231
+ // Setup tempo control
232
+ document.getElementById('tempoInput').oninput = function() {
233
+ tempo = parseInt(this.value);
234
+ document.getElementById('tempoValue').textContent = tempo;
235
+ };
 
 
236
  }
237
+
238
+ // UI rendering functions
239
  function renderChannels() {
240
  const channelList = document.getElementById('channelList');
241
  channelList.innerHTML = channels.map(ch => `
242
+ <div style="display:flex; justify-content:space-between; align-items:center; margin:5px 0; padding:5px; background:#0a0a0a; border:1px solid #0f0;">
243
  <span>${ch.name}</span>
244
  <div>
245
+ <button onclick="toggleMute(${ch.id})" style="margin-right:5px; padding:2px 5px; background:${ch.muted ? '#f00' : '#0f0'}; color:#000;">${ch.muted ? 'UNMUTE' : 'MUTE'}</button>
246
+ <button onclick="editChannel(${ch.id})" style="margin-right:5px; padding:2px 5px; background:#ff0; color:#000;">EDIT</button>
247
+ <button onclick="removeChannel(${ch.id})" style="padding:2px 5px; background:#f00; color:#fff;">X</button>
248
  </div>
249
  </div>
250
  `).join('');
251
  }
252
+
253
  function renderInstruments() {
254
  const instrumentList = document.getElementById('instrumentList');
255
  instrumentList.innerHTML = instruments.map(inst => `
256
+ <div style="display:flex; justify-content:space-between; align-items:center; margin:5px 0; padding:5px; background:#0a0a0a; border:1px solid #${inst.color.replace('#','')};">
257
+ <span style="color:#${inst.color.replace('#','')}">${inst.name}</span>
258
  <div>
259
+ <button onclick="editInstrument(${inst.id})" style="margin-right:5px; padding:2px 5px; background:#ff0; color:#000;">EDIT</button>
260
+ <button onclick="removeInstrument(${inst.id})" style="padding:2px 5px; background:#f00; color:#fff;">X</button>
261
  </div>
262
  </div>
263
  `).join('');
264
  }
265
+
266
  function renderPatternGrid() {
267
+ const grid = document.getElementById('patternGrid');
268
+ grid.innerHTML = `
269
+ <table style="width:100%; border-collapse:collapse;">
270
+ <thead>
271
+ <tr>
272
+ <th style="width:50px; border:1px solid #0f0; padding:5px;">STEP</th>
273
+ ${channels.map(ch => `
274
+ <th style="border:1px solid #0f0; padding:5px; color:#${instruments.find(i => i.id === ch.instrumentId)?.color.replace('#','') || '0f0'}">${ch.name}</th>
275
+ `).join('')}
276
+ </tr>
277
+ </thead>
278
+ <tbody>
279
+ ${pattern.map((step, stepIndex) => `
280
+ <tr>
281
+ <td style="border:1px solid #0f0; padding:5px; text-align:center; background:#111;">${stepIndex + 1}</td>
282
+ ${step.map((note, chIndex) => `
283
+ <td
284
+ data-step="${stepIndex}"
285
+ data-channel="${chIndex}"
286
+ onmousedown="startDrawing(event, ${chIndex}, ${stepIndex})"
287
+ onmouseover="continueDrawing(event, ${chIndex}, ${stepIndex})"
288
+ onmouseup="stopDrawing()"
289
+ style="border:1px solid #0f0; padding:5px; text-align:center; cursor:pointer; background:${currentChannel === chIndex && currentStep === stepIndex ? '#0a0a0a' : '#000'};"
290
+ >
291
+ ${note || '-'}
292
+ </td>
293
+ `).join('')}
294
+ </tr>
295
+ `).join('')}
296
+ </tbody>
297
+ </table>
298
+ `;
299
  }
300
+
301
  function renderPiano() {
 
302
  const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
303
  const octaves = [3, 4, 5];
304
 
305
+ const piano = document.getElementById('piano');
306
  piano.innerHTML = octaves.map(oct =>
307
  notes.map(note => {
308
  const isSharp = note.includes('#');
309
  const noteName = `${note}${oct}`;
310
+ const isSelected = selectedNote === noteName;
311
  return `
312
  <button
313
  onclick="playNote('${noteName}')"
 
316
  height:100%;
317
  border:1px solid #0f0;
318
  background:${isSharp ? '#333' : '#111'};
319
+ color:#0f0;
320
+ cursor:pointer;
321
+ font-size:7px;
322
  ${isSharp ? 'margin:0 -2px; z-index:1;' : ''}
323
+ ${isSelected ? 'box-shadow: 0 0 5px #0f0; border-width: 2px;' : ''}
324
  "
325
  >
326
  ${noteName}
 
329
  }).join('')
330
  ).join('');
331
  }
332
+
333
+ function renderVerseControls() {
334
+ const verseSelect = document.getElementById('verseSelect');
335
+ verseSelect.innerHTML = verses.map((_, index) =>
336
+ `<option value="${index}" ${index === currentVerse ? 'selected' : ''}>Verse ${index + 1}</option>`
337
+ ).join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  }
339
+
340
+ // Core functionality
341
  function playNote(note) {
342
+ selectedNote = note;
343
+ renderPiano();
344
+
345
+ if (currentChannel < channels.length && currentStep < pattern.length) {
346
+ pattern[currentStep][currentChannel] = note;
347
+ renderPatternGrid();
348
+ }
349
+
350
+ const channel = channels[currentChannel];
351
+ const instrument = instruments.find(i => i.id === channel.instrumentId);
352
+
353
+ if (instrument && !channel.muted) {
354
+ const now = audioContext.currentTime;
355
+ const oscillator = audioContext.createOscillator();
356
+ const gainNode = audioContext.createGain();
357
+
358
+ oscillator.type = instrument.type;
359
+ oscillator.frequency.value = noteToFrequency(note);
360
+
361
+ if (instrument.vibrato > 0) {
362
+ const vibratoOsc = audioContext.createOscillator();
363
+ const vibratoGain = audioContext.createGain();
364
+
365
+ vibratoOsc.type = 'sine';
366
+ vibratoOsc.frequency.value = instrument.vibrato;
367
+ vibratoGain.gain.value = instrument.vibrato * 5;
368
+
369
+ vibratoOsc.connect(vibratoGain);
370
+ vibratoGain.connect(oscillator.frequency);
371
+
372
+ vibratoOsc.start(now);
373
+ vibratoOsc.stop(now + 0.5);
374
+ }
375
+
376
+ gainNode.gain.setValueAtTime(0, now);
377
+ gainNode.gain.linearRampToValueAtTime(1, now + instrument.attack / 1000);
378
+ gainNode.gain.linearRampToValueAtTime(instrument.sustain, now + (instrument.attack + instrument.decay) / 1000);
379
+ gainNode.gain.linearRampToValueAtTime(0, now + (instrument.attack + instrument.decay + instrument.release) / 1000);
380
+
381
+ oscillator.connect(gainNode);
382
+ gainNode.connect(audioContext.destination);
383
+
384
+ oscillator.start(now);
385
+ oscillator.stop(now + (instrument.attack + instrument.decay + instrument.release) / 1000);
386
+ }
387
  }
388
+
389
  function noteToFrequency(note) {
390
  const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
391
  const octave = parseInt(note.slice(-1));
392
  const key = notes.indexOf(note.slice(0, -1));
393
  return 440 * Math.pow(2, (octave - 4) + (key - 9) / 12);
394
  }
395
+
396
+ // Channel and instrument management
 
 
 
 
397
  function addChannel() {
398
+ const newId = Math.max(0, ...channels.map(ch => ch.id)) + 1;
399
  channels.push({
400
  id: newId,
401
  name: `CH${newId}`,
402
+ instrumentId: instruments[0]?.id || 1,
403
  muted: false
404
  });
405
 
 
406
  pattern.forEach(step => step.push(null));
407
 
408
  renderChannels();
409
  renderPatternGrid();
410
  }
411
+
412
+ function removeChannel(id) {
413
+ if (channels.length <= 1) return;
414
+
415
+ const index = channels.findIndex(ch => ch.id === id);
416
+ if (index !== -1) {
417
+ channels.splice(index, 1);
418
+ pattern.forEach(step => step.splice(index, 1));
419
+ renderChannels();
420
+ renderPatternGrid();
421
+ }
422
+ }
423
+
424
+ function toggleMute(id) {
425
+ const channel = channels.find(ch => ch.id === id);
426
+ if (channel) {
427
+ channel.muted = !channel.muted;
428
+ renderChannels();
429
+ }
430
+ }
431
+
432
  function addInstrument() {
433
  const types = ['square', 'triangle', 'sawtooth', 'sine', 'noise'];
434
  const colors = ['#0ff', '#f0f', '#ff0', '#f80', '#0f0', '#00f'];
435
 
436
+ const newId = Math.max(0, ...instruments.map(inst => inst.id)) + 1;
437
  const type = types[Math.floor(Math.random() * types.length)];
438
  const color = colors[Math.floor(Math.random() * colors.length)];
439
 
 
441
  id: newId,
442
  name: `INST${newId}`,
443
  type: type,
444
+ color: color,
445
+ attack: 0,
446
+ decay: 100,
447
+ sustain: 0.7,
448
+ release: 300,
449
+ vibrato: 0
450
  });
451
 
452
  renderInstruments();
453
  }
454
+
455
+ function removeInstrument(id) {
456
+ if (instruments.length <= 1) return;
457
+
458
+ const index = instruments.findIndex(inst => inst.id === id);
459
+ if (index !== -1) {
460
+ instruments.splice(index, 1);
461
+ renderInstruments();
462
+ }
463
+ }
464
+
465
+ // Pattern management
466
  function updatePatternLength() {
467
  const newLength = parseInt(document.getElementById('patternLengthInput').value);
468
  if (newLength < 4 || newLength > 64) return;
469
 
470
  if (newLength > pattern.length) {
 
471
  for (let i = pattern.length; i < newLength; i++) {
472
  pattern.push(Array(channels.length).fill(null));
473
  }
474
  } else {
 
475
  pattern = pattern.slice(0, newLength);
476
  }
477
 
478
  verses[currentVerse] = JSON.parse(JSON.stringify(pattern));
479
  renderPatternGrid();
480
  }
481
+
482
  function addVerse() {
483
  const newVerse = Array(pattern.length).fill().map(() => Array(channels.length).fill(null));
484
  verses.push(newVerse);
485
  currentVerse = verses.length - 1;
486
  pattern = JSON.parse(JSON.stringify(newVerse));
487
+ renderVerseControls();
488
+ renderPatternGrid();
489
+ }
490
+
491
+ function copyVerse() {
492
+ const copy = JSON.parse(JSON.stringify(pattern));
493
+ verses.push(copy);
494
+ currentVerse = verses.length - 1;
495
+ renderVerseControls();
496
+ }
497
+
498
+ function switchVerse() {
499
+ const selectedVerse = parseInt(document.getElementById('verseSelect').value);
500
+ if (selectedVerse >= 0 && selectedVerse < verses.length) {
501
+ verses[currentVerse] = JSON.parse(JSON.stringify(pattern));
502
+ currentVerse = selectedVerse;
503
+ pattern = JSON.parse(JSON.stringify(verses[currentVerse]));
504
+ renderPatternGrid();
505
+ }
506
+ }
507
+
508
+ // Drawing functionality
509
+ function startDrawing(e, chIndex, stepIndex) {
510
+ isDrawing = true;
511
+ selectCell(chIndex, stepIndex);
512
+ }
513
+
514
+ function continueDrawing(e, chIndex, stepIndex) {
515
+ if (isDrawing && selectedNote) {
516
+ selectCell(chIndex, stepIndex);
517
+ }
518
+ }
519
+
520
+ function stopDrawing() {
521
+ isDrawing = false;
522
+ }
523
+
524
+ function selectCell(chIndex, stepIndex) {
525
+ currentChannel = chIndex;
526
+ currentStep = stepIndex;
527
 
528
+ if (selectedNote) {
529
+ pattern[stepIndex][chIndex] = selectedNote;
530
+ }
 
 
531
 
532
  renderPatternGrid();
533
  }
534
+
535
+ // Playback functionality
536
  function playSong() {
537
  if (isPlaying) return;
538
+
539
  isPlaying = true;
540
+ document.getElementById('playBtn').disabled = true;
541
+ document.getElementById('stopBtn').disabled = false;
542
 
543
  let stepIndex = 0;
544
+ const stepTime = 60000 / (tempo * 4);
545
 
546
  const playStep = () => {
547
  if (!isPlaying || stepIndex >= pattern.length) {
 
549
  return;
550
  }
551
 
552
+ pattern[stepIndex].forEach((note, chIndex) => {
553
+ if (note && !channels[chIndex]?.muted) {
554
+ const instrument = instruments.find(i => i.id === channels[chIndex]?.instrumentId);
555
+ if (instrument) {
556
+ const now = audioContext.currentTime;
557
+ const oscillator = audioContext.createOscillator();
558
+ const gainNode = audioContext.createGain();
559
+
560
+ oscillator.type = instrument.type;
561
+ oscillator.frequency.value = noteToFrequency(note);
562
+
563
+ gainNode.gain.setValueAtTime(0, now);
564
+ gainNode.gain.linearRampToValueAtTime(1, now + instrument.attack / 1000);
565
+ gainNode.gain.linearRampToValueAtTime(instrument.sustain, now + (instrument.attack + instrument.decay) / 1000);
566
+ gainNode.gain.linearRampToValueAtTime(0, now + (instrument.attack + instrument.decay + instrument.release) / 1000);
567
+
568
+ oscillator.connect(gainNode);
569
+ gainNode.connect(audioContext.destination);
570
+
571
+ oscillator.start(now);
572
+ oscillator.stop(now + (instrument.attack + instrument.decay + instrument.release) / 1000);
573
+ }
574
+ }
575
+ });
576
+
577
+ currentStep = stepIndex;
578
+ renderPatternGrid();
579
 
580
  stepIndex++;
581
  setTimeout(playStep, stepTime);
 
583
 
584
  playStep();
585
  }
586
+
587
  function stopSong() {
588
  isPlaying = false;
589
+ document.getElementById('playBtn').disabled = false;
590
+ document.getElementById('stopBtn').disabled = true;
591
+ currentStep = 0;
592
+ renderPatternGrid();
593
  }
594
+
595
+ // Project management
 
 
 
 
596
  function saveProject() {
597
  const project = {
598
  channels,
 
610
  linkElement.setAttribute('download', 'chiptune-project.json');
611
  linkElement.click();
612
  }
613
+
614
  function loadProject(event) {
615
  const file = event.target.files[0];
616
  if (!file) return;
 
620
  try {
621
  const project = JSON.parse(e.target.result);
622
 
 
623
  channels = project.channels || [];
624
  instruments = project.instruments || [];
625
  verses = project.verses || [Array(16).fill().map(() => Array(channels.length).fill(null))];
626
  tempo = project.tempo || 120;
627
  currentVerse = 0;
628
 
 
629
  document.getElementById('tempoInput').value = tempo;
630
  document.getElementById('tempoValue').textContent = tempo;
631
  document.getElementById('patternLengthInput').value = project.patternLength || 16;
632
 
 
633
  pattern = JSON.parse(JSON.stringify(verses[currentVerse]));
634
 
 
635
  renderChannels();
636
  renderInstruments();
637
  renderPatternGrid();
638
  renderPiano();
639
+ renderVerseControls();
 
 
 
 
 
640
 
641
  } catch (error) {
642
  console.error("Error loading project:", error);
 
644
  }
645
  };
646
  reader.readAsText(file);
 
 
647
  event.target.value = '';
648
  }
649
+
650
+ // AI generation
651
+ async function generateSong() {
652
+ const generateBtn = document.getElementById('generateBtn');
653
+ const loadingIndicator = document.getElementById('loadingIndicator');
654
+
655
+ generateBtn.disabled = true;
656
+ generateBtn.textContent = "GENERATING...";
657
+ loadingIndicator.hidden = false;
658
+
659
+ try {
660
+ const prompt = `Generate a 16-step chiptune pattern for ${channels.length} channels using only 8-bit instruments.
661
+ Format as JSON with steps array where each step contains notes for each channel.
662
+ Use note names like "C4", "D#5", or null for rests.
663
+ Make it upbeat and energetic like classic NES games.`;
664
+
665
+ // Simulate API call (replace with actual API implementation)
666
+ const generatedPattern = {
667
+ steps: Array(16).fill().map(() =>
668
+ Array(channels.length).fill().map(() =>
669
+ Math.random() > 0.7 ? ['C3','D3','E3','F3','G3','A3','B3','C4','D4','E4','F4','G4','A4','B4','C5'][Math.floor(Math.random() * 15)] : null
670
+ )
671
+ )
672
+ };
673
+
674
+ if (generatedPattern.steps && Array.isArray(generatedPattern.steps)) {
675
+ pattern = generatedPattern.steps;
676
+ verses[currentVerse] = JSON.parse(JSON.stringify(pattern));
677
+ renderPatternGrid();
678
+ } else {
679
+ alert("Invalid pattern format generated");
680
+ }
681
+ } catch (error) {
682
+ console.error("Generation failed:", error);
683
+ alert("Failed to generate song. Please try again.");
684
+ } finally {
685
+ generateBtn.disabled = false;
686
+ generateBtn.textContent = "GENERATE AI SONG";
687
+ loadingIndicator.hidden = true;
688
+ }
689
+ }
690
+
691
  // Initialize on load
692
  window.onload = init;
693
  </script>
index.html CHANGED
@@ -46,12 +46,12 @@
46
  <span class="ml-2 text-xl font-bold bg-gradient-to-r from-indigo-300 to-purple-400 bg-clip-text text-transparent">PanoramaGen 360°</span>
47
  </div>
48
  </div>
49
- <div class="hidden md:block">
50
  <div class="ml-10 flex items-baseline space-x-4">
51
  <a href="#" class="px-3 py-2 rounded-md text-sm font-medium text-white bg-indigo-900">Home</a>
52
  <a href="MyProfileView.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-800">Profile</a>
53
  <a href="MyGarageView.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-800">Garage</a>
54
- <a href="chiptune.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-800">Chiptune Maker</a>
55
  <a href="#demo" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-800">Demo</a>
56
  <a href="#installation" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-800">Installation</a>
57
  <a href="#features" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-800">Features</a>
 
46
  <span class="ml-2 text-xl font-bold bg-gradient-to-r from-indigo-300 to-purple-400 bg-clip-text text-transparent">PanoramaGen 360°</span>
47
  </div>
48
  </div>
49
+ <div class="hidden md:block">
50
  <div class="ml-10 flex items-baseline space-x-4">
51
  <a href="#" class="px-3 py-2 rounded-md text-sm font-medium text-white bg-indigo-900">Home</a>
52
  <a href="MyProfileView.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-800">Profile</a>
53
  <a href="MyGarageView.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-800">Garage</a>
54
+ <a href="chiptune.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-800">Chiptune</a>
55
  <a href="#demo" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-800">Demo</a>
56
  <a href="#installation" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-800">Installation</a>
57
  <a href="#features" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-800">Features</a>