ref: 86a1bffad7063d1f51dfe44c2126bbdf8b7fe5c8
dir: /music.js/
import {toNumber, toInteger} from "./compile.js" import {MakeBlock} from "./model.js" import {audioContext as ctx} from "./stage.js" import {PositiveSlot, EnumeratedSlot} from "./core.js" function playInstrument(instrument, note, seconds, destination) { let sample = instrument.samples.findLast(sample => sample.note <= note) ?? instrument.samples[0] if (!sample.buffer) return let node = ctx.createBufferSource() node.buffer = sample.buffer node.playbackRate.value = 2 ** ((note - sample.note) / 12) let gainNode = ctx.createGain() let releaseTime = ctx.currentTime + seconds gainNode.gain.setValueAtTime(1, releaseTime) gainNode.gain.linearRampToValueAtTime(0, releaseTime + (instrument.release ?? 0)) node.connect(gainNode) gainNode.connect(destination) node.start() } function playDrum(drum, destination) { if (!drum.buffer) return let node = ctx.createBufferSource() node.buffer = drum.buffer node.connect(destination) node.start() } export function extendSprite(sprite, name) { return extend(sprite, sprite.stage, name) } export function extendStage(stage, name) { return extend(stage, stage, name) } function extend(sprite, stage, name) { let music = {} let stageMusic = stage.extensions[name] ?? music let blockFns = { playDrum: (drum, beats) => { playDrum(drums[toInteger(drum) - 1] ?? drums[0], sprite.audioDestination) return blockFns.rest(beats) }, playNote: (note, beats) => { if (toNumber(beats) <= 0) return playInstrument(music.instrument, toNumber(note), toNumber(beats) / stageMusic.tempo * 60, sprite.audioDestination) return blockFns.rest(beats) }, rest: beats => sleep(toNumber(beats) / stageMusic.tempo * 60000), setInstrument: instrument => music.instrument = instruments[toInteger(instrument) - 1] ?? music.instrument, setTempo: tempo => stageMusic.tempo = toNumber(tempo), changeTempo: change => stageMusic.tempo += toNumber(change), tempo: () => stageMusic.tempo, } let blocks = {} for (let name of Object.keys(blockFns)) { let upperCaseName = name[0].toUpperCase() + name.slice(1) blocks[upperCaseName] = MakeBlock({ run: blockFns[name], async: sync[name] === "async", slots: slots[name] ?? [], name: nameFns[name], shape: name === "tempo" ? "reporter" : undefined, defaults: defaults[name], }) } music.blocks = blocks music.instrument = sprite.original?.extensions[name].instrument ?? instruments[0] if (sprite === stage) music.tempo = 60 return music } function sleep(duration) { if (typeof Animation === "undefined") { return new Promise(resolve => setTimeout(resolve, duration)) } let animation = new Animation(new KeyframeEffect(null, null, {duration}), document.timeline) animation.play() return animation.finished } let icon = `<svg viewbox="0 0 24 24" fill="#48E" stroke="#444"><path d="M12 5v8.55c-.94-.54-2.1-.75-3.33-.32-1.34.48-2.37 1.67-2.61 3.07-.46 2.74 1.86 5.08 4.59 4.65 1.96-.31 3.35-2.11 3.35-4.1V7h2c1.1 0 2-.9 2-2s-.9-2-2-2h-2c-1.1 0-2 .9-2 2z"/></svg>` function getIcon() { let span = document.createElement("span") span.insertAdjacentHTML("beforeend", icon) let svg = span.querySelector("svg") svg.style.setProperty("height", "1.5em") svg.style.setProperty("vertical-align", "middle") return span } let sync = { playDrum: "async", playNote: "async", rest: "async", } let nameFns = { playDrum: (drum, beats) => [getIcon(), "play drum", drum, "for", beats, "beats"], playNote: (note, beats) => [getIcon(), "play note", note, "for", beats, "beats"], rest: beats => [getIcon(), "rest for", beats, "beats"], setInstrument: instrument => [getIcon(), "set instrument to", instrument], setTempo: tempo => [getIcon(), "set tempo to", tempo], changeTempo: change => [getIcon(), "change tempo by", change], tempo: () => [getIcon(), "tempo"], } let slots = { playDrum: [undefined, PositiveSlot], playNote: [PositiveSlot, PositiveSlot], rest: [PositiveSlot], setInstrument: [undefined], setTempo: [PositiveSlot], changeTempo: [PositiveSlot], } let defaults = { playDrum: [1, 0.25], playNote: [60, 0.25], rest: [0.25], setInstrument: [1], setTempo: [60], changeTempo: [20], } let instruments = [ {name: "Piano", release: 0.5, notes: [24, 36, 48, 60, 72, 84, 96, 108]}, {name: "Electric Piano", release: 0.5, notes: [60]}, {name: "Organ", release: 0.5, notes: [60]}, {name: "Guitar", release: 0.5, notes: [60]}, {name: "Electric Guitar", release: 0.5, notes: [60]}, {name: "Bass", release: 0.25, notes: [36, 48]}, {name: "Pizzicato", release: 0.25, notes: [60]}, {name: "Cello", release: 0.1, notes: [36, 48, 60]}, {name: "Trombone", notes: [36, 48, 60]}, {name: "Clarinet", notes: [48, 60]}, {name: "Saxophone", notes: [36, 60, 84]}, {name: "Flute", notes: [60, 72]}, {name: "Wooden Flute", notes: [60, 72]}, {name: "Bassoon", notes: [36, 48, 60]}, {name: "Choir", release: 0.25, notes: [48, 60, 72]}, {name: "Vibraphone", release: 0.5, notes: [60, 72]}, {name: "Music Box", release: 0.25, notes: [60]}, {name: "Steel Drum", release: 0.5, notes: [60]}, {name: "Marimba", notes: [60]}, {name: "Synth Lead", release: 0.1, notes: [60]}, {name: "Synth Pad", release: 0.25, notes: [60]}, ] let drums = [ {name: "Snare Drum"}, {name: "Bass Drum"}, {name: "Side Stick"}, {name: "Crash Cymbal"}, {name: "Open Hi-Hat"}, {name: "Closed Hi-Hat"}, {name: "Tambourine"}, {name: "Hand Clap"}, {name: "Claves"}, {name: "Wood Block"}, {name: "Cowbell"}, {name: "Triangle"}, {name: "Bongo"}, {name: "Conga"}, {name: "Cabasa"}, {name: "Guiro"}, {name: "Vibraslap"}, {name: "Cuica"}, ] let instrumentOptions = [] let drumOptions = [] for (let [i, instrument] of instruments.entries()) { instrumentOptions.push(i + 1) let name = instrument.name.replaceAll(" ", "-").toLowerCase() let samples = instrument.notes.map(note => ({note})) instrument.samples = samples for (let [i, note] of instrument.notes.entries()) { fetch(new URL(`assets/${name}-${note}.mp3`, import.meta.url)) .then(response => response.arrayBuffer()) .then(buffer => new Promise((resolve, reject) => ctx.decodeAudioData(buffer, resolve, reject))) .then(buffer => samples[i].buffer = buffer) } } for (let [i, drum] of drums.entries()) { drumOptions.push(i + 1) let name = drum.name.replaceAll(" ", "-").toLowerCase() fetch(new URL(`assets/${name}.mp3`, import.meta.url)) .then(response => response.arrayBuffer()) .then(buffer => new Promise((resolve, reject) => ctx.decodeAudioData(buffer, resolve, reject))) .then(buffer => drum.buffer = buffer) } slots.setInstrument[0] = EnumeratedSlot(instrumentOptions, {canFit: undefined, name: i => `(${i}) ${instruments[i - 1].name}`}) slots.playDrum[0] = EnumeratedSlot(drumOptions, {canFit: undefined, name: i => `(${i}) ${drums[i - 1].name}`})