diff --git a/discord_bot/bot.py b/discord_bot/bot.py index ec1db60..21b20fa 100644 --- a/discord_bot/bot.py +++ b/discord_bot/bot.py @@ -102,7 +102,7 @@ def parse_song(message: discord.Message): return None note_begin = message.content.find("!bongo") - title = message.content[:note_begin].strip() + title = message.content[:note_begin].strip().replace("\n", "\\n") note_begin = message.content.find(" ", note_begin+1) notes = message.content[note_begin:].strip() @@ -121,7 +121,6 @@ def parse_song(message: discord.Message): if "v" in notes: notation = "bongo+" - pass return Song(title, title.replace(" ", "_").lower(), message.author.name, notation, notes) @@ -202,6 +201,8 @@ async def save_song(interaction: Interaction, message: Message): if not song: await interaction.response.send_message("No song detected!", ephemeral=True) return + print(song) + print(message) interaction.extras["song"] = song interaction.extras["modal"] = SaveSongModal(default_song_title=song.title, default_file_name=song.file_name, default_author=song.author, default_notation=song.notation, default_notes=song.notes, message=message) await interaction.response.send_modal(interaction.extras["modal"]) diff --git a/src/bongocat.css b/src/bongocat.css index 98ddcfd..637c171 100644 --- a/src/bongocat.css +++ b/src/bongocat.css @@ -126,4 +126,7 @@ body,html { } #instruments #TR808 { background: url("images/808.png"); +} +#instruments #nokia3210 { + background: url("images/NOKIA3210.png"); } \ No newline at end of file diff --git a/src/bongocat.js b/src/bongocat.js index 3f5d5f9..70949fe 100644 --- a/src/bongocat.js +++ b/src/bongocat.js @@ -1,4 +1,5 @@ import {parseSong as parseSongBongo} from "./modules/bongo.js"; +import {experimentalFeatures} from "./experimental/bongox.js"; // ====================================================== // // ================== type definitions ================== // @@ -42,7 +43,7 @@ var queue = []; var bongoEnabled = true; var playing = false; setBPM(128); -var githubUrls = ["https://raw.githubusercontent.com/jvpeek/twitch-bongocat/master/songs/","https://raw.githubusercontent.com/awsdcrafting/bongocat-songs/live/songs/"]; +var githubUrls = ["https://raw.githubusercontent.com/jvpeek/twitch-bongocat/master/songs/", "https://raw.githubusercontent.com/awsdcrafting/bongocat-songs/live/songs/"]; var stackMode = false; var maxSongLength = 90_000; //90 secs var defaultNotation = "bongo"; @@ -52,6 +53,12 @@ var volume = 1.0; window.maxNotesPerBatch = 5; +var audioContext; +var mainGainNode; +var oscillatorNode; +var synthStarted = false; +var synthDampening = 0.75; + // ====================================================== // // ================== notation handlers ================= // // ====================================================== // @@ -69,6 +76,7 @@ notations["bongo+"] = parseSongBongo; function setVolume(volumeParam) { volume = Math.min(1.0, Math.max(0, Number(volumeParam))); + mainGainNode.gain.value = volume * synthDampening; } function setMaxSongLength(maxSongLengthParam) @@ -83,6 +91,20 @@ function setMaxSongLength(maxSongLengthParam) } } +function clamp(value, min, max) +{ + if (value > max) + { + return max; + } + if (value < min) + { + return min; + } + return value; + //return Math.min(Math.max(value, min), max) +} + function setBPM(targetBPM, username) { targetBPM = Number(targetBPM); @@ -139,6 +161,36 @@ function playSound(cmd, cBpm) audio.play(); } +function prepareSynth(type) +{ + audioContext = new AudioContext(); + mainGainNode = audioContext.createGain(); + mainGainNode.connect(audioContext.destination); + mainGainNode.gain.value = 0; + oscillatorNode = audioContext.createOscillator(); + oscillatorNode.connect(mainGainNode); + oscillatorNode.type = type; + synthStarted = false; + return {"ctx": audioContext, "gain": mainGainNode, "osc": oscillatorNode}; +} + +function playSynthSound(frequency, time) +{ + if (!synthStarted) + { + oscillatorNode.start(); + oscillatorNode.stop(maxSongLength / 1000); + synthStarted = true; + } + mainGainNode.gain.setValueAtTime(volume * synthDampening, time); + oscillatorNode.frequency.setValueAtTime(frequency, time); +} + +function muteSynth(time) +{ + mainGainNode.gain.setValueAtTime(0, time); +} + function introAnimation(song) { let username = song.performer; @@ -174,6 +226,15 @@ function outroAnimation() { document.getElementById("bongocat").style.left = "-1920px"; setInstrument("none"); + if (mainGainNode) + { + mainGainNode.gain.value = 0; + } + if (oscillatorNode) + { + oscillatorNode.stop(); + synthStarted = false; + } setTimeout(function () { playing = false; @@ -198,7 +259,7 @@ function setPaw(paw, cBpm) { var currentPaw = document.getElementById(paw); currentPaw.style.backgroundPosition = "top right"; - window.setTimeout(releasePaw, cBpm / 2, paw); + window.setTimeout(releasePaw, 1000 * (60 / cBpm / 2), paw); } function releasePaw(paw) @@ -213,7 +274,7 @@ function preparePlaybackObject(cmd, time, ...args) return {time: time, cmd: cmd, args: args}; } -var helperMethods = {setBPM, getBPM, playSound, introAnimation, outroAnimation, setInstrument, setPaw, releasePaw, preparePlaybackObject}; +var helperMethods = {clamp, setBPM, getBPM, playSound, prepareSynth, playSynthSound, muteSynth, introAnimation, outroAnimation, setInstrument, setPaw, releasePaw, preparePlaybackObject}; for (const key in helperMethods) { window[key] = helperMethods[key]; @@ -294,12 +355,16 @@ function checkQueue() { var song = getFromQueue(); let handler = notations[song.notation]; + if (song.experimental) + { + handler = experimentalFeatures[song.notation] || handler; + } if (handler) { currentSong = song; currentSong.timeoutIDs = []; - introAnimation(song); let playbacks = handler(song); + introAnimation(song); console.log(playbacks); for (let playback of playbacks) { @@ -329,21 +394,22 @@ async function playFromGithub(song, user) song += ".json"; console.log("Playing", song, "from github for", user); - for (let githubUrl of githubUrls) { + for (let githubUrl of githubUrls) + { const response = await fetch(encodeURI(githubUrl + song.trim())); if (response.status != 200) { continue; } - //console.log(response) + //console.log(response) const jsonData = await response.json(); console.log(jsonData); jsonData.performer = user; jsonData.dedications = dedications; addToQueue(jsonData); - break + break; } - + } @@ -482,6 +548,19 @@ function bongo(args) } commands["!bongol"] = bongo; +function bongox(args) +{ + let split = args.arg.indexOf(" "); + let experiment = args.arg.slice(0, split); + let notes = args.arg.slice(split + 1); + let username = args.tags.username; + let song = {performer: args.tags.username, notes: notes, notation: experiment, "experimental": true}; + addToQueue(song); + //experimentalFeatures[experiment]?.(notes, username) +} +commands["!bongox"] = bongox; + + function changeBpm(args) { if (!bongoEnabled) diff --git a/src/experimental/bongox.js b/src/experimental/bongox.js index 2166ce4..993fd05 100644 --- a/src/experimental/bongox.js +++ b/src/experimental/bongox.js @@ -1,9 +1,264 @@ -experimentalFeatures = {} +let experimentalFeatures = {}; -function rttl(args) { - let song = args.arg +function createNoteTable() +{ + const noteFreq = []; + for (let i = 0; i < 9; i++) + { + noteFreq[i] = []; + } + + noteFreq[0]["A"] = 27.500000000000000; + noteFreq[0]["A#"] = 29.135235094880619; + noteFreq[0]["B"] = 30.867706328507756; + + noteFreq[1]["C"] = 32.703195662574829; + noteFreq[1]["C#"] = 34.647828872109012; + noteFreq[1]["D"] = 36.708095989675945; + noteFreq[1]["D#"] = 38.890872965260113; + noteFreq[1]["E"] = 41.203444614108741; + noteFreq[1]["F"] = 43.653528929125485; + noteFreq[1]["F#"] = 46.249302838954299; + noteFreq[1]["G"] = 48.999429497718661; + noteFreq[1]["G#"] = 51.913087197493142; + noteFreq[1]["A"] = 55.000000000000000; + noteFreq[1]["A#"] = 58.270470189761239; + noteFreq[1]["B"] = 61.735412657015513; + + noteFreq[2]["C"] = 65.406391325149658; + noteFreq[2]["C#"] = 69.295657744218024; + noteFreq[2]["D"] = 73.41619197935189; + noteFreq[2]["D#"] = 77.781745930520227; + noteFreq[2]["E"] = 82.406889228217482; + noteFreq[2]["F"] = 87.307057858250971; + noteFreq[2]["F#"] = 92.498605677908599; + noteFreq[2]["G"] = 97.998858995437323; + noteFreq[2]["G#"] = 103.826174394986284; + noteFreq[2]["A"] = 110.0; + noteFreq[2]["A#"] = 116.540940379522479; + noteFreq[2]["B"] = 123.470825314031027; + + noteFreq[3]["C"] = 130.812782650299317; + noteFreq[3]["C#"] = 138.591315488436048; + noteFreq[3]["D"] = 146.83238395870378; + noteFreq[3]["D#"] = 155.563491861040455; + noteFreq[3]["E"] = 164.813778456434964; + noteFreq[3]["F"] = 174.614115716501942; + noteFreq[3]["F#"] = 184.997211355817199; + noteFreq[3]["G"] = 195.997717990874647; + noteFreq[3]["G#"] = 207.652348789972569; + noteFreq[3]["A"] = 220.0; + noteFreq[3]["A#"] = 233.081880759044958; + noteFreq[3]["B"] = 246.941650628062055; + + noteFreq[4]["C"] = 261.625565300598634; + noteFreq[4]["C#"] = 277.182630976872096; + noteFreq[4]["D"] = 293.66476791740756; + noteFreq[4]["D#"] = 311.12698372208091; + noteFreq[4]["E"] = 329.627556912869929; + noteFreq[4]["F"] = 349.228231433003884; + noteFreq[4]["F#"] = 369.994422711634398; + noteFreq[4]["G"] = 391.995435981749294; + noteFreq[4]["G#"] = 415.304697579945138; + noteFreq[4]["A"] = 440.0; + noteFreq[4]["A#"] = 466.163761518089916; + noteFreq[4]["B"] = 493.883301256124111; + + noteFreq[5]["C"] = 523.251130601197269; + noteFreq[5]["C#"] = 554.365261953744192; + noteFreq[5]["D"] = 587.32953583481512; + noteFreq[5]["D#"] = 622.253967444161821; + noteFreq[5]["E"] = 659.255113825739859; + noteFreq[5]["F"] = 698.456462866007768; + noteFreq[5]["F#"] = 739.988845423268797; + noteFreq[5]["G"] = 783.990871963498588; + noteFreq[5]["G#"] = 830.609395159890277; + noteFreq[5]["A"] = 880.0; + noteFreq[5]["A#"] = 932.327523036179832; + noteFreq[5]["B"] = 987.766602512248223; + + noteFreq[6]["C"] = 1046.502261202394538; + noteFreq[6]["C#"] = 1108.730523907488384; + noteFreq[6]["D"] = 1174.659071669630241; + noteFreq[6]["D#"] = 1244.507934888323642; + noteFreq[6]["E"] = 1318.510227651479718; + noteFreq[6]["F"] = 1396.912925732015537; + noteFreq[6]["F#"] = 1479.977690846537595; + noteFreq[6]["G"] = 1567.981743926997176; + noteFreq[6]["G#"] = 1661.218790319780554; + noteFreq[6]["A"] = 1760.0; + noteFreq[6]["A#"] = 1864.655046072359665; + noteFreq[6]["B"] = 1975.533205024496447; + + noteFreq[7]["C"] = 2093.004522404789077; + noteFreq[7]["C#"] = 2217.461047814976769; + noteFreq[7]["D"] = 2349.318143339260482; + noteFreq[7]["D#"] = 2489.015869776647285; + noteFreq[7]["E"] = 2637.020455302959437; + noteFreq[7]["F"] = 2793.825851464031075; + noteFreq[7]["F#"] = 2959.955381693075191; + noteFreq[7]["G"] = 3135.963487853994352; + noteFreq[7]["G#"] = 3322.437580639561108; + noteFreq[7]["A"] = 3520.000000000000000; + noteFreq[7]["A#"] = 3729.310092144719331; + noteFreq[7]["B"] = 3951.066410048992894; + + noteFreq[8]["C"] = 4186.009044809578154; + return noteFreq; +} +const noteFreq = createNoteTable(); +//maybe replace with function, for now use lookup table + + + +function rtttl(song) +{ + let notes = song.notes; + let username = song.performer; + console.log("Playing RTTTL", notes, "for", username); + let duration = 4; + let octave = 5; + let bpm = 63; + + let splits = notes.split(":"); + // we do not use song names at the moment + let name = "" + if (splits.length >= 3) { + name = splits.shift() + name = name.trim() + } + + + let regex = /(?:(\w)=(\d+))|(?:(\d+)?(a#|ab|a|b|h|c#|c|db|d|eb|e#|e|fb|f#|f|gb|g#|g|p)(\d)?(\.)?)/gi; //rtttl notation converted to regex + //groups: + //0 = complete capture + //1 = key + //2 = value + //3 = duration + //4 = note + //5 = octave + //6 = dot + let time = 1; //current time in seconds + let playbacks = []; + prepareSynth("square"); + playbacks.push(preparePlaybackObject(setInstrument, 0, "nokia3210")) + while (splits.length > 0) + { + let notes = splits.shift(); + notes = [...notes.matchAll(regex)]; + for (let note of notes) + { + //param handling + if (note[1] && note[2]) + { + let numberValue = Math.floor(Number(note[2])); + if (Number.isNaN(numberValue)) + { + continue; + } + let key = note[1]; + switch (key) + { + case "b": + bpm = numberValue; + break; + case "o": + octave = numberValue; + break; + case "d": + duration = numberValue; + break; + } + continue; + } + //note handling + let noteDuration = duration; + if (note[3]) + { + let numberValue = Math.floor(Number(note[3])); + if (numberValue && !Number.isNaN(numberValue)) + { + noteDuration = clamp(numberValue, 1, 64); + } + } + let noteLength = 240 / bpm / noteDuration; + if (note[6]) + { + noteLength *= 1.5; + } + let noteOctave = octave; + //noteOctave = 1 + if (note[5]) + { + let numberValue = Math.floor(Number(note[5])); + if (numberValue && !Number.isNaN(numberValue)) + { + noteOctave = clamp(numberValue, 0, 8); + } + } + let noteStr = note[4].toUpperCase(); + //convert alternative notation + switch (noteStr) + { + case "BB": + noteStr = "A#"; + break; + case "CB": + noteStr = "C"; + break; + case "DB": + noteStr = "C#"; + break; + case "EB": + noteStr = "D#"; + break; + case "FB": + noteStr = "E"; + break; + case "E#": + noteStr = "F"; + break; + case "GB": + noteStr = "F#"; + break; + case "AB": + noteStr = "G#"; + break; + } + if (noteStr == "P") + { + + //playback objects do not work as intended??? + playbacks.push(preparePlaybackObject(muteSynth, 0, time)); + //muteSynth(time) + time += noteLength; + } else + { + + let noteFrequency = noteFreq[noteOctave][noteStr]; + if (noteFrequency) + { + let paws = ["paw-left", "paw-right"] + let rnd = 0 + (Math.random() > 0.95) + playbacks.push(preparePlaybackObject(setPaw, time*1000, paws[rnd], bpm * (noteDuration / 4))) + //playSynthSound(noteFrequency, time) + playbacks.push(preparePlaybackObject(playSynthSound, 0, noteFrequency, time)); + time += noteLength / 10 * 8 + //muteSynth(time) + playbacks.push(preparePlaybackObject(muteSynth, 0, time)); + time += noteLength / 10 * 2 + } + } + } + } + + playbacks.push(preparePlaybackObject(outroAnimation, time * 1000)); + prepareSynth("square"); + return playbacks; } -experimentalFeatures["rttl"] = rttl +experimentalFeatures["rtttl"] = rtttl; +experimentalFeatures["rttl"] = rtttl; +export {experimentalFeatures}; diff --git a/src/images/NOKIA3210.png b/src/images/NOKIA3210.png new file mode 100644 index 0000000..e9c7430 Binary files /dev/null and b/src/images/NOKIA3210.png differ diff --git a/src/index.html b/src/index.html index 1e349a4..4df33ec 100644 --- a/src/index.html +++ b/src/index.html @@ -20,6 +20,7 @@
+