diff --git a/tasks.txt b/tasks.txt index ce9bc61a..a471466a 100644 --- a/tasks.txt +++ b/tasks.txt @@ -213,20 +213,41 @@ √ layout was messed up if browser default font size is set to large. √ add a "clean slate"/"new song" button with a little dog-eared page icon. √ while I'm at it, the edit menu should have a pencil icon and the prefs should have a gear or wrench. +√ rename barpattern to pattern, bareditor to patterneditor, attack to envelope... +√ tempo slider could use tooltip for BPM +√ add auto-follow and auto-play preferences. (I guess autoplay is only on computers? but defaults to true?) -- add auto-follow and auto-play preferences. (I guess autoplay is only in browser? but defaults to true?) +PHASE MODULATION: +- interface to select type of instrument. wave/phase/drums? goes above instrument number. +- allow channels to be in any order regardless of type... how do they get colored? or can only pitch channels swap types? wait maybe pitch and drums both have a phase modulation variant... +- new change to swap channel type +- modulation sliders, ratio selectors, envelop selectors. steady/decay123/attack123/rise123/in-out123?/custom? +- forward compatible instrument url +- backward compatible url parsing +- dim modulator settings when modulation is zero. +- provide some visual indication for which modulators affect what. + +MAJOR REMAINING FEATURES: +- phase modulation +- instrument options: reverb,chorus,fadeout,distortion (applied to all enabled channels at once?) +- embedded player +- playlist editor +- remember recent songs +- export mp3/ogg and audio buffer +- audio worklet thread +- translation MISC: - I don't like how when you click the wave menu then use arrow keys to select drums the editor loses focus. - pitch bends mixed with arpeggios don’t properly check min/max bounds -- can I improve the interaction for volume envelope dragging and pitch dragging? illustrate volume envelope dragging to make it less confusing. +- Distortion applying to some channels? new song setting: distortion, nonlinear mixing of channels... can't handle a range of volumes? +- Maybe add a fadeout option that decays carrier volume until the next note starts? Or is that redundant since you can do the same thing with envelope editing? - Bring beepbox and openscores coding styles closer together. See html functions, Synth-vs-SongPlayer - remember recent songs in cookies or local shared object. -- export mp3 and ogg? +- asynchronous export. worker thread? - a way to pre-generate a javascript sound buffer. -- asynchronous export -- Make an embeddable song player and album player. (See below.) - document synth api +- export mp3 and ogg? - do audio worker threads work yet? Maybe feature-detect them? "AudioWorkerNode" vs "AudioWorkletNode"??? EMBEDDED PLAYER: @@ -245,7 +266,7 @@ EMBEDDED PLAYER: - manifests to allow installing web app as mobile app. add note in instructions. add any new files to app.yaml - allow copying the url from the share menu, so the app version can hide the url bar. - sharing menu uses localSharedObject to remember your settings. same as editor volume? -- should I make cards for twitter/facebook/slack/discord etc? Wait I need https for that I think... +- should I make cards for twitter/facebook/slack/discord etc? - make sure bots ignore embed.html PLAYLIST EDITOR: @@ -258,44 +279,30 @@ PLAYLIST EDITOR: - when starting at a bar where all tones are at the end of the bar, go ahead and skip the beginning of the bar, up to maybe one beat before the first tone. - put songs in browser history before/after pasting new songs. - offsite album/playlist player, with scrolling notes. hashbang urls, bot indexable. - -NEW EDITORS: -- Rename synth.ts to BeepBoxSynth.ts. Pull out some of it into CommonSynth.ts. Rename SongEditor to BeepBoxSongEditor. OR: start using directories... but Typescript is pretty mean about directories if I recall? +- custom cover art, color schemes in playlists and embed player. MAYBE: +- compacting and concatenating songs! +- can I improve the interaction for volume envelope dragging and pitch dragging? illustrate volume envelope dragging to make it less confusing. +- If adding new note and mouse drags at least two beats closer to start than previous max, then enter precise mode where you can have any length. If flipping to backwards or <= doc.parts, reset out of precise mode. +- make some button to bookmark the song in your browser +- snap to tone edges from other channels, to help making NES style harmonies. +- piano should still play when song is paused - translation/localization - Get in touch with ludum dare about linking to beepbox: http://www.ludumdare.com/compo/tools/ - get in touch with http://youcanmakevideogames.com/resources/music -- new preferences: follow playhead, play at load (on desktop)? - multitouch? - replace piano/drums canvas with svg -- piano should still play when song is paused - illustrate arpeggio texture on tones with multiple notes, striped fill -- @Gasparatus asked for a BPM display. -- custom cover art, color schemes in playlists and embed player. - on linux chrome, select drop downs use same background color as select element, it's a bit too bright when focused. -- per-pattern drop down menu for instrument selection, prepopulated with common ones. - export fade in/out checkboxes -- derive song length from number of bars with non-zero patterns -- more “effect” keys like stutter, scratch, scrub, drag playhead, speedmult, whatever. -- detect vibrate switch +- per-pattern drop down menu for instrument selection, prepopulated with common ones. - circle of fifths display -- allow drums to be divided into shorter parts, crucial for songs like the mario overworld -- 5 parts per beat? -- double number of beat divisions -- it would be awesome to have a couple of modes where when you drag the playhead, it overrides the tempo of the song to match the motion of your mouse. In one mode it distorts the pitch like a DJ table, in the other it plays the normal pitch like SoundSPEAR. -- Distortion applying to blue and yellow channels - Dim the column occupied by a tone to communicate mono voice? -- rename barpattern to pattern, bareditor to patterneditor, attack to envelope... -- snap to tone edges from other channels, to help making NES style harmonies. -- new song setting: distortion, nonlinear mixing of channels... -- If mouse drags at least two beats closer to start than previous max, then enter precise mode where you can have any length. If flipping to backwards or <= doc.parts, reset out of precise mode. -- compacting and concatenating songs! - visual feedback when a note is played - when on the same pitch as a tone in an adjacent part, and you're really close to it, and you drag horizontally, go ahead and drag that adjacent tone instead of creating a new one. - options for timing offset - phoneme filter option, imitate vowels and voiced consonants with IIR filters, fall back on noise channel for S, T, CH, SH, etc. - a facebook app to put songs in your profile - preload some common tone shapes into the recentShape list. increment format number. -- make some button to bookmark the song in your browser - Make a bunch of asserts, that the tones don't overlap, that the pins have the same duration as the tones, that the pins are in order, etc. And trace out the arguments to every change constructor and maybe even trace out the entire bar document data after every step. diff --git a/ts/ExportPrompt.ts b/ts/ExportPrompt.ts index e77f6fa9..96a9ef45 100644 --- a/ts/ExportPrompt.ts +++ b/ts/ExportPrompt.ts @@ -495,7 +495,7 @@ namespace beepbox { const intervalScale: number = isDrums ? Config.drumInterval : 1; for (const bar of unrolledBars) { - const pattern: BarPattern | null = song.getPattern(channel, bar); + const pattern: Pattern | null = song.getPattern(channel, bar); if (pattern != null) { diff --git a/ts/PatternEditor.ts b/ts/PatternEditor.ts index 1e382a6d..1aa8da4e 100644 --- a/ts/PatternEditor.ts +++ b/ts/PatternEditor.ts @@ -109,7 +109,7 @@ namespace beepbox { //private _precisionX: number = 0; private _dragChange: UndoableChange | null = null; private _cursor: PatternCursor = new PatternCursor(); - private _pattern: BarPattern | null = null; + private _pattern: Pattern | null = null; private _playheadX: number = 0.0; private _octaveOffset: number = 0; private _renderedWidth: number = -1; @@ -119,6 +119,7 @@ namespace beepbox { private _renderedPartsPerBeat: number = -1; private _renderedPitchChannelCount: number = -1; private _renderedDrumChannelCount: number = -1; + private _followPlayheadBar: number = -1; constructor(private _doc: SongDocument) { for (let i: number = 0; i < 12; i++) { @@ -381,11 +382,12 @@ namespace beepbox { } private _animatePlayhead = (timestamp: number): void => { + const playheadBar: number = Math.floor(this._doc.synth.playhead); if (!this._doc.synth.playing || this._pattern == null || this._doc.song.getPattern(this._doc.channel, Math.floor(this._doc.synth.playhead)) != this._pattern) { this._svgPlayhead.setAttribute("visibility", "hidden"); } else { this._svgPlayhead.setAttribute("visibility", "visible"); - const modPlayhead: number = this._doc.synth.playhead - Math.floor(this._doc.synth.playhead); + const modPlayhead: number = this._doc.synth.playhead - playheadBar; if (Math.abs(modPlayhead - this._playheadX) > 0.1) { this._playheadX = modPlayhead; } else { @@ -393,6 +395,12 @@ namespace beepbox { } this._svgPlayhead.setAttribute("x", "" + prettyNumber(this._playheadX * this._editorWidth - 2)); } + + if (this._doc.synth.playing && this._doc.autoFollow && this._followPlayheadBar != playheadBar) { + new ChangeChannelBar(this._doc, this._doc.channel, playheadBar); + this._doc.notifier.notifyWatchers(); + } + this._followPlayheadBar = playheadBar; window.requestAnimationFrame(this._animatePlayhead); } @@ -616,7 +624,7 @@ namespace beepbox { this._dragVolume = bendVolume; this._dragVisible = true; - sequence.append(new ChangeVolumeBend(this._doc, this._pattern, this._cursor.curNote, bendPart, bendVolume, bendInterval)); + sequence.append(new ChangeVolumeBend(this._doc, this._cursor.curNote, bendPart, bendVolume, bendInterval)); this._copyPins(this._cursor.curNote); } else { this._dragVolume = this._cursor.curNote.pins[this._cursor.nearPinIndex].volume; @@ -821,7 +829,7 @@ namespace beepbox { if (channel == this._doc.channel) continue; if (this._doc.song.getChannelIsDrum(channel) != this._doc.song.getChannelIsDrum(this._doc.channel)) continue; - const pattern2: BarPattern | null = this._doc.song.getPattern(channel, this._doc.bar); + const pattern2: Pattern | null = this._doc.song.getPattern(channel, this._doc.bar); if (pattern2 == null) continue; for (const note of pattern2.notes) { for (const pitch of note.pitches) { diff --git a/ts/SongDocument.ts b/ts/SongDocument.ts index 321f4f54..8d18fc57 100644 --- a/ts/SongDocument.ts +++ b/ts/SongDocument.ts @@ -41,6 +41,8 @@ namespace beepbox { public notifier: ChangeNotifier = new ChangeNotifier(); public channel: number = 0; public bar: number = 0; + public autoPlay: boolean; + public autoFollow: boolean; public showFifth: boolean; public showLetters: boolean; public showChannels: boolean; @@ -61,6 +63,8 @@ namespace beepbox { this.song = new Song(string); this.synth = new Synth(this.song); + this.autoPlay = localStorage.getItem("autoPlay") != "false"; + this.autoFollow = localStorage.getItem("autoFollow") == "true"; this.showFifth = localStorage.getItem("showFifth") == "true"; this.showLetters = localStorage.getItem("showLetters") == "true"; this.showChannels = localStorage.getItem("showChannels") == "true"; @@ -197,6 +201,8 @@ namespace beepbox { } public savePreferences(): void { + localStorage.setItem("autoPlay", this.autoPlay ? "true" : "false"); + localStorage.setItem("autoFollow", this.autoFollow ? "true" : "false"); localStorage.setItem("showFifth", this.showFifth ? "true" : "false"); localStorage.setItem("showLetters", this.showLetters ? "true" : "false"); localStorage.setItem("showChannels", this.showChannels ? "true" : "false"); @@ -214,12 +220,12 @@ namespace beepbox { return Math.min(1.0, Math.pow(this.volume / 50.0, 0.5)) * Math.pow(2.0, (this.volume - 75.0) / 25.0); } - public getCurrentPattern(): BarPattern | null { + public getCurrentPattern(): Pattern | null { return this.song.getPattern(this.channel, this.bar); } public getCurrentInstrument(): number { - const pattern: BarPattern | null = this.getCurrentPattern(); + const pattern: Pattern | null = this.getCurrentPattern(); return pattern == null ? 0 : pattern.instrument; } } diff --git a/ts/SongEditor.ts b/ts/SongEditor.ts index ef4b3529..01ad8e33 100644 --- a/ts/SongEditor.ts +++ b/ts/SongEditor.ts @@ -38,6 +38,8 @@ SOFTWARE. namespace beepbox { const {button, div, span, select, option, input, text} = html; + const isMobile: boolean = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|android|ipad|playbook|silk/i.test(navigator.userAgent); + function buildOptions(menu: HTMLSelectElement, items: ReadonlyArray): HTMLSelectElement { for (const item of items) { menu.appendChild(option(item, item, false, false)); @@ -95,6 +97,8 @@ namespace beepbox { ]); private readonly _optionsMenu: HTMLSelectElement = select({style: "width:100%;"}, [ option("", "Preferences", true, true), + option("autoPlay", "Auto Play On Load", false, false), + option("autoFollow", "Auto Follow Track", false, false), option("showLetters", "Show Piano", false, false), option("showFifth", "Highlight 'Fifth' Notes", false, false), option("showChannels", "Show All Channels", false, false), @@ -155,7 +159,7 @@ namespace beepbox { public readonly mainLayer: HTMLDivElement = div({className: "beepboxEditor", tabIndex: "0"}, [ this._editorBox, div({className: "editor-widget-column"}, [ - div({style: "text-align: center; color: #999;"}, [text("BeepBox 2.2.1")]), + div({style: "text-align: center; color: #999;"}, [text("BeepBox 2.2.2")]), div({className: "editor-widgets"}, [ div({className: "editor-controls"}, [ div({className: "playback-controls"}, [ @@ -261,6 +265,8 @@ namespace beepbox { this._editorBox.addEventListener("mousedown", this._refocusStage); this.mainLayer.addEventListener("keydown", this._whenKeyPressed); + + if (isMobile) ( this._optionsMenu.children[1]).disabled = true; } private _openPrompt(promptName: string): void { @@ -312,6 +318,8 @@ namespace beepbox { this._trackEditor.render(); const optionCommands: ReadonlyArray = [ + (this._doc.autoPlay ? "✓ " : "") + "Auto Play On Load", + (this._doc.autoFollow ? "✓ " : "") + "Auto Follow Track", (this._doc.showLetters ? "✓ " : "") + "Show Piano", (this._doc.showFifth ? "✓ " : "") + "Highlight 'Fifth' Notes", (this._doc.showChannels ? "✓ " : "") + "Show All Channels", @@ -325,6 +333,7 @@ namespace beepbox { setSelectedIndex(this._scaleDropDown, this._doc.song.scale); setSelectedIndex(this._keyDropDown, this._doc.song.key); this._tempoSlider.value = "" + this._doc.song.tempo; + this._tempoSlider.title = this._doc.song.getBeatsPerMinute() + " beats per minute"; this._reverbSlider.value = "" + this._doc.song.reverb; setSelectedIndex(this._partDropDown, Config.partCounts.indexOf(this._doc.song.partsPerBeat)); if (this._doc.song.getChannelIsDrum(this._doc.channel)) { @@ -341,7 +350,7 @@ namespace beepbox { this._drumNames.style.display = "none"; } - const pattern: BarPattern | null = this._doc.getCurrentPattern(); + const pattern: Pattern | null = this._doc.getCurrentPattern(); this._instrumentDropDownGroup.style.display = (this._doc.song.instrumentsPerChannel > 1) ? "flex" : "none"; this._instrumentDropDownGroup.style.visibility = (pattern != null) ? "visible" : "hidden"; @@ -366,7 +375,6 @@ namespace beepbox { this._channelVolumeSlider.value = -this._doc.song.instrumentVolumes[this._doc.channel][instrument]+""; setSelectedIndex(this._instrumentDropDown, instrument); - //currentState = this._doc.showLetters ? (this._doc.showScrollBar ? "showPianoAndScrollBar" : "showPiano") : (this._doc.showScrollBar ? "showScrollBar" : "hideAll"); this._piano.container.style.display = this._doc.showLetters ? "block" : "none"; this._octaveScrollBar.container.style.display = this._doc.showScrollBar ? "block" : "none"; this._barScrollBar.container.style.display = this._doc.song.barCount > this._doc.trackVisibleBars ? "" : "none"; @@ -379,6 +387,10 @@ namespace beepbox { this._volumeSlider.value = String(this._doc.volume); this._setPrompt(this._doc.prompt); + + if (this._doc.autoFollow && !this._doc.synth.playing) { + this._doc.synth.snapToBar(this._doc.bar); + } } public updatePlayButton(): void { @@ -435,10 +447,16 @@ namespace beepbox { break; case 219: // left brace this._doc.synth.prevBar(); + if (this._doc.autoFollow) { + new ChangeChannelBar(this._doc, this._doc.channel, Math.floor(this._doc.synth.playhead)); + } event.preventDefault(); break; case 221: // right brace this._doc.synth.nextBar(); + if (this._doc.autoFollow) { + new ChangeChannelBar(this._doc, this._doc.channel, Math.floor(this._doc.synth.playhead)); + } event.preventDefault(); break; case 189: // - @@ -477,7 +495,11 @@ namespace beepbox { private _pause(): void { this._doc.synth.pause(); - this._doc.synth.snapToBar(); + if (this._doc.autoFollow) { + this._doc.synth.snapToBar(this._doc.bar); + } else { + this._doc.synth.snapToBar(); + } this.updatePlayButton(); } @@ -486,7 +508,7 @@ namespace beepbox { } private _copy(): void { - const pattern: BarPattern | null = this._doc.getCurrentPattern(); + const pattern: Pattern | null = this._doc.getCurrentPattern(); if (pattern == null) return; const patternCopy: PatternCopy = { @@ -500,7 +522,7 @@ namespace beepbox { } private _paste(): void { - const pattern: BarPattern | null = this._doc.getCurrentPattern(); + const pattern: Pattern | null = this._doc.getCurrentPattern(); if (pattern == null) return; const patternCopy: PatternCopy | null = JSON.parse(String(window.localStorage.getItem("patternCopy"))); @@ -511,7 +533,7 @@ namespace beepbox { } private _transpose(upward: boolean): void { - const pattern: BarPattern | null = this._doc.getCurrentPattern(); + const pattern: Pattern | null = this._doc.getCurrentPattern(); if (pattern == null) return; const continuousChange: boolean = this._doc.history.lastChangeWas(this._changeTranspose); @@ -586,7 +608,7 @@ namespace beepbox { } private _whenSetInstrument = (): void => { - const pattern : BarPattern | null = this._doc.getCurrentPattern(); + const pattern : Pattern | null = this._doc.getCurrentPattern(); if (pattern == null) return; this._doc.history.record(new ChangePatternInstrument(this._doc, this._instrumentDropDown.selectedIndex, pattern)); } @@ -623,6 +645,12 @@ namespace beepbox { private _optionsMenuHandler = (event:Event): void => { switch (this._optionsMenu.value) { + case "autoPlay": + this._doc.autoPlay = !this._doc.autoPlay; + break; + case "autoFollow": + this._doc.autoFollow = !this._doc.autoFollow; + break; case "showLetters": this._doc.showLetters = !this._doc.showLetters; break; @@ -651,7 +679,7 @@ namespace beepbox { editor.mainLayer.focus(); // don't autoplay on mobile devices, wait for input. - if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|android|ipad|playbook|silk/i.test(navigator.userAgent) ) { + if (!isMobile && doc.autoPlay) { function autoplay(): void { if (!document.hidden) { doc.synth.play(); diff --git a/ts/TrackEditor.ts b/ts/TrackEditor.ts index d1e95ba9..2b2486f5 100644 --- a/ts/TrackEditor.ts +++ b/ts/TrackEditor.ts @@ -103,7 +103,7 @@ namespace beepbox { private readonly _grid: Box[][] = []; private _mouseX: number = 0; private _mouseY: number = 0; - private _pattern: BarPattern | null = null; + private _pattern: Pattern | null = null; private _mouseOver: boolean = false; private _digits: string = ""; private _editorHeight: number = 128; @@ -113,7 +113,7 @@ namespace beepbox { private _renderedPatternCount: number = 0; private _renderedPlayhead: number = -1; private _renderedSquashed: boolean = false; - private _changeBarPattern: ChangeBarPattern | null = null; + private _changePattern: ChangePattern | null = null; constructor(private _doc: SongDocument, private _songEditor: SongEditor) { this._svg.appendChild(this._boxContainer); @@ -133,7 +133,7 @@ namespace beepbox { } private _whenSelectChanged = (): void => { - this._setBarPattern(this._select.selectedIndex); + this._setPattern(this._select.selectedIndex); } private _animatePlayhead = (timestamp: number): void => { @@ -151,13 +151,13 @@ namespace beepbox { this._doc.history.forgetLastChange(); } - private _setBarPattern(pattern: number): void { + private _setPattern(pattern: number): void { const currentValue: number = this._doc.song.channelBars[this._doc.channel][this._doc.bar]; - const continuousChange: boolean = this._doc.history.lastChangeWas(this._changeBarPattern); - const oldValue: number = continuousChange ? this._changeBarPattern!.oldValue : currentValue; + const continuousChange: boolean = this._doc.history.lastChangeWas(this._changePattern); + const oldValue: number = continuousChange ? this._changePattern!.oldValue : currentValue; if (pattern != currentValue) { - this._changeBarPattern = new ChangeBarPattern(this._doc, oldValue, pattern); - this._doc.history.record(this._changeBarPattern, continuousChange); + this._changePattern = new ChangePattern(this._doc, oldValue, pattern); + this._doc.history.record(this._changePattern, continuousChange); } } @@ -229,14 +229,14 @@ namespace beepbox { this._digits += digit; let parsed: number = parseInt(this._digits); if (parsed <= this._doc.song.patternsPerChannel) { - this._setBarPattern(parsed); + this._setPattern(parsed); return; } this._digits = digit; parsed = parseInt(this._digits); if (parsed <= this._doc.song.patternsPerChannel) { - this._setBarPattern(parsed); + this._setPattern(parsed); return; } @@ -263,7 +263,7 @@ namespace beepbox { if (this._doc.channel == channel && this._doc.bar == bar) { const up: boolean = (this._mouseY % this._channelHeight) < this._channelHeight / 2; const patternCount: number = this._doc.song.patternsPerChannel; - this._setBarPattern((this._doc.song.channelBars[channel][bar] + (up ? 1 : patternCount)) % (patternCount + 1)); + this._setPattern((this._doc.song.channelBars[channel][bar] + (up ? 1 : patternCount)) % (patternCount + 1)); } else { this._setChannelBar(channel, bar); } @@ -403,7 +403,7 @@ namespace beepbox { for (let j: number = 0; j < this._doc.song.getChannelCount(); j++) { for (let i: number = 0; i < this._renderedBarCount; i++) { - const pattern: BarPattern | null = this._doc.song.getPattern(j, i); + const pattern: Pattern | null = this._doc.song.getPattern(j, i); const selected: boolean = (i == this._doc.bar && j == this._doc.channel); const dim: boolean = (pattern == null || pattern.notes.length == 0); diff --git a/ts/changes.ts b/ts/changes.ts index 22667374..4f8929a3 100644 --- a/ts/changes.ts +++ b/ts/changes.ts @@ -112,7 +112,7 @@ namespace beepbox { } } - export class ChangeBarPattern extends Change { + export class ChangePattern extends Change { constructor(doc: SongDocument, public oldValue: number, newValue: number) { super(); if (newValue > doc.song.channelPatterns[doc.channel].length) throw new Error("invalid pattern"); @@ -162,7 +162,7 @@ namespace beepbox { constructor(doc: SongDocument, newPitchChannelCount: number, newDrumChannelCount: number) { super(); if (doc.song.pitchChannelCount != newPitchChannelCount || doc.song.drumChannelCount != newDrumChannelCount) { - const channelPatterns: BarPattern[][] = []; + const channelPatterns: Pattern[][] = []; const channelBars: number[][] = []; const channelOctaves: number[] = []; const instrumentWaves: number[][] = []; @@ -187,7 +187,7 @@ namespace beepbox { instrumentVolumes[channel] = doc.song.instrumentVolumes[oldChannel]; } else { channelPatterns[channel] = []; - for (let j = 0; j < doc.song.patternsPerChannel; j++) channelPatterns[channel][j] = new BarPattern(); + for (let j = 0; j < doc.song.patternsPerChannel; j++) channelPatterns[channel][j] = new Pattern(); channelBars[channel] = filledArray(doc.song.barCount, 1); channelOctaves[channel] = 2; instrumentWaves[channel] = filledArray(doc.song.instrumentsPerChannel, 1); @@ -214,7 +214,7 @@ namespace beepbox { instrumentVolumes[channel] = doc.song.instrumentVolumes[oldChannel]; } else { channelPatterns[channel] = []; - for (let j = 0; j < doc.song.patternsPerChannel; j++) channelPatterns[channel][j] = new BarPattern(); + for (let j = 0; j < doc.song.patternsPerChannel; j++) channelPatterns[channel][j] = new Pattern(); channelBars[channel] = filledArray(doc.song.barCount, 1); channelOctaves[channel] = 0; instrumentWaves[channel] = filledArray(doc.song.instrumentsPerChannel, 1); @@ -412,11 +412,11 @@ namespace beepbox { export class ChangePitchAdded extends UndoableChange { private _doc: SongDocument; - private _pattern: BarPattern; + private _pattern: Pattern; private _note: Note; private _pitch: number; private _index: number; - constructor(doc: SongDocument, pattern: BarPattern, note: Note, pitch: number, index: number, deletion: boolean = false) { + constructor(doc: SongDocument, pattern: Pattern, note: Note, pitch: number, index: number, deletion: boolean = false) { super(deletion); this._doc = doc; this._pattern = pattern; @@ -464,7 +464,7 @@ namespace beepbox { } export class ChangePaste extends ChangeGroup { - constructor(doc: SongDocument, pattern: BarPattern, notes: Note[], newBeatsPerBar: number, newPartsPerBeat: number) { + constructor(doc: SongDocument, pattern: Pattern, notes: Note[], newBeatsPerBar: number, newPartsPerBeat: number) { super(); pattern.notes = notes; @@ -482,7 +482,7 @@ namespace beepbox { } export class ChangePatternInstrument extends Change { - constructor(doc: SongDocument, newValue: number, pattern: BarPattern) { + constructor(doc: SongDocument, newValue: number, pattern: Pattern) { super(); if (pattern.instrument != newValue) { pattern.instrument = newValue; @@ -498,12 +498,12 @@ namespace beepbox { if (doc.song.patternsPerChannel != newValue) { for (let i: number = 0; i < doc.song.getChannelCount(); i++) { const channelBars: number[] = doc.song.channelBars[i]; - const channelPatterns: BarPattern[] = doc.song.channelPatterns[i]; + const channelPatterns: Pattern[] = doc.song.channelPatterns[i]; for (let j: number = 0; j < channelBars.length; j++) { if (channelBars[j] > newValue) channelBars[j] = 0; } for (let j: number = channelPatterns.length; j < newValue; j++) { - channelPatterns[j] = new BarPattern(); + channelPatterns[j] = new Pattern(); } channelPatterns.length = newValue; } @@ -619,7 +619,7 @@ namespace beepbox { } export class ChangeRhythm extends ChangeSequence { - constructor(doc: SongDocument, bar: BarPattern, oldPartsPerBeat: number, newPartsPerBeat: number) { + constructor(doc: SongDocument, bar: Pattern, oldPartsPerBeat: number, newPartsPerBeat: number) { super(); let changeRhythm: (oldTime:number)=>number; @@ -699,13 +699,13 @@ namespace beepbox { export class ChangeNoteAdded extends UndoableChange { private _doc: SongDocument; - private _bar: BarPattern; + private _pattern: Pattern; private _note: Note; private _index: number; - constructor(doc: SongDocument, bar: BarPattern, note: Note, index: number, deletion: boolean = false) { + constructor(doc: SongDocument, pattern: Pattern, note: Note, index: number, deletion: boolean = false) { super(deletion); this._doc = doc; - this._bar = bar; + this._pattern = pattern; this._note = note; this._index = index; this._didSomething(); @@ -713,12 +713,12 @@ namespace beepbox { } protected _doForwards(): void { - this._bar.notes.splice(this._index, 0, this._note); + this._pattern.notes.splice(this._index, 0, this._note); this._doc.notifier.changed(); } protected _doBackwards(): void { - this._bar.notes.splice(this._index, 1); + this._pattern.notes.splice(this._index, 1); this._doc.notifier.changed(); } } @@ -762,7 +762,7 @@ namespace beepbox { } export class ChangeNoteTruncate extends ChangeSequence { - constructor(doc: SongDocument, bar: BarPattern, start: number, end: number, skipNote?: Note) { + constructor(doc: SongDocument, bar: Pattern, start: number, end: number, skipNote?: Note) { super(); let i: number = 0; while (i < bar.notes.length) { @@ -901,7 +901,7 @@ namespace beepbox { } export class ChangeTranspose extends ChangeSequence { - constructor(doc: SongDocument, bar: BarPattern, upward: boolean) { + constructor(doc: SongDocument, bar: Pattern, upward: boolean) { super(); for (let i: number = 0; i < bar.notes.length; i++) { this.append(new ChangeTransposeNote(doc, bar.notes[i], upward)); @@ -920,14 +920,12 @@ namespace beepbox { export class ChangeVolumeBend extends UndoableChange { private _doc: SongDocument; - private _bar: BarPattern; private _note: Note; private _oldPins: NotePin[]; private _newPins: NotePin[]; - constructor(doc: SongDocument, bar: BarPattern, note: Note, bendPart: number, bendVolume: number, bendInterval: number) { + constructor(doc: SongDocument, note: Note, bendPart: number, bendVolume: number, bendInterval: number) { super(false); this._doc = doc; - this._bar = bar; this._note = note; this._oldPins = note.pins; this._newPins = []; diff --git a/ts/synth.ts b/ts/synth.ts index 48511734..7f47e496 100644 --- a/ts/synth.ts +++ b/ts/synth.ts @@ -448,7 +448,7 @@ namespace beepbox { }; } - export class BarPattern { + export class Pattern { public notes: Note[]; public instrument: number; constructor() { @@ -491,7 +491,7 @@ namespace beepbox { public loopLength: number; public pitchChannelCount: number; public drumChannelCount: number; - public channelPatterns: BarPattern[][]; + public channelPatterns: Pattern[][]; public channelBars: number[][]; public channelOctaves: number[]; public instrumentWaves: number[][]; @@ -532,10 +532,10 @@ namespace beepbox { public initToDefault(): void { this.channelPatterns = [ - [new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern()], - [new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern()], - [new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern()], - [new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern(), new BarPattern()], + [new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern()], + [new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern()], + [new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern()], + [new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern(), new Pattern()], ]; this.channelBars = [ [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], @@ -1007,7 +1007,7 @@ namespace beepbox { recentPitches[i] += octaveOffset; } for (let i: number = 0; i < this.patternsPerChannel; i++) { - const newPattern: BarPattern | null = new BarPattern(); + const newPattern: Pattern | null = new Pattern(); newPattern.instrument = bits.read(neededInstrumentBits); this.channelPatterns[channel][i] = newPattern; @@ -1367,7 +1367,7 @@ namespace beepbox { } for (let i: number = 0; i < this.patternsPerChannel; i++) { - const pattern: BarPattern = new BarPattern(); + const pattern: Pattern = new Pattern(); this.channelPatterns[channel][i] = pattern; let patternObject: any = undefined; @@ -1482,14 +1482,14 @@ namespace beepbox { } } - public getPattern(channel: number, bar: number): BarPattern | null { + public getPattern(channel: number, bar: number): Pattern | null { const patternIndex: number = this.channelBars[channel][bar]; if (patternIndex == 0) return null; return this.channelPatterns[channel][patternIndex - 1]; } public getPatternInstrument(channel: number, bar: number): number { - const pattern: BarPattern | null = this.getPattern(channel, bar); + const pattern: Pattern | null = this.getPattern(channel, bar); return pattern == null ? 0 : pattern.instrument; } @@ -1654,7 +1654,8 @@ namespace beepbox { this.snapToBar(); } - public snapToBar(): void { + public snapToBar(bar?: number): void { + if (bar !== undefined) this.bar = bar; this.playheadInternal = this.bar; this.beat = 0; this.part = 0; @@ -1742,7 +1743,7 @@ namespace beepbox { } private static computeChannelInstrument(synth: Synth, song: Song, channel: number, time: number, sampleTime: number, samplesPerArpeggio: number, samples: number, isDrum: boolean) { - const pattern: BarPattern | null = song.getPattern(channel, synth.bar); + const pattern: Pattern | null = song.getPattern(channel, synth.bar); const envelope: number = pattern == null ? 0 : song.instrumentEnvelopes[channel][pattern.instrument];