From cbeb0382bf3321dd3ef819677927c9e60bb0a88d Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 12 Jan 2024 17:57:15 +0100 Subject: [PATCH 01/12] Update the hello world response text --- app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app.py b/app.py index d82c51f0d..d825abd75 100644 --- a/app.py +++ b/app.py @@ -3,4 +3,7 @@ @app.route('/') def hello_world(): - return 'Hello, World!' + return 'Hello, World! Local' + +if __name__ == "__main__": + app.run(); \ No newline at end of file From 8a1adc65b6267e6b69df517d1b2eccd5400f0422 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 12 Jan 2024 18:11:39 +0100 Subject: [PATCH 02/12] Update the hello world response text --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index d825abd75..1dc184ee4 100644 --- a/app.py +++ b/app.py @@ -3,7 +3,7 @@ @app.route('/') def hello_world(): - return 'Hello, World! Local' + return 'Hello, World! Local testing' if __name__ == "__main__": app.run(); \ No newline at end of file From 3d5e3655adf20a30490bbbbfc981c4d5d903ec48 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 19 Jan 2024 16:09:24 +0100 Subject: [PATCH 03/12] Update the hello world response text --- .idea/.gitignore | 8 +++ .idea/flask-hello-world.iml | 16 +++++ .../inspectionProfiles/profiles_settings.xml | 6 ++ .idea/modules.xml | 8 +++ .idea/vcs.xml | 6 ++ app.py | 65 ++++++++++++++++++- 6 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/flask-hello-world.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..13566b81b --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/flask-hello-world.iml b/.idea/flask-hello-world.iml new file mode 100644 index 000000000..66cd14894 --- /dev/null +++ b/.idea/flask-hello-world.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 000000000..105ce2da2 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..cb00f9bfd --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..35eb1ddfb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app.py b/app.py index 1dc184ee4..24cee42f7 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,72 @@ from flask import Flask +from flask import jsonify +from flask import request +import tkinter as tk +import random app = Flask(__name__) +lessons = [ + {"id": 1, "name": "Python programing", "credits": 5}, + {"id": 2, "name": "Java programing", "credits": 4}, + {"id": 3, "name": "TypeScript programing", "credits": 3}, + {"id": 4, "name": "C++ programing", "credits": 1} +] + @app.route('/') def hello_world(): return 'Hello, World! Local testing' +@app.route('/lessons') +def get_lessons(): + return jsonify(lessons) + if __name__ == "__main__": - app.run(); \ No newline at end of file + app.run() + + +@app.route('/create-lesson') +def create_lesson(): + name = request.args["name"] + credits = request.args["credits"] + id = len(lessons) + 1 + newLesson = {"id": id, "name": name, "credits": credits} + lessons.append(newLesson) + return jsonify(lessons) + +@app.route('/delete/', methods=["GET"]) +def delete_lesson(lesson_id): + for lesson in lessons: + if lesson["id"] == int(lesson_id): + lessons.remove(lesson) + return jsonify(lessons) + +@app.route('/test') +def test(): + # Create the main application window + root = tk.Tk() + + # Set the size of the window to 640x800 pixels + root.geometry("800x640") + + # Set a title for the window + root.title("Application Window") + + root.configure(bg="grey") + + def change_button(old_button): + old_button.place_forget() # This removes the old button + create_trolled_button() + + def create_trolled_button(): + x_pos = random.randint(0, 750) + y_pos = random.randint(0, 610) + trolled_button = tk.Button(root, text="Get Trolled", command=lambda: + [trolled_button.place_forget(), create_trolled_button()]) + trolled_button.pack() + trolled_button.place(x=x_pos, y=y_pos) + + play_button = tk.Button(root, text="Play", command=lambda: change_button(play_button), fg="black", bg="white") + play_button.place(relx=0.5, rely=0.5, anchor=tk.CENTER) + + # Run the application + root.mainloop() From 2b048fe28c84fd431c5c5fbc5e296568c195972e Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 19 Jan 2024 16:37:44 +0100 Subject: [PATCH 04/12] Update the hello world response text --- app.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/app.py b/app.py index 24cee42f7..5014a11ff 100644 --- a/app.py +++ b/app.py @@ -14,7 +14,35 @@ @app.route('/') def hello_world(): - return 'Hello, World! Local testing' + # Create the main application window + root = tk.Tk() + + # Set the size of the window to 640x800 pixels + root.geometry("800x640") + + # Set a title for the window + root.title("Application Window") + + root.configure(bg="grey") + + def change_button(old_button): + old_button.place_forget() # This removes the old button + create_trolled_button() + + def create_trolled_button(): + x_pos = random.randint(0, 750) + y_pos = random.randint(0, 610) + trolled_button = tk.Button(root, text="Get Trolled", command=lambda: + [trolled_button.place_forget(), create_trolled_button()]) + trolled_button.pack() + trolled_button.place(x=x_pos, y=y_pos) + + play_button = tk.Button(root, text="Play", command=lambda: change_button(play_button), fg="black", bg="white") + play_button.place(relx=0.5, rely=0.5, anchor=tk.CENTER) + + # Run the application + root.mainloop() + return 'Hello, World! Local testing123' @app.route('/lessons') def get_lessons(): From a75c3b50f3030786d74609bf2ae25c12e17d9396 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 9 Feb 2024 23:11:12 +0100 Subject: [PATCH 05/12] Update the hello world response text --- app.py | 79 ++++---------- requirements.txt | 1 + static/css/style.css | 62 +++++++++++ static/js/script.js | 244 +++++++++++++++++++++++++++++++++++++++++++ templates/home.html | 32 ++++++ 5 files changed, 361 insertions(+), 57 deletions(-) create mode 100644 static/css/style.css create mode 100644 static/js/script.js create mode 100644 templates/home.html diff --git a/app.py b/app.py index 5014a11ff..4e7b6d76e 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,11 @@ -from flask import Flask +from flask import Flask, render_template, redirect from flask import jsonify from flask import request import tkinter as tk +from flask_cors import CORS import random app = Flask(__name__) +CORS(app) lessons = [ {"id": 1, "name": "Python programing", "credits": 5}, @@ -14,40 +16,20 @@ @app.route('/') def hello_world(): - # Create the main application window - root = tk.Tk() + return 'Hello, World! Local testing1243' - # Set the size of the window to 640x800 pixels - root.geometry("800x640") +@app.route('/lessons-jinja') +def get_lessons_jinja(): + return render_template("home.html", les=lessons) - # Set a title for the window - root.title("Application Window") - - root.configure(bg="grey") - - def change_button(old_button): - old_button.place_forget() # This removes the old button - create_trolled_button() - - def create_trolled_button(): - x_pos = random.randint(0, 750) - y_pos = random.randint(0, 610) - trolled_button = tk.Button(root, text="Get Trolled", command=lambda: - [trolled_button.place_forget(), create_trolled_button()]) - trolled_button.pack() - trolled_button.place(x=x_pos, y=y_pos) - - play_button = tk.Button(root, text="Play", command=lambda: change_button(play_button), fg="black", bg="white") - play_button.place(relx=0.5, rely=0.5, anchor=tk.CENTER) - - # Run the application - root.mainloop() - return 'Hello, World! Local testing123' - -@app.route('/lessons') -def get_lessons(): +@app.route('/lessons-javascript') +def get_lessons_javascript(): return jsonify(lessons) +@app.route('/home-javascript') +def load_home_js(): + return render_template("home-js.html") + if __name__ == "__main__": app.run() @@ -66,35 +48,18 @@ def delete_lesson(lesson_id): for lesson in lessons: if lesson["id"] == int(lesson_id): lessons.remove(lesson) - return jsonify(lessons) - -@app.route('/test') -def test(): - # Create the main application window - root = tk.Tk() - - # Set the size of the window to 640x800 pixels - root.geometry("800x640") + # return jsonify(lessons) + return redirect("/lessons-jinja") - # Set a title for the window - root.title("Application Window") - root.configure(bg="grey") +my_recordings = {} - def change_button(old_button): - old_button.place_forget() # This removes the old button - create_trolled_button() +@app.route('/saveRecording', methods=["POST"]) +def saveRecording(): + data = request.get_json() + print(data) + return {"status" : "OK"} - def create_trolled_button(): - x_pos = random.randint(0, 750) - y_pos = random.randint(0, 610) - trolled_button = tk.Button(root, text="Get Trolled", command=lambda: - [trolled_button.place_forget(), create_trolled_button()]) - trolled_button.pack() - trolled_button.place(x=x_pos, y=y_pos) - play_button = tk.Button(root, text="Play", command=lambda: change_button(play_button), fg="black", bg="white") - play_button.place(relx=0.5, rely=0.5, anchor=tk.CENTER) - # Run the application - root.mainloop() +# SAVE, DELETE, SHOW \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 147ddd086..ac272f07a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ Flask Gunicorn +flask_cors \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 000000000..ed7d142f2 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,62 @@ +body { + width: 100%; + padding-left: 1%; + height: 100%; + margin: 5px; + background-color: #b0c4de; +} + +h1 { + margin-bottom: 39px; +} + +#container { + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; +} + + +.piano-tile { + display: flex; + text-align: center; + align-items: flex-start; + padding-top: 19%; + justify-content: center; + user-select: none; + width: 47px; + /* Adjust the width as needed */ + height: 250px; + /* Adjust the height as needed */ + background-color: white; + border: 3px solid black; + font-size: 25px; + font-weight: bold; + font-family: Ensures; + color: Red; + margin: 15px 1px; + box-sizing: border-box; + /* Ensures that the border width is included in the total width and height */ +} + +.active { + background: linear-gradient(to top, rgb(22 2, 22, 22), white); +} + +#rand { + font-style: italic; +} + +button { + padding: 7px 14px; + color: Red; + background-color: black; + font-family: Tahoma; + font-size: 12 px; +} + +.recording { + font-style: italic; + font-weight: 900; +} \ No newline at end of file diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 000000000..3e3831f30 --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,244 @@ + +const notesFreq = new Map([ + ['C4', 261.625], + ['D4', 293.665], + ['E4', 329.628], + ['F4', 349.228], + ['G4', 391.995], + ['A4', 440], + ['B4', 493.883], + ['C5', 523.251], + ['D5', 587.33], + ['E5', 659.25], + ['F5', 698.46], + ['G5', 783.99], + ['A5', 880.00], + ['B5', 987.77], + ['C6', 1046.50], + ['D6', 1174.66], + ['E6', 1318.51], + ['F6', 1396.91], + ['G6', 1567.98], + ['A6', 1760.00], + ['B6', 1975.53], + ['C7', 2093.00] +]); + + + +const container = document.querySelector('#container'); + +// Using forEach +notesFreq.forEach((value, key) => { + const pianoKey = document.createElement('div'); + pianoKey.id = key; + pianoKey.classList.add('piano-tile'); + pianoKey.innerText = key; + container.appendChild(pianoKey); +}); + + + +const pianoTiles = document.querySelectorAll('.piano-tile'); +// Function to start playing the note and change CSS + +const activeOscillators = new Map(); + +function createOscillatorAndGainNode(pitch) { + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + // Define the harmonic content for the custom waveform + const numberOfHarmonics = 4; + const real = new Float32Array(numberOfHarmonics); + const imag = new Float32Array(numberOfHarmonics); + + // DC offset and fundamental tone + real[0] = 0; imag[0] = 0; // DC offset, not used for audio signal + real[1] = 1; imag[1] = 0; // Fundamental tone + + // Harmonics + real[2] = 0.8; imag[2] = 0; // First harmonic + real[3] = 0.6; imag[3] = 0; // Second harmonic + + // Create the custom periodic waveform based on the defined harmonics + const customWave = audioContext.createPeriodicWave(real, imag); + oscillator.setPeriodicWave(customWave); + + oscillator.frequency.setValueAtTime(pitch, audioContext.currentTime); + + // ADSR Envelope + gainNode.gain.setValueAtTime(0, audioContext.currentTime); // Start with no volume + const attackTime = 0.02; // Attack + gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + attackTime); // Ramp to full volume + + const decayTime = 0.1; // Decay + const sustainLevel = 0.65; // Sustain level + gainNode.gain.linearRampToValueAtTime(sustainLevel, audioContext.currentTime + attackTime + decayTime); // Ramp to sustain level + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + return { oscillator, gainNode }; +} + + +function startNote() { + const note = this.id; + const pitch = notesFreq.get(note); + this.classList.add('active'); + + + // Stop any existing note for this key + if (activeOscillators.has(note)) { + const existing = activeOscillators.get(note); + existing.oscillator.stop(); + existing.oscillator.disconnect(); + existing.gainNode.disconnect(); + } + + const { oscillator, gainNode } = createOscillatorAndGainNode(pitch); + oscillator.start(); + const noteEventId = Date.now(); + activeOscillators.set(note, { oscillator, gainNode, noteEventId }); +} + +function stopNote() { + const note = this.id; + this.classList.remove('active'); + const releaseTime = audioContext.currentTime; + if (!activeOscillators.has(note)) { + return; + } + if (activeOscillators.has(note)) { + const { oscillator, gainNode, noteEventId } = activeOscillators.get(note); + const decayDuration = 2; + gainNode.gain.cancelScheduledValues(releaseTime); + gainNode.gain.setValueAtTime(gainNode.gain.value, releaseTime); // New line to set current gain + gainNode.gain.exponentialRampToValueAtTime(0.001, releaseTime + decayDuration); + setTimeout(() => { + // Check if the current note event is still the one that should be stopped + if (activeOscillators.has(note) && activeOscillators.get(note).noteEventId === noteEventId) { + oscillator.stop(); + oscillator.disconnect(); + gainNode.disconnect(); + activeOscillators.delete(note); + } + }, decayDuration * 1000); + } +} + + +// PC touch event handler +let isMouseDown = false; + +document.addEventListener('pointerdown', function (event) { + if (event.buttons === 1) { // Check if left mouse button + isMouseDown = true; + } +}, false); +document.addEventListener('pointerup', function () { + isMouseDown = false; + // Optionally, stop the note here if you want notes to stop when the mouse is released +}, false); + +for (const tile of pianoTiles) { + tile.addEventListener('pointerdown', startNote); + tile.addEventListener('pointerup', stopNote); + tile.addEventListener('pointerover', function (event) { + if (isMouseDown) { + startNote.call(this, event); // Play note if mouse is down and pointer moves over a tile + } + }); + tile.addEventListener('pointerleave', stopNote); +} + + +// Create a new audio context +const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + +// Variable of global scope initialization. +const dataOfClicks = []; +let startTime; +let dataLi; +let recordedTime; + +function timeNkey(tile, dataLi) { + const time = Date.now() - startTime; + const key = tile.id; + const json = { + 'time': time, + 'key': key + } + dataOfClicks.push(json); +} + +function record() { + recording = true; + recBtn.classList.add('recording'); + recBtn.innerText = 'Recording...'; + dataLi = document.createElement('li'); + startTime = Date.now(); + dataLi.id = startTime; + document.querySelector('ul').appendChild(dataLi); + showTimeOfRecording(startTime, dataLi); + for (const tile of pianoTiles) { + tile.addEventListener('pointerdown', () => timeNkey(tile, dataLi)); + tile.addEventListener('pointerup', () => timeNkey(tile, dataLi)); + } + recBtn.addEventListener("click", () => stopRecording(dataLi), { once: true }); +} + +function preview() { + const novLi = document.createElement('li'); + document.querySelector('ul').appendChild(novLi); + novLi.innerText = JSON.stringify(dataOfClicks, null, 1); +} + +const recBtn = document.querySelector("#rec"); +recBtn.addEventListener("click", record, { once: true }); + +const jsonBtn = document.querySelector("#jsonPrev"); +jsonBtn.addEventListener("click", preview); + +let recording = false; + +function showTimeOfRecording(time, dataLi) { + if (recording) { + const newTime = Date.now(); + let realTime = newTime - time; + recordedTime = realTime; + dataLi.innerText = `Recording for ${(realTime / 1000).toFixed(1)} seconds.`; + setTimeout(() => showTimeOfRecording(time, dataLi), 223); + } +} + +function stopRecording() { + recBtn.addEventListener("click", record, { once: true }); + recording = false; + recBtn.classList.remove('recording'); + recBtn.innerText = 'Record'; + dataLi.innerText = `Recording saved.Duration - ${(recordedTime / 1000).toFixed(2)} seconds.` +} + +function onSaveClick() { + fetch("http://localhost:5000/saveRecording", { + method: "post", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + //tuki pride json + body: JSON.stringify([{ + time: 1, + key: "F" + }, + { + time: 12, + key: "G" + }]) + }) + .then((response) => { + console.log(response) + }); +} \ No newline at end of file diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 000000000..dd24dc158 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,32 @@ + + + + + + + Klaviatura + + + + + +

Piano Virtuosso~

+
+ + +

Database storage

+
+
    +
  • Test
  • +
  • Time: 11.3 seconds.
  • +
+
+
+ +
+ + + + + + \ No newline at end of file From 6da8de72cc5e689e5254b0750606ecb32e67621a Mon Sep 17 00:00:00 2001 From: Bob Date: Sat, 10 Feb 2024 18:18:03 +0100 Subject: [PATCH 06/12] Update the hello world response text --- .idea/misc.xml | 7 ++++++ app.py | 57 +++++++++++++++------------------------------ templates/home.html | 2 +- 3 files changed, 27 insertions(+), 39 deletions(-) create mode 100644 .idea/misc.xml diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..b742dd436 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app.py b/app.py index 4e7b6d76e..f5c148b92 100644 --- a/app.py +++ b/app.py @@ -7,50 +7,31 @@ app = Flask(__name__) CORS(app) -lessons = [ - {"id": 1, "name": "Python programing", "credits": 5}, - {"id": 2, "name": "Java programing", "credits": 4}, - {"id": 3, "name": "TypeScript programing", "credits": 3}, - {"id": 4, "name": "C++ programing", "credits": 1} -] - @app.route('/') def hello_world(): - return 'Hello, World! Local testing1243' - -@app.route('/lessons-jinja') -def get_lessons_jinja(): - return render_template("home.html", les=lessons) - -@app.route('/lessons-javascript') -def get_lessons_javascript(): - return jsonify(lessons) - -@app.route('/home-javascript') -def load_home_js(): - return render_template("home-js.html") + return render_template("home.html", my_rec=my_recordings) if __name__ == "__main__": app.run() - -@app.route('/create-lesson') -def create_lesson(): - name = request.args["name"] - credits = request.args["credits"] - id = len(lessons) + 1 - newLesson = {"id": id, "name": name, "credits": credits} - lessons.append(newLesson) - return jsonify(lessons) - -@app.route('/delete/', methods=["GET"]) -def delete_lesson(lesson_id): - for lesson in lessons: - if lesson["id"] == int(lesson_id): - lessons.remove(lesson) - # return jsonify(lessons) - return redirect("/lessons-jinja") - +# +# @app.route('/create-lesson') +# def create_lesson(): +# name = request.args["name"] +# credits = request.args["credits"] +# id = len(lessons) + 1 +# newLesson = {"id": id, "name": name, "credits": credits} +# lessons.append(newLesson) +# return jsonify(lessons) +# +# @app.route('/delete/', methods=["GET"]) +# def delete_lesson(lesson_id): +# for lesson in lessons: +# if lesson["id"] == int(lesson_id): +# lessons.remove(lesson) +# # return jsonify(lessons) +# return redirect("/lessons-jinja") +# my_recordings = {} diff --git a/templates/home.html b/templates/home.html index dd24dc158..0cf5ff882 100644 --- a/templates/home.html +++ b/templates/home.html @@ -17,7 +17,7 @@

Piano Virtuosso~

Database storage

    -
  • Test
  • +
  • {{ my_rec }}
  • Time: 11.3 seconds.
From 79acc0dc5b8a0261ed4c64cbc6947b37fe72fa0b Mon Sep 17 00:00:00 2001 From: Bob Date: Sat, 10 Feb 2024 22:33:16 +0100 Subject: [PATCH 07/12] Update the hello world response text --- app.py | 82 ++++++++++++++++++++++++++++----------------- requirements.txt | 4 ++- static/js/script.js | 47 ++++++++++++++++---------- 3 files changed, 85 insertions(+), 48 deletions(-) diff --git a/app.py b/app.py index f5c148b92..623251207 100644 --- a/app.py +++ b/app.py @@ -1,46 +1,68 @@ -from flask import Flask, render_template, redirect -from flask import jsonify -from flask import request -import tkinter as tk +from flask import Flask, render_template, redirect, jsonify, request +import json from flask_cors import CORS -import random +from flask_sqlalchemy import SQLAlchemy + app = Flask(__name__) CORS(app) +db = SQLAlchemy() + @app.route('/') def hello_world(): - return render_template("home.html", my_rec=my_recordings) + return render_template("home.html", my_rec=recordings) if __name__ == "__main__": + app.config['SQLALCHEMY_DATABASE_URI'] = 'postgres://robert:LTrCCDwSsyrNhFKffv5TewRi1TQXX9Hs@dpg-cn3qur5jm4es73bmkga0-a.frankfurt-postgres.render.com/recordingsdatabase' + db.init_app(app) + with app.app_context(): + db.create_all() app.run() -# -# @app.route('/create-lesson') -# def create_lesson(): -# name = request.args["name"] -# credits = request.args["credits"] -# id = len(lessons) + 1 -# newLesson = {"id": id, "name": name, "credits": credits} -# lessons.append(newLesson) -# return jsonify(lessons) -# -# @app.route('/delete/', methods=["GET"]) -# def delete_lesson(lesson_id): -# for lesson in lessons: -# if lesson["id"] == int(lesson_id): -# lessons.remove(lesson) -# # return jsonify(lessons) -# return redirect("/lessons-jinja") -# - -my_recordings = {} + + +# SAVE, DELETE, SHOW + +# Database integration + +class Recording(db.Model): + id = db.Column('id', db.Integer, primary_key=True) + name = db.Column(db.String(50)) + data = db.Column(db.Text) # This stores JSON data as text + + def serialize(self): + return { + "id": self.id, + "name": self.name, + "data": self.data + } + +def jsonify_recordings(recordings): + result = [] + for recording in recordings: + result.append({ + "id": recording.id, + "name": recording.name, + # "description": recording.description + }) + return result + +# My recordings JS to Flask RESTful API +recordings = {} + @app.route('/saveRecording', methods=["POST"]) def saveRecording(): - data = request.get_json() - print(data) - return {"status" : "OK"} + data = request.get_json() # This is your dataOfClicks from the frontend + if not data: + return jsonify({"error": "No data provided"}), 400 + # Assuming you want to use the name as a unique identifier for now, but you could modify this + name = data.get('name', 'Unnamed Recording') + recording_data = json.dumps(data.get('clicks', [])) # Convert the clicks list to a JSON string + new_recording = Recording(name=name, data=recording_data) + db.session.add(new_recording) + db.session.commit() -# SAVE, DELETE, SHOW \ No newline at end of file + return jsonify({"status": "OK", "id": new_recording.id}) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ac272f07a..63e77d32a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ Flask Gunicorn -flask_cors \ No newline at end of file +flask_cors +flask-sqlalchemy +psycopg2 \ No newline at end of file diff --git a/static/js/script.js b/static/js/script.js index 3e3831f30..730416d61 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -158,12 +158,12 @@ for (const tile of pianoTiles) { const audioContext = new (window.AudioContext || window.webkitAudioContext)(); // Variable of global scope initialization. -const dataOfClicks = []; +let dataOfClicks = []; let startTime; let dataLi; let recordedTime; -function timeNkey(tile, dataLi) { +function timeNkey(tile) { const time = Date.now() - startTime; const key = tile.id; const json = { @@ -222,23 +222,36 @@ function stopRecording() { } function onSaveClick() { - fetch("http://localhost:5000/saveRecording", { - method: "post", + // Assuming dataOfClicks is already populated with your clicks data in the correct format + // First, create an object with the name and clicks keys + const recordingData = { + "name": "My Recording", // Set the recording name as desired + "clicks": dataOfClicks // Your existing clicks data + }; + + // Then, proceed with the fetch request, sending recordingData as the body + fetch("https://onkrajreda.onrender.com/saveRecording", { + method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, - //tuki pride json - body: JSON.stringify([{ - time: 1, - key: "F" - }, - { - time: 12, - key: "G" - }]) + body: JSON.stringify(recordingData) // Convert the recordingData object into a JSON string + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); // Parse JSON response body + }) + .then(data => { + console.log(data); // Handle success }) - .then((response) => { - console.log(response) - }); -} \ No newline at end of file + .catch(error => { + console.error('Error:', error); // Handle errors, such as network issues + }); + + // Optionally, clear dataOfClicks if you don't need it anymore after sending + dataOfClicks = []; +} + From 6d038b885ed3d0920128e91952e91fcbcd3a7602 Mon Sep 17 00:00:00 2001 From: Bob Date: Sun, 11 Feb 2024 00:47:24 +0100 Subject: [PATCH 08/12] Update the hello world response text --- app.py | 49 ++++++++++++++++++++++++++++----------------- static/js/script.js | 2 ++ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/app.py b/app.py index 623251207..652ba41af 100644 --- a/app.py +++ b/app.py @@ -6,36 +6,50 @@ app = Flask(__name__) CORS(app) -db = SQLAlchemy() +print("start connection to db") +app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://robert:LTrCCDwSsyrNhFKffv5TewRi1TQXX9Hs@dpg-cn3qur5jm4es73bmkga0-a.frankfurt-postgres.render.com/recordingsdatabase' +app.config['SQLALCHEMY_ECHO'] = True +db = SQLAlchemy(app) +print("connected to db") + +class Recording(db.Model): + id = db.Column('id', db.Integer, primary_key=True) + name = db.Column(db.String(50)) + data = db.Column(db.Text) # This stores JSON data as text + + def serialize(self): + return { + "id": self.id, + "name": self.name, + "data": self.data + } + +with app.app_context(): + print("Creating database tables...") + db.create_all() + print("Tables created.") + +# My recordings JS to Flask RESTful API +recordings = {} + @app.route('/') def hello_world(): return render_template("home.html", my_rec=recordings) + if __name__ == "__main__": - app.config['SQLALCHEMY_DATABASE_URI'] = 'postgres://robert:LTrCCDwSsyrNhFKffv5TewRi1TQXX9Hs@dpg-cn3qur5jm4es73bmkga0-a.frankfurt-postgres.render.com/recordingsdatabase' - db.init_app(app) + print("Starting application...") with app.app_context(): db.create_all() - app.run() - + app.run(debug=True) # SAVE, DELETE, SHOW # Database integration -class Recording(db.Model): - id = db.Column('id', db.Integer, primary_key=True) - name = db.Column(db.String(50)) - data = db.Column(db.Text) # This stores JSON data as text - def serialize(self): - return { - "id": self.id, - "name": self.name, - "data": self.data - } def jsonify_recordings(recordings): result = [] @@ -43,17 +57,16 @@ def jsonify_recordings(recordings): result.append({ "id": recording.id, "name": recording.name, - # "description": recording.description + "data": recording.data }) return result -# My recordings JS to Flask RESTful API -recordings = {} @app.route('/saveRecording', methods=["POST"]) def saveRecording(): data = request.get_json() # This is your dataOfClicks from the frontend + print(data) if not data: return jsonify({"error": "No data provided"}), 400 diff --git a/static/js/script.js b/static/js/script.js index 730416d61..420d341b4 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -224,10 +224,12 @@ function stopRecording() { function onSaveClick() { // Assuming dataOfClicks is already populated with your clicks data in the correct format // First, create an object with the name and clicks keys + console.log(dataOfClicks); const recordingData = { "name": "My Recording", // Set the recording name as desired "clicks": dataOfClicks // Your existing clicks data }; + console.log(recordingData); // Then, proceed with the fetch request, sending recordingData as the body fetch("https://onkrajreda.onrender.com/saveRecording", { From 7694d73f4d53e42ab8a2338372e74a15705898f8 Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 13 Feb 2024 21:37:46 +0100 Subject: [PATCH 09/12] Update the hello world response text --- app.py | 36 ++++++++- static/css/style.css | 62 +++++++++++++++- static/js/script.js | 173 +++++++++++++++++++++++++++++++++++++++---- templates/home.html | 50 ++++++++++--- 4 files changed, 290 insertions(+), 31 deletions(-) diff --git a/app.py b/app.py index 652ba41af..c75c5693b 100644 --- a/app.py +++ b/app.py @@ -26,6 +26,7 @@ def serialize(self): with app.app_context(): print("Creating database tables...") + # db.drop_all() delete all tables for dev only db.create_all() print("Tables created.") @@ -78,4 +79,37 @@ def saveRecording(): db.session.add(new_recording) db.session.commit() - return jsonify({"status": "OK", "id": new_recording.id}) \ No newline at end of file + return jsonify({"status": "OK", "id": new_recording.id}) + +@app.route('/list-recordings', methods=['GET']) +def list_recordings(): + recordings = Recording.query.order_by(Recording.id).all() # Fetch all recordings from the database, + sort recordings by ID + return jsonify([recording.serialize() for recording in recordings]) + +@app.route('/rename-recording', methods=['POST']) +def rename_recording(): + data = request.get_json() + if not data or 'id' not in data or 'newName' not in data: + return jsonify({"error": "Invalid request"}), 400 + + recording = Recording.query.get(data['id']) + if recording: + recording.name = data['newName'] + db.session.commit() + return jsonify({"success": True, "id": recording.id, "newName": recording.name}) + else: + return jsonify({"error": "Recording not found"}), 404 + +@app.route('/delete-recording', methods=['POST']) +def delete_recording(): + data = request.get_json() + if not data or 'id' not in data: + return jsonify({"error": "Invalid request"}), 400 + + recording = Recording.query.get(data['id']) + if recording: + db.session.delete(recording) + db.session.commit() + return jsonify({"success": True, "id": data['id']}) + else: + return jsonify({"error": "Recording not found"}), 404 diff --git a/static/css/style.css b/static/css/style.css index ed7d142f2..7b537e81f 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,3 +1,7 @@ +html, body { + overflow-x: hidden; +} + body { width: 100%; padding-left: 1%; @@ -7,10 +11,14 @@ body { } h1 { - margin-bottom: 39px; + font-family: Georgia, 'Times New Roman', Times, serif; + margin-bottom: 70px; + font-size: 4em; + margin-top: 20px; } #container { + width: 100%; display: flex; justify-content: center; align-items: center; @@ -41,7 +49,7 @@ h1 { } .active { - background: linear-gradient(to top, rgb(22 2, 22, 22), white); + background: linear-gradient(to top, rgb(199, 29, 29), white); } #rand { @@ -53,10 +61,56 @@ button { color: Red; background-color: black; font-family: Tahoma; - font-size: 12 px; + font-size: 16px; } .recording { font-style: italic; font-weight: 900; -} \ No newline at end of file +} + +#rec { + margin-right: 10px; + +} + +li { + white-space: nowrap; +} + +#database { + margin-top: 30px; +} +/* Custom table styling */ +#recordingsTable { + border-collapse: collapse; + width: 100%; + box-shadow: 0 4px 8px rgba(0,0,0,0.4); /* Soft shadow around the table */ + border-radius: 1rem; /* Rounded corners */ +} + +/* Header styling */ +#recordingsTable thead th { + background-color: #007bff; /* Bootstrap primary color */ + color: white; +} + +/* Button styling within the table */ +#recordingsTable button { + border: none; + border-radius: 0.25rem; + padding: 5px 10px; + color: white; + cursor: pointer; /* Hand icon on hover */ +} + +/* Specific button colors */ +.play-btn { background-color: #28a745; } /* Bootstrap success color */ +.rename-btn { background-color: #17a2b8; } /* Bootstrap info color */ +.delete-btn { background-color: #dc3545; } /* Bootstrap danger color */ + +/* Hover effect for buttons */ +#recordingsTable button:hover { + background-color: rgb(0,40,40); + color: red; +} diff --git a/static/js/script.js b/static/js/script.js index 420d341b4..c98b00b6c 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -162,6 +162,7 @@ let dataOfClicks = []; let startTime; let dataLi; let recordedTime; +let recordingName; function timeNkey(tile) { const time = Date.now() - startTime; @@ -177,29 +178,26 @@ function record() { recording = true; recBtn.classList.add('recording'); recBtn.innerText = 'Recording...'; - dataLi = document.createElement('li'); + dataLi = document.querySelector('#dataCont li'); startTime = Date.now(); dataLi.id = startTime; - document.querySelector('ul').appendChild(dataLi); showTimeOfRecording(startTime, dataLi); for (const tile of pianoTiles) { - tile.addEventListener('pointerdown', () => timeNkey(tile, dataLi)); - tile.addEventListener('pointerup', () => timeNkey(tile, dataLi)); + tile.addEventListener('pointerdown', () => timeNkey(tile)); + tile.addEventListener('pointerup', () => timeNkey(tile)); } recBtn.addEventListener("click", () => stopRecording(dataLi), { once: true }); } function preview() { - const novLi = document.createElement('li'); - document.querySelector('ul').appendChild(novLi); - novLi.innerText = JSON.stringify(dataOfClicks, null, 1); + console.log(JSON.stringify(dataOfClicks, null, 1)); } const recBtn = document.querySelector("#rec"); recBtn.addEventListener("click", record, { once: true }); const jsonBtn = document.querySelector("#jsonPrev"); -jsonBtn.addEventListener("click", preview); +/*jsonBtn.addEventListener("click", preview);*/ let recording = false; @@ -214,20 +212,28 @@ function showTimeOfRecording(time, dataLi) { } function stopRecording() { + recordingName = prompt("Please enter a name for the recording:", "enter name"); + + if (recordingName !== null && recordingName !== "") { + console.log("Recording name saved:", recordingName); + } else { + recordingName = "My Recording"; + } + + saveData(); recBtn.addEventListener("click", record, { once: true }); recording = false; recBtn.classList.remove('recording'); recBtn.innerText = 'Record'; - dataLi.innerText = `Recording saved.Duration - ${(recordedTime / 1000).toFixed(2)} seconds.` + dataLi.innerText = `Recording saved. Duration - ${(recordedTime / 1000).toFixed(2)} seconds.` } -function onSaveClick() { - // Assuming dataOfClicks is already populated with your clicks data in the correct format +function saveData() { // First, create an object with the name and clicks keys console.log(dataOfClicks); const recordingData = { - "name": "My Recording", // Set the recording name as desired - "clicks": dataOfClicks // Your existing clicks data + "name": recordingName, // Set the recording name as desired + "clicks": dataOfClicks // data }; console.log(recordingData); @@ -252,8 +258,145 @@ function onSaveClick() { .catch(error => { console.error('Error:', error); // Handle errors, such as network issues }); - - // Optionally, clear dataOfClicks if you don't need it anymore after sending + fetchRecordings(); + // clear dataOfClicks dataOfClicks = []; } +function fetchRecordings() { + fetch("https://onkrajreda.onrender.com/list-recordings") + .then(response => response.json()) + .then(data => { + const tbody = document.getElementById('recordingsTable').getElementsByTagName('tbody')[0]; + tbody.innerHTML = ''; // Clear existing rows + data.forEach(recording => { + const row = tbody.insertRow(); + row.insertCell().textContent = recording.id; + row.insertCell().textContent = recording.name; + const playCell = row.insertCell(); + const playButton = document.createElement('button'); + playButton.textContent = 'Play'; + playButton.onclick = () => playRecording(recording.id, recording.data); + playCell.appendChild(playButton); + + const renameCell = row.insertCell(); + const renameButton = document.createElement('button'); + renameButton.textContent = 'Rename'; + renameButton.onclick = () => renameRecording(recording.id); + renameCell.appendChild(renameButton); + + const deleteCell = row.insertCell(); + const deleteButton = document.createElement('button'); + deleteButton.textContent = 'Delete'; + deleteButton.onclick = () => deleteRecording(recording.id); + deleteCell.appendChild(deleteButton); + }); + }); +} + +function renameRecording(id) { + const newName = prompt('Enter new name:'); + if (newName) { + fetch('https://onkrajreda.onrender.com/rename-recording', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ id, newName }), + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + fetchRecordings(); // Refresh the table + } else { + alert('Rename failed'); + } + }); + } +} + +function deleteRecording(id) { + if (confirm('Are you sure you want to delete this recording?')) { + fetch('https://onkrajreda.onrender.com/delete-recording', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ id }), + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + fetchRecordings(); // Refresh the table + } else { + alert('Delete failed'); + } + }); + } +} + + +function startNoteAutoplay(key) { + const note = key; + const pitch = notesFreq.get(note); + const tile = document.querySelector(`#${key}`); + tile.classList.add('active'); + + // Stop any existing note for this key + if (activeOscillators.has(note)) { + const existing = activeOscillators.get(note); + existing.oscillator.stop(); + existing.oscillator.disconnect(); + existing.gainNode.disconnect(); + } + + const { oscillator, gainNode } = createOscillatorAndGainNode(pitch); + oscillator.start(); + const noteEventId = Date.now(); + activeOscillators.set(note, { oscillator, gainNode, noteEventId }); +} + +function stopNoteAutoplay(key) { + const note = key; + const tile = document.querySelector(`#${key}`); + tile.classList.remove('active'); + const releaseTime = audioContext.currentTime; + const { oscillator, gainNode, noteEventId } = activeOscillators.get(note); + const decayDuration = 2; + gainNode.gain.cancelScheduledValues(releaseTime); + gainNode.gain.setValueAtTime(gainNode.gain.value, releaseTime); // New line to set current gain + gainNode.gain.exponentialRampToValueAtTime(0.001, releaseTime + decayDuration); + setTimeout(() => { + // Check if the current note event is still the one that should be stopped + if (activeOscillators.has(note) && activeOscillators.get(note).noteEventId === noteEventId) { + oscillator.stop(); + oscillator.disconnect(); + gainNode.disconnect(); + activeOscillators.delete(note); + } + }, decayDuration * 1000); +} + + /*[{"time": 878, "key": "E4"}, {"time": 984, "key": "E4"}, {"time": 1516, "key": "E4"}, {"time": 1609, "key": "E4"}, + {"time": 2179, "key": "F4"}, {"time": 2279, "key": "F4"}, {"time": 2765, "key": "F4"}, {"time": 2869, "key": "F4"}, + {"time": 3405, "key": "A4"}, {"time": 3479, "key": "A4"}, {"time": 4019, "key": "A4"}, {"time": 4085, "key": "A4"}, + {"time": 4656, "key": "G4"}, {"time": 4735, "key": "G4"}] + */ + + +function playRecording(id, jsonData) { + console.log('Playing recording:', id, 'data: ', jsonData); + const data = JSON.parse(jsonData); + data.forEach(({time, key}) => { + console.log(`key - ${key}, time - ${time}ms`); + setTimeout(function () { + if(!activeOscillators.has(key)) { + startNoteAutoplay(key); // Start the note if not already playing + } else { + stopNoteAutoplay(key); // Stop the note if it is playing + } + }, time); + }); +} + +fetchRecordings(); \ No newline at end of file diff --git a/templates/home.html b/templates/home.html index 0cf5ff882..5099a3e96 100644 --- a/templates/home.html +++ b/templates/home.html @@ -5,27 +5,55 @@ Klaviatura + -

Piano Virtuosso~

+

Piano Virtuosso~


- - -

Database storage

-
-
    -
  • {{ my_rec }}
  • -
  • Time: 11.3 seconds.
  • -
+
+
+
+ +
+ +
    +
  • +
+
+
+
- - + +
+
+
+ Database connection + + + + + + + + + + + + + +
IDNamePlayRenameDelete
+
+
+
From 1533b182a51692360b26bf9c116814b05ca628d9 Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 13 Feb 2024 22:02:46 +0100 Subject: [PATCH 10/12] Update the hello world response text --- static/js/script.js | 95 ++++++++++++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 32 deletions(-) diff --git a/static/js/script.js b/static/js/script.js index c98b00b6c..1ba310b49 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1,4 +1,5 @@ - +// Map of note frequencies, associating musical notes with their frequencies +// for sound generation. const notesFreq = new Map([ ['C4', 261.625], ['D4', 293.665], @@ -25,10 +26,8 @@ const notesFreq = new Map([ ]); - +// Initialize piano keys on the webpage dynamically based on the notesFreq map. const container = document.querySelector('#container'); - -// Using forEach notesFreq.forEach((value, key) => { const pianoKey = document.createElement('div'); pianoKey.id = key; @@ -38,16 +37,15 @@ notesFreq.forEach((value, key) => { }); - +// Select all dynamically created piano keys for interaction. const pianoTiles = document.querySelectorAll('.piano-tile'); -// Function to start playing the note and change CSS - +// Active oscillators map to keep track of currently playing notes. const activeOscillators = new Map(); - +// Function to create an oscillator and gain node for playing a note. function createOscillatorAndGainNode(pitch) { const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); - + // Custom waveform for a more natural piano sound. // Define the harmonic content for the custom waveform const numberOfHarmonics = 4; const real = new Float32Array(numberOfHarmonics); @@ -64,10 +62,10 @@ function createOscillatorAndGainNode(pitch) { // Create the custom periodic waveform based on the defined harmonics const customWave = audioContext.createPeriodicWave(real, imag); oscillator.setPeriodicWave(customWave); - + // Set pitch. oscillator.frequency.setValueAtTime(pitch, audioContext.currentTime); - // ADSR Envelope + // ADSR Envelope for realistic note attack and decay. gainNode.gain.setValueAtTime(0, audioContext.currentTime); // Start with no volume const attackTime = 0.02; // Attack gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + attackTime); // Ramp to full volume @@ -83,35 +81,44 @@ function createOscillatorAndGainNode(pitch) { } +// Start playing a note. function startNote() { + // Note is the ID of the clicked/touched piano key. const note = this.id; + // Retrieve frequency from map. const pitch = notesFreq.get(note); + // Visual feedback for key press. this.classList.add('active'); - // Stop any existing note for this key + // Stop any previously playing oscillator for this note. if (activeOscillators.has(note)) { const existing = activeOscillators.get(note); existing.oscillator.stop(); existing.oscillator.disconnect(); existing.gainNode.disconnect(); } - + // Create and start a new oscillator for this note. const { oscillator, gainNode } = createOscillatorAndGainNode(pitch); oscillator.start(); const noteEventId = Date.now(); activeOscillators.set(note, { oscillator, gainNode, noteEventId }); } + +// Stop playing a note. function stopNote() { const note = this.id; this.classList.remove('active'); const releaseTime = audioContext.currentTime; + // Exit if no oscillator is playing this note. if (!activeOscillators.has(note)) { return; } + // Retrieve and stop the oscillator for this note. if (activeOscillators.has(note)) { const { oscillator, gainNode, noteEventId } = activeOscillators.get(note); + // Time for the note to fade out. const decayDuration = 2; gainNode.gain.cancelScheduledValues(releaseTime); gainNode.gain.setValueAtTime(gainNode.gain.value, releaseTime); // New line to set current gain @@ -131,7 +138,6 @@ function stopNote() { // PC touch event handler let isMouseDown = false; - document.addEventListener('pointerdown', function (event) { if (event.buttons === 1) { // Check if left mouse button isMouseDown = true; @@ -139,9 +145,9 @@ document.addEventListener('pointerdown', function (event) { }, false); document.addEventListener('pointerup', function () { isMouseDown = false; - // Optionally, stop the note here if you want notes to stop when the mouse is released }, false); +// Setup to handle user interactions with piano keys. for (const tile of pianoTiles) { tile.addEventListener('pointerdown', startNote); tile.addEventListener('pointerup', stopNote); @@ -154,7 +160,7 @@ for (const tile of pianoTiles) { } -// Create a new audio context +// Initialize the audio context used for playing notes. const audioContext = new (window.AudioContext || window.webkitAudioContext)(); // Variable of global scope initialization. @@ -164,6 +170,8 @@ let dataLi; let recordedTime; let recordingName; + +// Function to capture the time and key when a note is played or released. function timeNkey(tile) { const time = Date.now() - startTime; const key = tile.id; @@ -174,6 +182,8 @@ function timeNkey(tile) { dataOfClicks.push(json); } + +// Starts recording user input. function record() { recording = true; recBtn.classList.add('recording'); @@ -181,7 +191,9 @@ function record() { dataLi = document.querySelector('#dataCont li'); startTime = Date.now(); dataLi.id = startTime; + // Continuously update the display with the recording duration. showTimeOfRecording(startTime, dataLi); + // Add event listeners to all piano tiles for capturing clicks. for (const tile of pianoTiles) { tile.addEventListener('pointerdown', () => timeNkey(tile)); tile.addEventListener('pointerup', () => timeNkey(tile)); @@ -189,10 +201,14 @@ function record() { recBtn.addEventListener("click", () => stopRecording(dataLi), { once: true }); } +// Function to display a preview of the recorded data in the console. DEV only function preview() { console.log(JSON.stringify(dataOfClicks, null, 1)); } + + +// Initialization of recording controls. const recBtn = document.querySelector("#rec"); recBtn.addEventListener("click", record, { once: true }); @@ -201,6 +217,9 @@ const jsonBtn = document.querySelector("#jsonPrev"); let recording = false; + + +// Updates the display with the current recording duration. function showTimeOfRecording(time, dataLi) { if (recording) { const newTime = Date.now(); @@ -211,6 +230,7 @@ function showTimeOfRecording(time, dataLi) { } } +// Stops the recording process. function stopRecording() { recordingName = prompt("Please enter a name for the recording:", "enter name"); @@ -228,9 +248,10 @@ function stopRecording() { dataLi.innerText = `Recording saved. Duration - ${(recordedTime / 1000).toFixed(2)} seconds.` } + +// Saves the recorded data to the server. function saveData() { - // First, create an object with the name and clicks keys - console.log(dataOfClicks); + // Package the recording name and data. const recordingData = { "name": recordingName, // Set the recording name as desired "clicks": dataOfClicks // data @@ -259,32 +280,41 @@ function saveData() { console.error('Error:', error); // Handle errors, such as network issues }); fetchRecordings(); - // clear dataOfClicks + // clear dataOfClicks for next use dataOfClicks = []; } + + +// Fetches the list of recordings from the server and updates the UI to display them. function fetchRecordings() { fetch("https://onkrajreda.onrender.com/list-recordings") - .then(response => response.json()) + .then(response => response.json()) // Parse the JSON response. .then(data => { const tbody = document.getElementById('recordingsTable').getElementsByTagName('tbody')[0]; - tbody.innerHTML = ''; // Clear existing rows + tbody.innerHTML = ''; // Clear the table body to ensure fresh display of recordings. + // Iterate through each recording received from the server. data.forEach(recording => { + // Create a new row for each recording with its ID, name, and action buttons. const row = tbody.insertRow(); row.insertCell().textContent = recording.id; row.insertCell().textContent = recording.name; + + // Create and append a 'Play' button for each recording. const playCell = row.insertCell(); const playButton = document.createElement('button'); playButton.textContent = 'Play'; playButton.onclick = () => playRecording(recording.id, recording.data); playCell.appendChild(playButton); + // Create and append a 'Rename' button for each recording. const renameCell = row.insertCell(); const renameButton = document.createElement('button'); renameButton.textContent = 'Rename'; renameButton.onclick = () => renameRecording(recording.id); renameCell.appendChild(renameButton); + // Create and append a 'Delete' button for each recording. const deleteCell = row.insertCell(); const deleteButton = document.createElement('button'); deleteButton.textContent = 'Delete'; @@ -294,6 +324,8 @@ function fetchRecordings() { }); } + +// Prompts the user for a new name and sends a request to the server to rename a recording. function renameRecording(id) { const newName = prompt('Enter new name:'); if (newName) { @@ -302,7 +334,7 @@ function renameRecording(id) { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ id, newName }), + body: JSON.stringify({ id, newName }), // Send the new name along with the recording ID. }) .then(response => response.json()) .then(data => { @@ -315,6 +347,7 @@ function renameRecording(id) { } } +// Confirms with the user before sending a request to the server to delete a recording. function deleteRecording(id) { if (confirm('Are you sure you want to delete this recording?')) { fetch('https://onkrajreda.onrender.com/delete-recording', { @@ -322,7 +355,7 @@ function deleteRecording(id) { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ id }), + body: JSON.stringify({ id }), // Send the recording ID to be deleted. }) .then(response => response.json()) .then(data => { @@ -336,6 +369,7 @@ function deleteRecording(id) { } +// Same as startNote but modified for automatic replay function startNoteAutoplay(key) { const note = key; const pitch = notesFreq.get(note); @@ -356,6 +390,7 @@ function startNoteAutoplay(key) { activeOscillators.set(note, { oscillator, gainNode, noteEventId }); } +// Same as stopNote but modified for automatic replay function stopNoteAutoplay(key) { const note = key; const tile = document.querySelector(`#${key}`); @@ -377,17 +412,12 @@ function stopNoteAutoplay(key) { }, decayDuration * 1000); } - /*[{"time": 878, "key": "E4"}, {"time": 984, "key": "E4"}, {"time": 1516, "key": "E4"}, {"time": 1609, "key": "E4"}, - {"time": 2179, "key": "F4"}, {"time": 2279, "key": "F4"}, {"time": 2765, "key": "F4"}, {"time": 2869, "key": "F4"}, - {"time": 3405, "key": "A4"}, {"time": 3479, "key": "A4"}, {"time": 4019, "key": "A4"}, {"time": 4085, "key": "A4"}, - {"time": 4656, "key": "G4"}, {"time": 4735, "key": "G4"}] - */ - +// Plays the playback of the recording with note timings and keys function playRecording(id, jsonData) { console.log('Playing recording:', id, 'data: ', jsonData); - const data = JSON.parse(jsonData); - data.forEach(({time, key}) => { + const data = JSON.parse(jsonData); // parse json + data.forEach(({time, key}) => { // iterate over each press console.log(`key - ${key}, time - ${time}ms`); setTimeout(function () { if(!activeOscillators.has(key)) { @@ -395,8 +425,9 @@ function playRecording(id, jsonData) { } else { stopNoteAutoplay(key); // Stop the note if it is playing } - }, time); + }, time); // activate start or stop given the recorded time of key press }); } +// Start the website by populating the table with server data fetchRecordings(); \ No newline at end of file From edeba4f0428d04632fd4a75589c92ff45e1c86cf Mon Sep 17 00:00:00 2001 From: Bobzera95 <122205511+Bobzera95@users.noreply.github.com> Date: Tue, 13 Feb 2024 22:05:19 +0100 Subject: [PATCH 11/12] Update README.md --- README.md | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 42553ee66..56b2c9806 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,28 @@ -# README +This JavaScript code implements a virtual piano application with functionality for playing notes, recording user input, and managing recordings. Here's an overview of the key components and functionalities: -This is the [Flask](http://flask.pocoo.org/) [quick start](http://flask.pocoo.org/docs/1.0/quickstart/#a-minimal-application) example for [Render](https://render.com). +### Setup and Initialization +- **Frequency Map (`notesFreq`)**: Defines the frequencies for musical notes, facilitating the creation of sound based on piano key presses. +- **DOM Elements Creation**: Dynamically generates piano keys (`div` elements) for each note defined in `notesFreq` and adds them to the page. +- **Audio Context**: Initializes an `AudioContext` for managing and playing sounds. -The app in this repo is deployed at [https://flask.onrender.com](https://flask.onrender.com). +### Sound Generation +- **Oscillator and Gain Node Creation (`createOscillatorAndGainNode`)**: Creates an oscillator for generating waveforms at specific frequencies and a gain node for controlling the volume, including an ADSR envelope for natural sounding note attacks and decays. +- **Start and Stop Note Functions**: Handle starting and stopping notes based on user interactions with piano keys, updating the visual state of keys and managing oscillators to play the corresponding sounds. -## Deployment +### User Interaction +- **Mouse and Pointer Events**: Captures user interactions with piano keys through mouse and pointer events, allowing for playing notes both by clicking and by dragging across keys. +- **Recording Functionality**: Allows users to record their sequences of note presses, including the timing of each note, and provides functionality to stop recording and name the recording. -Follow the guide at https://render.com/docs/deploy-flask. +### Recording Management +- **Playback**: Plays back recorded sequences by scheduling the start and stop times of notes based on the recorded timings. +- **CRUD Operations for Recordings**: Communicates with a backend server to save, list, rename, and delete recordings. This involves sending HTTP requests and handling responses to reflect changes in the UI dynamically. + +### Web Application Interactions +- **Fetching and Displaying Recordings**: Retrieves a list of saved recordings from the server and updates the UI to allow users to play, rename, or delete recordings. +- **Server Communication**: Uses `fetch` API to send and receive data from the server, handling both the creation of new recordings and the retrieval of existing ones. + +### Considerations and Enhancements +- The application emphasizes the use of the Web Audio API for sound generation and control, showcasing how web technologies can create interactive musical experiences. +- It demonstrates handling of complex user interactions, dynamic content creation, and communication with a server-side application for persistent storage. + +This code serves as a practical example of combining various web technologies to build an interactive application, suitable for a university-level computer science project. It illustrates key concepts such as DOM manipulation, event handling, asynchronous JavaScript, and working with the Web Audio API. From 5ac4011ed03ccb20416bda742e975a92043532c1 Mon Sep 17 00:00:00 2001 From: Bobzera95 <122205511+Bobzera95@users.noreply.github.com> Date: Tue, 13 Feb 2024 22:07:04 +0100 Subject: [PATCH 12/12] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 56b2c9806..42239a80b 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,11 @@ This JavaScript code implements a virtual piano application with functionality f - **CRUD Operations for Recordings**: Communicates with a backend server to save, list, rename, and delete recordings. This involves sending HTTP requests and handling responses to reflect changes in the UI dynamically. ### Web Application Interactions -- **Fetching and Displaying Recordings**: Retrieves a list of saved recordings from the server and updates the UI to allow users to play, rename, or delete recordings. +- **Fetching and Displaying Recordings**: Retrieves a list of saved recordings from the server and updates the UI to allow users to **play**, **rename**, or **delete** recordings. - **Server Communication**: Uses `fetch` API to send and receive data from the server, handling both the creation of new recordings and the retrieval of existing ones. ### Considerations and Enhancements - The application emphasizes the use of the Web Audio API for sound generation and control, showcasing how web technologies can create interactive musical experiences. - It demonstrates handling of complex user interactions, dynamic content creation, and communication with a server-side application for persistent storage. -This code serves as a practical example of combining various web technologies to build an interactive application, suitable for a university-level computer science project. It illustrates key concepts such as DOM manipulation, event handling, asynchronous JavaScript, and working with the Web Audio API. +This code serves as a practical example of combining various web technologies to build an interactive application. It illustrates key concepts such as DOM manipulation, event handling, asynchronous JavaScript, and working with the Web Audio API.