├── .config └── dotnet-tools.json ├── .editorconfig ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── Content ├── Content.mgcb ├── OpenSans-SemiBold.ttf ├── WaveTracker.ttf ├── high_resolution_font.ttf ├── img.png ├── wavetracker_font.spritefont └── wt_logo.ico ├── Icon.ico ├── LICENSE ├── README.md ├── Source ├── App.cs ├── Audio │ ├── AudioEngine.cs │ ├── Channel.cs │ ├── ChannelManager.cs │ ├── EnvelopePlayer.cs │ ├── StereoBiQuadFilter.cs │ └── TickEvent.cs ├── Helpers.cs ├── Input │ ├── Input.cs │ ├── KeyboardShortcut.cs │ ├── PianoInput.cs │ ├── SystemClipboard.cs │ └── Tooltip.cs ├── Program.cs ├── Rendering │ ├── Colors.cs │ ├── Graphics.cs │ └── HSLColor.cs ├── SettingsProfile.cs ├── Tracker │ ├── CursorPos.cs │ ├── Envelope.cs │ ├── Instrument.cs │ ├── NoiseInstrument.cs │ ├── PatternEditorPosition.cs │ ├── PatternEditorState.cs │ ├── PatternSelection.cs │ ├── Playback.cs │ ├── Sample.cs │ ├── SampleInstrument.cs │ ├── SaveLoad.cs │ ├── WTFrame.cs │ ├── WTModule.cs │ ├── WTPattern.cs │ ├── WTSong.cs │ ├── Wave.cs │ └── WaveInstrument.cs └── UI │ ├── Button.cs │ ├── ButtonColors.cs │ ├── ChannelHeader.cs │ ├── Checkbox.cs │ ├── CheckboxLabeled.cs │ ├── Clickable.cs │ ├── ColorButton.cs │ ├── ColorButtonList.cs │ ├── ColorPickerDialog.cs │ ├── ConfigurationDialog.cs │ ├── ContextMenu.cs │ ├── Dialog.cs │ ├── Dialogs.cs │ ├── Dropdown.cs │ ├── DropdownButton.cs │ ├── EditSettings.cs │ ├── Element.cs │ ├── EnvelopeEditor.cs │ ├── EnvelopeListBox.cs │ ├── EnvelopeListItem.cs │ ├── ExportDialog.cs │ ├── ExportingDialog.cs │ ├── FrameButton.cs │ ├── FramesPanel.cs │ ├── HorizontalSlider.cs │ ├── HumanizeDialog.cs │ ├── InputField.cs │ ├── InstrumentBank.cs │ ├── InstrumentEditor.cs │ ├── KeyboardBindingList.cs │ ├── ListBox.cs │ ├── Menu.cs │ ├── MenuItemBase.cs │ ├── MenuOption.cs │ ├── MenuStrip.cs │ ├── MessageDialog.cs │ ├── ModulePanel.cs │ ├── ModuleSettingsDialog.cs │ ├── MouseRegion.cs │ ├── NumberBox.cs │ ├── NumberBoxDecimal.cs │ ├── Panel.cs │ ├── PatternEditor.cs │ ├── PreviewPiano.cs │ ├── SampleAmplifyDialog.cs │ ├── SampleBitcrushDialog.cs │ ├── SampleBrowser.cs │ ├── SampleEditor.cs │ ├── SampleModifyDialog.cs │ ├── Scrollbar.cs │ ├── ScrollbarHorizontal.cs │ ├── SetFramePatternDialog.cs │ ├── SpriteButton.cs │ ├── SpriteToggle.cs │ ├── SpriteToggleTwoSided.cs │ ├── StretchDialog.cs │ ├── SubMenu.cs │ ├── SwitchToggle.cs │ ├── SwitchToggleCustom.cs │ ├── TabGroup.cs │ ├── Textbox.cs │ ├── Toggle.cs │ ├── Toolbar.cs │ ├── UISoundsManager.cs │ ├── VerticalListSelector.cs │ ├── Visualizer.cs │ ├── WaveAddFuzzDialog.cs │ ├── WaveBank.cs │ ├── WaveBankElement.cs │ ├── WaveEditor.cs │ ├── WaveMathExpressionDialog.cs │ ├── WaveModifyDialog.cs │ ├── WaveSampleAndHoldDialog.cs │ ├── WaveSmoothDialog.cs │ ├── WaveSyncDialog.cs │ └── Window.cs ├── WaveTracker.csproj ├── WaveTracker.sln ├── app.manifest └── wavetracker.png /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-mgcb": { 6 | "version": "3.8.1.303", 7 | "commands": [ 8 | "mgcb" 9 | ] 10 | }, 11 | "dotnet-mgcb-editor": { 12 | "version": "3.8.1.303", 13 | "commands": [ 14 | "mgcb-editor" 15 | ] 16 | }, 17 | "dotnet-mgcb-editor-linux": { 18 | "version": "3.8.1.303", 19 | "commands": [ 20 | "mgcb-editor-linux" 21 | ] 22 | }, 23 | "dotnet-mgcb-editor-windows": { 24 | "version": "3.8.1.303", 25 | "commands": [ 26 | "mgcb-editor-windows" 27 | ] 28 | }, 29 | "dotnet-mgcb-editor-mac": { 30 | "version": "3.8.1.303", 31 | "commands": [ 32 | "mgcb-editor-mac" 33 | ] 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug in WaveTracker 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: 28 | - Version: 29 | 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for WaveTracker 4 | title: "[Feature Request]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /Content/Content.mgcb: -------------------------------------------------------------------------------- 1 | 2 | #----------------------------- Global Properties ----------------------------# 3 | 4 | /outputDir:bin/$(Platform) 5 | /intermediateDir:obj/$(Platform) 6 | /platform:DesktopGL 7 | /config: 8 | /profile:Reach 9 | /compress:False 10 | 11 | #-------------------------------- References --------------------------------# 12 | 13 | 14 | #---------------------------------- Content ---------------------------------# 15 | 16 | #begin high_resolution_font.ttf 17 | /copy:high_resolution_font.ttf 18 | 19 | #begin img.png 20 | /importer:TextureImporter 21 | /processor:TextureProcessor 22 | /processorParam:ColorKeyColor=255,0,255,255 23 | /processorParam:ColorKeyEnabled=True 24 | /processorParam:GenerateMipmaps=False 25 | /processorParam:PremultiplyAlpha=True 26 | /processorParam:ResizeToPowerOfTwo=False 27 | /processorParam:MakeSquare=False 28 | /processorParam:TextureFormat=Color 29 | /build:img.png 30 | 31 | #begin wavetracker_font.spritefont 32 | /importer:FontDescriptionImporter 33 | /processor:FontDescriptionProcessor 34 | /processorParam:PremultiplyAlpha=True 35 | /processorParam:TextureFormat=Compressed 36 | /build:wavetracker_font.spritefont 37 | 38 | -------------------------------------------------------------------------------- /Content/OpenSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squiggythings/WaveTracker/50f619586c185f52ad03d74b264144909d5166f5/Content/OpenSans-SemiBold.ttf -------------------------------------------------------------------------------- /Content/WaveTracker.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squiggythings/WaveTracker/50f619586c185f52ad03d74b264144909d5166f5/Content/WaveTracker.ttf -------------------------------------------------------------------------------- /Content/high_resolution_font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squiggythings/WaveTracker/50f619586c185f52ad03d74b264144909d5166f5/Content/high_resolution_font.ttf -------------------------------------------------------------------------------- /Content/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squiggythings/WaveTracker/50f619586c185f52ad03d74b264144909d5166f5/Content/img.png -------------------------------------------------------------------------------- /Content/wavetracker_font.spritefont: -------------------------------------------------------------------------------- 1 |  2 | 8 | 9 | 10 | 11 | 14 | WaveTracker.ttf 15 | 16 | 20 | 8 21 | 22 | 26 | 0 27 | 28 | 32 | true 33 | 34 | 38 | 39 | 40 | 44 | 45 | 46 | 53 | 54 | 55 | 56 | 翿 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Content/wt_logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squiggythings/WaveTracker/50f619586c185f52ad03d74b264144909d5166f5/Content/wt_logo.ico -------------------------------------------------------------------------------- /Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squiggythings/WaveTracker/50f619586c185f52ad03d74b264144909d5166f5/Icon.ico -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 squiggythings/Elias Ananiadis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WaveTracker 2 | 3 |
wavetracker
4 | 5 | \ 6 | [WaveTracker](https://www.wavetracker.org) is a free and open source music-making software for Windows. It uses basic wavetable synthesis and sampling to generate sounds, with endless combinations of effects to warp, modify or distort waves and sounds. 7 | 8 | **If you are looking to download WaveTracker, visit the [itch.io](https://squiggygames.itch.io/wavetracker) page.** 9 | 10 | ### Features 11 | 12 | - Realtime playback and editing 13 | - Draw and edit up to 100 waveforms 14 | - Up to 100 instrument macros to control various aspects of the sound 15 | - Sample importing and editing 16 | - MIDI input 17 | - Export to .wav 18 | - Built in oscilloscope and piano roll visualizer 19 | 20 | For an in depth explanation of how to use WaveTracker, visit the [official documentation](https://www.wavetracker.org/documentation) 21 | 22 | WaveTracker and its source code are licensed under the MIT license. 23 | 24 | ### Links 25 | 26 | - [wavetracker.org](https://www.wavetracker.org) 27 | - [documentation](https://www.wavetracker.org/documentation) 28 | - [itch.io page](https://squiggygames.itch.io/wavetracker) 29 | 30 | ## Contributing 31 | 32 | First, thank you so much for wanting to contribute to WaveTracker! If you would like to contribute I suggest opening a feature request or getting in contact with me first so we can discuss and come up with a roadmap, this way we avoid wasting both our time. 33 | 34 | ## Contact 35 | 36 | If you would like to report a bug or suggest a feature please open an issue here or contact me: 37 | - Twitter: [@squiggythings](https://twitter.com/squiggythings) 38 | - YouTube: [@squiggythings](https://www.youtube.com/channel/UCrNoYf6XA4IHLf-1ZeqN81g?view_as=subscriber) 39 | - Itch.io: [squiggy](https://squiggygames.itch.io/wavetracker) 40 | - E-mail: squiggymakesmusic@gmail.com 41 | -------------------------------------------------------------------------------- /Source/Audio/EnvelopePlayer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using WaveTracker.Tracker; 3 | 4 | namespace WaveTracker.Audio { 5 | public class EnvelopePlayer { 6 | public bool Enabled { get; set; } 7 | public int step; 8 | public bool released; 9 | public int Value { get; private set; } 10 | public Envelope EnvelopeToPlay { get; private set; } 11 | public bool EnvelopeEnded { get; private set; } 12 | public Envelope.EnvelopeType Type { get; private set; } 13 | 14 | public EnvelopePlayer(Envelope.EnvelopeType type) { 15 | step = 0; 16 | Type = type; 17 | released = false; 18 | EnvelopeToPlay = new Envelope(0); 19 | EnvelopeEnded = true; 20 | } 21 | 22 | public void SetEnvelope(Envelope envelope) { 23 | if (envelope == null) { 24 | EnvelopeToPlay = null; 25 | Evaluate(); 26 | return; 27 | } 28 | if (envelope.Type != Type) { 29 | throw new ArgumentException("Envelope type and player type do not match!"); 30 | } 31 | else { 32 | EnvelopeToPlay = envelope; 33 | Evaluate(); 34 | } 35 | } 36 | 37 | public bool HasActiveEnvelopeData { 38 | get { 39 | return EnvelopeToPlay != null && EnvelopeToPlay.Length > 0 && EnvelopeToPlay.IsActive; 40 | } 41 | } 42 | 43 | public void Start() { 44 | step = 0; 45 | EnvelopeEnded = false; 46 | released = false; 47 | Evaluate(); 48 | } 49 | 50 | public void Release() { 51 | released = true; 52 | if (EnvelopeToPlay.HasRelease) { 53 | step = EnvelopeToPlay.ReleaseIndex + 1; 54 | Evaluate(); 55 | } 56 | } 57 | 58 | private int GetDefaultValue() { 59 | return Type switch { 60 | Envelope.EnvelopeType.Volume => 99, 61 | _ => 0, 62 | }; 63 | } 64 | 65 | public void Step() { 66 | if (EnvelopeToPlay.IsActive && EnvelopeToPlay.Length > 0) { 67 | step++; 68 | if (EnvelopeToPlay.HasRelease) { 69 | if (step > EnvelopeToPlay.ReleaseIndex && !released) { 70 | if (EnvelopeToPlay.ReleaseIndex <= EnvelopeToPlay.LoopIndex || !EnvelopeToPlay.HasLoop) { 71 | step = EnvelopeToPlay.ReleaseIndex; 72 | } 73 | } 74 | if (EnvelopeToPlay.HasLoop) { 75 | if (EnvelopeToPlay.ReleaseIndex >= EnvelopeToPlay.LoopIndex) { 76 | if (step > EnvelopeToPlay.ReleaseIndex && !released) { 77 | step = EnvelopeToPlay.LoopIndex; 78 | } 79 | } 80 | else { 81 | if (step >= EnvelopeToPlay.Length) { 82 | step = EnvelopeToPlay.LoopIndex; 83 | } 84 | } 85 | } 86 | 87 | } 88 | else { 89 | // no release 90 | if (EnvelopeToPlay.HasLoop) { 91 | if (step >= EnvelopeToPlay.Length) { 92 | step = EnvelopeToPlay.LoopIndex; 93 | } 94 | } 95 | } 96 | if (step >= EnvelopeToPlay.Length) { 97 | EnvelopeEnded = true; 98 | step = EnvelopeToPlay.Length - 1; 99 | } 100 | else { 101 | EnvelopeEnded = false; 102 | } 103 | } 104 | Evaluate(); 105 | } 106 | 107 | private void Evaluate() { 108 | if (EnvelopeToPlay == null) { 109 | Value = GetDefaultValue(); 110 | } 111 | else if (!EnvelopeToPlay.IsActive) { 112 | Value = GetDefaultValue(); 113 | } 114 | else if (step >= 0 && step < EnvelopeToPlay.values.Length) { 115 | try { 116 | Value = EnvelopeToPlay.values[step]; 117 | } catch { 118 | Value = GetDefaultValue(); 119 | } 120 | } 121 | else { 122 | Value = GetDefaultValue(); 123 | } 124 | } 125 | 126 | public string GetState() { 127 | return Value + " " + step + " "; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Source/Audio/StereoBiQuadFilter.cs: -------------------------------------------------------------------------------- 1 | using NAudio.Dsp; 2 | 3 | namespace WaveTracker.Audio { 4 | public class StereoBiQuadFilter { 5 | private BiQuadFilter filterL; 6 | private BiQuadFilter filterR; 7 | 8 | public StereoBiQuadFilter() { 9 | filterL = BiQuadFilter.LowPassFilter(AudioEngine.TrueSampleRate, AudioEngine.SampleRate / 2f, 1); 10 | filterR = BiQuadFilter.LowPassFilter(AudioEngine.TrueSampleRate, AudioEngine.SampleRate / 2f, 1); 11 | } 12 | 13 | public void SetLowpassFilter(float sampleRate, float cutoffFrequency, float q) { 14 | filterL.SetLowPassFilter(sampleRate, cutoffFrequency, q); 15 | filterR.SetLowPassFilter(sampleRate, cutoffFrequency, q); 16 | } 17 | 18 | public void Transform(float inputL, float inputR, out float outputL, out float outputR) { 19 | outputL = filterL.Transform(inputL); 20 | outputR = filterR.Transform(inputR); 21 | } 22 | 23 | public float TransformL(float input) { 24 | return filterL.Transform(input); 25 | } 26 | public float TransformR(float input) { 27 | return filterR.Transform(input); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Source/Audio/TickEvent.cs: -------------------------------------------------------------------------------- 1 | namespace WaveTracker.Audio { 2 | public class TickEvent { 3 | public TickEventType eventType; 4 | public int value; 5 | public int value2; 6 | public int countdown; 7 | 8 | public TickEvent(TickEventType eventType, int val, int val2, int timer) { 9 | this.eventType = eventType; 10 | value = val; 11 | value2 = val2; 12 | countdown = timer; 13 | } 14 | 15 | public void Update() { 16 | countdown--; 17 | } 18 | } 19 | public enum TickEventType { 20 | Note, Instrument, Volume, Effect 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Source/Input/KeyboardShortcut.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework.Input; 2 | using System; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace WaveTracker { 6 | /// 7 | /// Structure containing a key and a key modifier 8 | /// 9 | [Serializable] 10 | public struct KeyboardShortcut { 11 | [JsonRequired] 12 | public Keys Key { get; set; } 13 | [JsonRequired] 14 | public KeyModifier Modifier { get; set; } 15 | public KeyboardShortcut(Keys key, KeyModifier modifier) { 16 | Key = key; 17 | Modifier = modifier; 18 | } 19 | public KeyboardShortcut(Keys key) { 20 | Key = key; 21 | Modifier = KeyModifier.None; 22 | } 23 | public KeyboardShortcut() { 24 | Key = Keys.None; 25 | Modifier = KeyModifier.None; 26 | } 27 | [JsonIgnore] 28 | public bool IsPressed { 29 | get { return Input.GetKey(Key, Modifier); } 30 | } 31 | [JsonIgnore] 32 | public bool IsPressedDown { 33 | get { return Input.GetKeyDown(Key, Modifier); } 34 | } 35 | [JsonIgnore] 36 | public bool IsPressedRepeat { 37 | get { return Input.GetKeyRepeat(Key, Modifier); } 38 | } 39 | [JsonIgnore] 40 | public bool WasReleasedThisFrame { 41 | get { return Input.GetKeyUp(Key, Modifier); } 42 | } 43 | 44 | public override string ToString() { 45 | return Key == Keys.None ? "(none)" : Helpers.ModifierToString(Modifier) + Helpers.KeyToString(Key); 46 | } 47 | 48 | public static bool operator ==(KeyboardShortcut one, KeyboardShortcut two) { 49 | return one.Key == two.Key && one.Modifier == two.Modifier; 50 | } 51 | public static bool operator !=(KeyboardShortcut one, KeyboardShortcut two) { 52 | return one.Key != two.Key || one.Modifier != two.Modifier; 53 | } 54 | 55 | public override bool Equals(object obj) { 56 | if (obj is KeyboardShortcut other) { 57 | if (other.Key == Key && other.Modifier == Modifier) { 58 | return true; 59 | } 60 | } 61 | return false; 62 | } 63 | 64 | public override int GetHashCode() { 65 | return (Key, Modifier).GetHashCode(); 66 | } 67 | 68 | public static KeyboardShortcut None { get { return new KeyboardShortcut(Keys.None, KeyModifier.None); } } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Source/Input/SystemClipboard.cs: -------------------------------------------------------------------------------- 1 | using TextCopy; 2 | 3 | namespace WaveTracker { 4 | /// 5 | /// Wrapper class for accessing multiple platform's clipboards 6 | /// 7 | public static class SystemClipboard { 8 | public static void SetText(string text) { 9 | ClipboardService.SetText(text); 10 | } 11 | public static string GetText() { 12 | return ClipboardService.GetText() ?? ""; 13 | } 14 | public static bool HasText() { 15 | return ClipboardService.GetText() != null; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Source/Input/Tooltip.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using WaveTracker.Rendering; 3 | using WaveTracker.Tracker; 4 | 5 | namespace WaveTracker { 6 | public static class Tooltip { 7 | private static int hoverTime; 8 | private static int elapsedMS; 9 | private static int px, py; 10 | private static string lasttooltip; 11 | private static bool show; 12 | public static string TooltipText { get; set; } 13 | public static string TooltipTextLong { get; set; } 14 | 15 | public static char LastEffect { get; set; } 16 | 17 | public static void Update() { 18 | TooltipText = ""; 19 | TooltipTextLong = ""; 20 | 21 | elapsedMS = App.GameTime.ElapsedGameTime.Milliseconds; 22 | 23 | } 24 | 25 | /// 26 | /// Write bottom info bar 27 | /// 28 | private static void WriteInfo() { 29 | int y = App.WindowHeight - 8; 30 | // draw bar 31 | Graphics.DrawRect(0, y, App.WindowWidth, 9, new Color(230, 230, 230, 255)); 32 | int x = App.WindowWidth - 40; 33 | Color textColor = UIColors.label; 34 | Graphics.WriteMonospaced(Playback.PlaybackTime.Minute.ToString("D2") + ":", x, y, textColor, 4); 35 | x += 12; 36 | Graphics.WriteMonospaced(Playback.PlaybackTime.Second.ToString("D2") + ":", x, y, textColor, 4); 37 | x += 12; 38 | Graphics.WriteMonospaced((Playback.PlaybackTime.Millisecond / 10).ToString("D2"), x, y, textColor, 4); 39 | x = App.WindowWidth - 100; 40 | 41 | float ticksPerBeat = (float)Playback.CurrentTicksPerRow * App.CurrentSong.RowHighlightSecondary; 42 | float ticksPerMinute = (float)App.CurrentModule.TickRate * 60; 43 | float BPM = ticksPerMinute / ticksPerBeat; 44 | 45 | Graphics.Write(BPM.ToString("0.00") + " BPM", x, y, textColor); 46 | 47 | x -= 46; 48 | Graphics.Write("Speed " + Playback.CurrentTicksPerRow, x, y, textColor); 49 | x -= 46; 50 | Graphics.Write(App.CurrentModule.TickRate + " Hz", x, y, textColor); 51 | if (TooltipTextLong != "" && TooltipTextLong != null) { 52 | Graphics.Write(TooltipTextLong, 2, y, new Color(58, 63, 94)); 53 | LastEffect = '\0'; 54 | } 55 | else if (LastEffect != '\0') { 56 | (string, string, string, string) effectDescription = Helpers.GetEffectDescription(LastEffect); 57 | if (effectDescription.Item1.Length > 0) { 58 | Graphics.Write(effectDescription.Item1 + " - " + effectDescription.Item2 + " " + effectDescription.Item3, 2, y, new Color(58, 63, 94)); 59 | } 60 | } 61 | 62 | } 63 | 64 | public static void Draw() { 65 | WriteInfo(); 66 | if (TooltipText != lasttooltip) { 67 | hoverTime = 0; 68 | lasttooltip = TooltipText; 69 | } 70 | 71 | if (TooltipText != "" && TooltipText != null) { 72 | hoverTime += elapsedMS; 73 | if (hoverTime > 500) { 74 | if (show == false) { 75 | px = Input.MousePositionX; 76 | py = Input.MousePositionY + 10; 77 | } 78 | show = true; 79 | int width = Helpers.GetWidthOfText(TooltipText) + 6; 80 | if (px + width + 1 > App.WindowWidth) { 81 | int diff = px + width + 1 - App.WindowWidth; 82 | px -= diff; 83 | } 84 | Graphics.DrawRect(px - 1, py - 1, width, 10, new Color(151, 156, 186)); 85 | 86 | Graphics.DrawRect(px, py, width - 2, 8, new Color(255, 255, 255)); 87 | Graphics.Write(TooltipText, px + 2, py, new Color(151, 156, 186)); 88 | } 89 | else { 90 | show = false; 91 | } 92 | } 93 | else { 94 | show = false; 95 | hoverTime = 0; 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Source/Program.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | 4 | namespace WaveTracker { 5 | 6 | /// 7 | /// The main class. 8 | /// 9 | public static class Program { 10 | /// 11 | /// The main entry point for the application. 12 | /// 13 | [STAThread] 14 | private static void Main(string[] args) { 15 | using (App game = new App(args)) { 16 | game.Run(); 17 | } 18 | 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Rendering/Graphics.cs: -------------------------------------------------------------------------------- 1 | using FontStashSharp; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Content; 4 | using Microsoft.Xna.Framework.Graphics; 5 | using System.IO; 6 | 7 | namespace WaveTracker.Rendering { 8 | public static class Graphics { 9 | public static SpriteBatch spriteBatch; 10 | public static SpriteFont defaultFont; 11 | 12 | public static readonly RasterizerState scissorRasterizer = new RasterizerState() { CullMode = CullMode.None, ScissorTestEnable = true }; 13 | public static FontSystem loadedFont; 14 | public static SpriteFontBase customFont; 15 | 16 | public static Texture2D pixel; 17 | public static Texture2D img; 18 | public static int Scale { get; set; } 19 | public static float fontScale = 2; 20 | public static float fontOffsetY = 2; 21 | 22 | public static readonly BlendState BlendState = new BlendState { 23 | ColorSourceBlend = Blend.SourceAlpha, 24 | AlphaSourceBlend = Blend.One, 25 | ColorDestinationBlend = Blend.InverseSourceAlpha, 26 | AlphaDestinationBlend = Blend.InverseSourceAlpha, 27 | }; 28 | 29 | public static readonly SamplerState SamplerState = SamplerState.PointClamp; 30 | public static readonly DepthStencilState DepthStencilState = DepthStencilState.None; 31 | public static bool IsUsingCustomFont { get; private set; } 32 | 33 | public static void Initialize(ContentManager content, GraphicsDevice device) { 34 | img = content.Load("img"); 35 | pixel = new Texture2D(device, 1, 1); 36 | pixel.SetData(new[] { Color.White }); 37 | spriteBatch = new SpriteBatch(device); 38 | defaultFont = content.Load("wavetracker_font"); 39 | 40 | IsUsingCustomFont = App.Settings.General.UseHighResolutionText; 41 | FontSystemSettings fontSettings = new FontSystemSettings(); 42 | fontSettings.GlyphRenderer = (input, output, options) => { 43 | int size = options.Size.X * options.Size.Y; 44 | for (int i = 0; i < size; i++) { 45 | int ci = i * 4; 46 | output[ci] = output[ci + 1] = output[ci + 2] = 255; // set all color to white (smoother antialiasing on colored text) 47 | output[ci + 3] = input[i]; // keep alpha the same 48 | } 49 | }; 50 | loadedFont = new FontSystem(fontSettings); 51 | try { 52 | string path = Path.Combine(content.RootDirectory, "high_resolution_font.ttf"); 53 | loadedFont.AddFont(File.ReadAllBytes(path)); 54 | } catch { 55 | IsUsingCustomFont = false; 56 | } 57 | SetFont(); 58 | } 59 | 60 | public static void SetFont() { 61 | if (IsUsingCustomFont) { 62 | customFont = loadedFont.GetFont(9 * App.Settings.General.ScreenScale); 63 | fontScale = 1; 64 | fontOffsetY = 1; 65 | } 66 | else { 67 | fontScale = App.Settings.General.ScreenScale; 68 | fontOffsetY = 2; 69 | } 70 | } 71 | 72 | public static void DrawRect(int x, int y, int width, int height, Color color) { 73 | spriteBatch.Draw(pixel, new Rectangle(x * Scale, y * Scale, width * Scale, height * Scale), color); 74 | } 75 | 76 | public static void Write(string text, int x, int y, Color c) { 77 | if (IsUsingCustomFont) { 78 | customFont.DrawText(spriteBatch, text, new Vector2(x * Scale, (y - fontOffsetY) * Scale), c); 79 | } 80 | else { 81 | spriteBatch.DrawString(defaultFont, text, new Vector2(x * Scale, (y - fontOffsetY) * Scale), c, 0, Vector2.Zero, fontScale, SpriteEffects.None, 0); 82 | } 83 | } 84 | 85 | /// 86 | /// Renders multicolored text with colors for each character 87 | /// 88 | /// 89 | /// 90 | /// 91 | /// 92 | public static void Write(string text, int x, int y, Color[] colors) { 93 | 94 | if (IsUsingCustomFont) { 95 | float xpos = x; 96 | for (int i = 0; i < colors.Length; i++) { 97 | customFont.DrawText(spriteBatch, text[i] + "", new Vector2(xpos * Scale, (y - fontOffsetY) * Scale), colors[i]); 98 | xpos += customFont.MeasureString(text[i] + "").X / Scale; 99 | } 100 | } 101 | else { 102 | for (int i = 0; i < colors.Length; i++) { 103 | spriteBatch.DrawString(defaultFont, text[i] + "", new Vector2(x * Scale, (y - fontOffsetY) * Scale), colors[i], 0, Vector2.Zero, fontScale, SpriteEffects.None, 0); 104 | x += (int)defaultFont.MeasureString(text[i] + "").X; 105 | } 106 | } 107 | } 108 | 109 | public static void WriteRightJustified(string text, int x, int y, Color c) { 110 | Write(text, x - Helpers.GetWidthOfText(text), y, c); 111 | } 112 | 113 | public static void WriteMonospaced(string text, int x, int y, Color c, int width = 5) { 114 | foreach (char ch in text) { 115 | Write(ch + "", x, y, c); 116 | x += width + 1; 117 | } 118 | } 119 | 120 | public static void DrawSprite(int x, int y, int width, int height, Rectangle bounds, Color col) { 121 | spriteBatch.Draw(img, new Rectangle(x * Scale, y * Scale, width * Scale, height * Scale), bounds, col); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Source/Rendering/HSLColor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace WaveTracker.Rendering { 4 | public struct HSLColor { 5 | /// 6 | /// Hue from 0.0-360.0 7 | /// 8 | public float H; 9 | /// 10 | /// Saturation from 0.0-1.0 11 | /// 12 | public float S; 13 | /// 14 | /// Lightness from 0.0-1.0 15 | /// 16 | public float L; 17 | /// 18 | /// Alpha from 0.0-1.0 19 | /// 20 | public float A; 21 | 22 | public HSLColor(float h, float s, float l, float a) { 23 | H = h; 24 | S = s; 25 | L = l; 26 | A = a; 27 | } 28 | 29 | public HSLColor(float h, float s, float l) { 30 | H = h; 31 | S = s; 32 | L = l; 33 | A = 1.0f; 34 | } 35 | 36 | public Color ToRGB() { 37 | byte r; 38 | byte g; 39 | byte b; 40 | if (S == 0) { 41 | r = g = b = (byte)(L * 255); 42 | } 43 | else { 44 | float v1, v2; 45 | float hue = H / 360; 46 | 47 | v2 = (L < 0.5) ? (L * (1 + S)) : (L + S - L * S); 48 | v1 = 2 * L - v2; 49 | 50 | r = (byte)(255 * HueToRGB(v1, v2, hue + 1.0f / 3)); 51 | g = (byte)(255 * HueToRGB(v1, v2, hue)); 52 | b = (byte)(255 * HueToRGB(v1, v2, hue - 1.0f / 3)); 53 | } 54 | 55 | return new Color(r, g, b, (byte)(A * 255)); 56 | } 57 | private static float HueToRGB(float v1, float v2, float vH) { 58 | if (vH < 0) { 59 | vH += 1; 60 | } 61 | 62 | if (vH > 1) { 63 | vH -= 1; 64 | } 65 | 66 | return 6 * vH < 1 ? v1 + (v2 - v1) * 6 * vH : 2 * vH < 1 ? v2 : 3 * vH < 2 ? v1 + (v2 - v1) * (2.0f / 3 - vH) * 6 : v1; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Source/Tracker/Instrument.cs: -------------------------------------------------------------------------------- 1 | using ProtoBuf; 2 | using System.Collections.Generic; 3 | 4 | namespace WaveTracker.Tracker { 5 | /// 6 | /// Base class for an instrument 7 | /// 8 | [ProtoContract] 9 | [ProtoInclude(10, typeof(WaveInstrument))] 10 | [ProtoInclude(20, typeof(SampleInstrument))] 11 | [ProtoInclude(30, typeof(NoiseInstrument))] 12 | public abstract class Instrument { 13 | [ProtoMember(1)] 14 | public string name; 15 | [ProtoMember(2)] 16 | public List envelopes; 17 | 18 | public Instrument() { 19 | name = "New Instrument"; 20 | envelopes = []; 21 | 22 | } 23 | 24 | public bool HasEnvelope(Envelope.EnvelopeType type) { 25 | foreach (Envelope envelope in envelopes) { 26 | if (envelope.Type == type) { 27 | return true; 28 | } 29 | } 30 | return false; 31 | } 32 | 33 | public abstract Instrument Clone(); 34 | public override string ToString() { 35 | return name; 36 | } 37 | public void SetName(string name) { 38 | this.name = name; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Source/Tracker/NoiseInstrument.cs: -------------------------------------------------------------------------------- 1 | namespace WaveTracker.Tracker { 2 | public class NoiseInstrument : Instrument { 3 | public NoiseInstrument() : base() { 4 | name = "New Noise Instrument"; 5 | } 6 | 7 | public override NoiseInstrument Clone() { 8 | NoiseInstrument m = new NoiseInstrument(); 9 | m.name = name; 10 | m.envelopes = []; 11 | foreach (Envelope envelope in envelopes) { 12 | m.envelopes.Add(envelope.Clone()); 13 | } 14 | return m; 15 | } 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /Source/Tracker/PatternEditorPosition.cs: -------------------------------------------------------------------------------- 1 | namespace WaveTracker.Tracker { 2 | /// 3 | /// Stores a snapshot of cursor positions and selection information 4 | /// 5 | public struct PatternEditorPosition { 6 | public CursorPos CursorPosition { get; private set; } 7 | public CursorPos SelectionStart { get; private set; } 8 | public CursorPos SelectionEnd { get; private set; } 9 | public bool SelectionIsActive { get; private set; } 10 | 11 | public PatternEditorPosition(CursorPos cursorPosition, PatternSelection patternSelection) { 12 | CursorPosition = cursorPosition; 13 | SelectionStart = patternSelection.min; 14 | SelectionEnd = patternSelection.max; 15 | SelectionIsActive = patternSelection.IsActive; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Source/Tracker/PatternEditorState.cs: -------------------------------------------------------------------------------- 1 | namespace WaveTracker.Tracker { 2 | 3 | public record PatternEditorState { 4 | private string[] patternStrings; 5 | /// 6 | /// The position before this action was committed 7 | /// 8 | public PatternEditorPosition PrePosition { get; private set; } 9 | /// 10 | /// The position after this action was committed 11 | /// 12 | public PatternEditorPosition PostPosition { get; private set; } 13 | 14 | /// 15 | /// The song's frameSequence 16 | /// 17 | private byte[] frameSequence; 18 | 19 | public PatternEditorState(WTSong song, PatternEditorPosition previous, PatternEditorPosition next) { 20 | patternStrings = song.ForcePackPatternsToStrings(); 21 | frameSequence = song.GetFrameSequenceAsByteArray(); 22 | PrePosition = previous; 23 | PostPosition = next; 24 | } 25 | 26 | /// 27 | /// Writes this state's data into song 28 | /// 29 | /// 30 | public void RestoreIntoSong(WTSong song) { 31 | song.UnpackFrameSequence(frameSequence); 32 | song.UnpackPatternsFromStrings(patternStrings); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Source/Tracker/PatternSelection.cs: -------------------------------------------------------------------------------- 1 | namespace WaveTracker.Tracker { 2 | public class PatternSelection { 3 | public WTSong Song { get; private set; } 4 | /// 5 | /// The top-left position of the selection rectangle 6 | /// 7 | public CursorPos min; 8 | /// 9 | /// The bottom-right position of the selection rectangle 10 | /// 11 | public CursorPos max; 12 | 13 | public int Width { get; private set; } 14 | public int Height { get; private set; } 15 | 16 | public bool IsActive { get; set; } 17 | 18 | public void Set(WTSong song, CursorPos startPos, CursorPos endPos) { 19 | Song = song; 20 | min = startPos; 21 | max = endPos; 22 | 23 | // perform any swaps necessary 24 | if (startPos.IsBelow(endPos)) { 25 | min.Row = endPos.Row; 26 | min.Frame = endPos.Frame; 27 | max.Row = startPos.Row; 28 | max.Frame = startPos.Frame; 29 | } 30 | if (startPos.IsRightOf(endPos)) { 31 | max.Channel = startPos.Channel; 32 | max.Column = startPos.Column; 33 | min.Channel = endPos.Channel; 34 | min.Column = endPos.Column; 35 | } 36 | 37 | // if the user selected on the row column, set the selection to span all channels 38 | if (max.Channel == -1 && min.Channel == -1) { 39 | min.Channel = 0; 40 | min.Column = 0; 41 | max.Channel = App.CurrentModule.ChannelCount - 1; 42 | max.Column = song.GetLastCursorColumnOfChannel(max.Channel); 43 | } 44 | 45 | // adjust the cursor positions to fill all of mulit-digit fields like instrument, volume and effects 46 | if (max.Column == CursorColumnType.Instrument1) { 47 | max.Column = CursorColumnType.Instrument2; 48 | } 49 | else if (max.Column == CursorColumnType.Volume1) { 50 | max.Column = CursorColumnType.Volume2; 51 | } 52 | else if (max.Column is CursorColumnType.Effect1 or CursorColumnType.Effect1Param1) { 53 | max.Column = CursorColumnType.Effect1Param2; 54 | } 55 | else if (max.Column is CursorColumnType.Effect2 or CursorColumnType.Effect2Param1) { 56 | max.Column = CursorColumnType.Effect2Param2; 57 | } 58 | else if (max.Column is CursorColumnType.Effect3 or CursorColumnType.Effect3Param1) { 59 | max.Column = CursorColumnType.Effect3Param2; 60 | } 61 | else if (max.Column is CursorColumnType.Effect4 or CursorColumnType.Effect4Param1) { 62 | max.Column = CursorColumnType.Effect4Param2; 63 | } 64 | 65 | if (min.Column == CursorColumnType.Instrument2) { 66 | min.Column = CursorColumnType.Instrument1; 67 | } 68 | else if (min.Column == CursorColumnType.Volume2) { 69 | min.Column = CursorColumnType.Volume1; 70 | } 71 | else if (min.Column is CursorColumnType.Effect1Param1 or CursorColumnType.Effect1Param2) { 72 | min.Column = CursorColumnType.Effect1; 73 | } 74 | else if (min.Column is CursorColumnType.Effect2Param1 or CursorColumnType.Effect2Param2) { 75 | min.Column = CursorColumnType.Effect2; 76 | } 77 | else if (min.Column is CursorColumnType.Effect3Param1 or CursorColumnType.Effect3Param2) { 78 | min.Column = CursorColumnType.Effect3; 79 | } 80 | else if (min.Column is CursorColumnType.Effect4Param1 or CursorColumnType.Effect4Param2) { 81 | min.Column = CursorColumnType.Effect4; 82 | } 83 | 84 | if (min.Channel < 0) { 85 | min.Channel = 0; 86 | } 87 | 88 | if (max.Channel < 0) { 89 | max.Channel = 0; 90 | } 91 | 92 | if (max.Column > song.GetLastCursorColumnOfChannel(max.Channel)) { 93 | max.Column = song.GetLastCursorColumnOfChannel(max.Channel); 94 | } 95 | 96 | Width = max.CellColumn - min.CellColumn + 1; 97 | Height = max.Row - min.Row + 1; 98 | } 99 | 100 | public PatternSelection(WTSong song, CursorPos startPos, CursorPos endPos) { 101 | Set(song, startPos, endPos); 102 | } 103 | 104 | public PatternSelection() { 105 | IsActive = false; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Source/Tracker/SampleInstrument.cs: -------------------------------------------------------------------------------- 1 | using ProtoBuf; 2 | 3 | namespace WaveTracker.Tracker { 4 | 5 | [ProtoContract] 6 | public class SampleInstrument : Instrument { 7 | [ProtoMember(21)] 8 | public Sample sample; 9 | 10 | public SampleInstrument() : base() { 11 | name = "New Sample Instrument"; 12 | sample = new Sample(); 13 | } 14 | 15 | public override SampleInstrument Clone() { 16 | SampleInstrument m = new SampleInstrument(); 17 | m.name = name; 18 | m.envelopes = []; 19 | foreach (Envelope envelope in envelopes) { 20 | m.envelopes.Add(envelope.Clone()); 21 | } 22 | m.sample.sampleDataL = new short[sample.sampleDataL.Length]; 23 | m.sample.sampleDataR = new short[sample.sampleDataR.Length]; 24 | for (int i = 0; i < sample.sampleDataL.Length; i++) { 25 | m.sample.sampleDataL[i] = sample.sampleDataL[i]; 26 | if (sample.sampleDataR.Length != 0) { 27 | m.sample.sampleDataR[i] = sample.sampleDataR[i]; 28 | } 29 | } 30 | 31 | m.sample.loopType = sample.loopType; 32 | m.sample.loopPoint = sample.loopPoint; 33 | m.sample.resampleMode = sample.resampleMode; 34 | m.sample.SetBaseKey(sample.BaseKey); 35 | m.sample.SetDetune(sample.Detune); 36 | m.sample.useInVisualization = sample.useInVisualization; 37 | return m; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Source/Tracker/WTFrame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WaveTracker.Tracker { 4 | /// 5 | /// Object in a song's frame sequence which determines what patterns to play. 6 | /// 7 | public class WTFrame { 8 | 9 | /// 10 | /// The index of the pattern this frame should reference 11 | /// 12 | private int patternIndex; 13 | 14 | /// 15 | /// The index of the pattern this frame should reference, automatically clamped between 0-99 16 | /// 17 | public int PatternIndex { 18 | get { return patternIndex; } 19 | set { 20 | if (patternIndex != value) { 21 | patternIndex = Math.Clamp(value, 0, 99); 22 | App.CurrentModule.SetDirty(); 23 | } 24 | } 25 | } 26 | 27 | /// 28 | /// The song that owns this frame 29 | /// 30 | public WTSong ParentSong { get; private set; } 31 | 32 | /// 33 | /// Returns the pattern this frame references 34 | /// 35 | /// 36 | public WTPattern GetPattern() { 37 | return ParentSong.Patterns[PatternIndex]; 38 | } 39 | 40 | /// 41 | /// Gets the length of this frame's pattern, taking into account Cxx, Bxx and Dxx commands 42 | /// 43 | /// 44 | public int GetLength() { 45 | return ParentSong.Patterns[PatternIndex].GetModifiedLength(); 46 | } 47 | 48 | /// 49 | /// Creates a new frame that holds a pattern index and a reference to song that holds it. 50 | /// 51 | /// 52 | /// 53 | public WTFrame(int index, WTSong parent) { 54 | PatternIndex = index; 55 | ParentSong = parent; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Source/Tracker/WaveInstrument.cs: -------------------------------------------------------------------------------- 1 | namespace WaveTracker.Tracker { 2 | public class WaveInstrument : Instrument { 3 | 4 | public WaveInstrument() : base() { 5 | name = "New Wave Instrument"; 6 | } 7 | 8 | public override WaveInstrument Clone() { 9 | WaveInstrument m = new WaveInstrument(); 10 | m.name = name; 11 | m.envelopes = []; 12 | foreach (Envelope envelope in envelopes) { 13 | m.envelopes.Add(envelope.Clone()); 14 | } 15 | return m; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Source/UI/Button.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace WaveTracker.UI { 4 | public class Button : Clickable { 5 | public string Label { get; private set; } 6 | public bool LabelIsCentered { get; set; } 7 | 8 | private int labelWidth; 9 | 10 | public Button(string label, int x, int y, Element parent) { 11 | enabled = true; 12 | this.x = x; 13 | this.y = y; 14 | Label = label; 15 | LabelIsCentered = true; 16 | width = Helpers.GetWidthOfText(label) + 8; 17 | labelWidth = Helpers.GetWidthOfText(label); 18 | height = 13; 19 | SetParent(parent); 20 | } 21 | public Button(string label, int x, int y, int width, Element parent) { 22 | enabled = true; 23 | this.x = x; 24 | this.y = y; 25 | Label = label; 26 | LabelIsCentered = true; 27 | this.width = width; 28 | labelWidth = Helpers.GetWidthOfText(label); 29 | height = 13; 30 | SetParent(parent); 31 | } 32 | 33 | public void SetLabel(string label) { 34 | Label = label; 35 | labelWidth = Helpers.GetWidthOfText(label); 36 | } 37 | 38 | private Color GetBackgroundColor() { 39 | if (IsPressed) { 40 | return ButtonColors.backgroundColorPressed; 41 | } 42 | else { 43 | if (IsHovered) { 44 | return ButtonColors.backgroundColorHover; 45 | } 46 | else { 47 | return ButtonColors.backgroundColor; 48 | } 49 | } 50 | } 51 | 52 | private Color GetTextColor() { 53 | if (IsPressed) { 54 | return ButtonColors.textColorPressed; 55 | } 56 | else { 57 | return ButtonColors.textColor; 58 | } 59 | } 60 | 61 | public void Draw() { 62 | if (enabled) { 63 | DrawRoundedRect(0, 0, width, height, GetBackgroundColor()); 64 | 65 | int textOffset = IsPressed ? 1 : 0; 66 | 67 | if (LabelIsCentered) { 68 | Write(Label, (width - labelWidth) / 2, (height + 1) / 2 - 4 + textOffset, GetTextColor()); 69 | } 70 | else { 71 | Write(Label, 4, (height + 1) / 2 - 4 + textOffset, GetTextColor()); 72 | } 73 | } 74 | else { 75 | DrawRoundedRect(0, 0, width, height, ButtonColors.backgroundColorDisabled); 76 | if (LabelIsCentered) { 77 | Write(Label, (width - labelWidth) / 2, (height + 1) / 2 - 4, ButtonColors.textColorDisabled); 78 | } 79 | else { 80 | Write(Label, 4, (height + 1) / 2 - 4, ButtonColors.textColorDisabled); 81 | } 82 | } 83 | 84 | } 85 | 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Source/UI/ButtonColors.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace WaveTracker.UI { 4 | public class ButtonColors { 5 | public static readonly Color backgroundColor = new Color(104, 111, 153); 6 | public static readonly Color backgroundColorHover = new Color(132, 138, 172); 7 | public static readonly Color backgroundColorPressed = new Color(8, 124, 232); 8 | public static readonly Color textColor = new Color(255, 255, 255); 9 | public static readonly Color textColorPressed = new Color(255, 255, 255); 10 | public static readonly Color textColorDisabled = new Color(255, 255, 255); 11 | public static readonly Color backgroundColorDisabled = new Color(192, 195, 212); 12 | public static readonly Color toggleBackgroundColor = new Color(32, 144, 246); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Source/UI/Checkbox.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace WaveTracker.UI { 4 | public class Checkbox : Clickable { 5 | public bool Value { get; set; } 6 | 7 | public bool ValueWasChangedInternally; 8 | 9 | public Checkbox(int x, int y, Element parent) { 10 | this.x = x; 11 | this.y = y; 12 | width = 9; 13 | height = 9; 14 | SetParent(parent); 15 | } 16 | 17 | public void Update() { 18 | if (Clicked) { 19 | Value = !Value; 20 | ValueWasChangedInternally = true; 21 | } 22 | else { 23 | ValueWasChangedInternally = false; 24 | } 25 | } 26 | 27 | private Rectangle GetBounds(int num) { 28 | return new Rectangle(400, 144 + num * 9, 9, 9); 29 | } 30 | 31 | public void DrawAsTabToggle() { 32 | if (Value) { 33 | DrawSprite(0, 0, GetBounds(3)); 34 | } 35 | else { 36 | DrawSprite(0, 0, GetBounds(6)); 37 | } 38 | } 39 | 40 | public void Draw(bool isHovered, bool isPressed) { 41 | if (enabled) { 42 | if (Value) { 43 | if (isHovered) { 44 | if (isPressed) { 45 | DrawSprite(0, 0, GetBounds(5)); 46 | } 47 | else { 48 | DrawSprite(0, 0, GetBounds(4)); 49 | } 50 | } 51 | else { 52 | DrawSprite(0, 0, GetBounds(3)); 53 | } 54 | } 55 | else { 56 | if (isHovered) { 57 | if (isPressed) { 58 | DrawSprite(0, 0, GetBounds(2)); 59 | } 60 | else { 61 | DrawSprite(0, 0, GetBounds(1)); 62 | } 63 | } 64 | else { 65 | DrawSprite(0, 0, GetBounds(0)); 66 | } 67 | } 68 | 69 | } 70 | else { 71 | if (Value) { 72 | DrawSprite(0, 0, GetBounds(7)); 73 | } 74 | else { 75 | DrawSprite(0, 0, GetBounds(6)); 76 | } 77 | } 78 | 79 | } 80 | 81 | public void Draw() { 82 | if (enabled) { 83 | if (Value) { 84 | if (IsHovered) { 85 | if (IsPressed) { 86 | DrawSprite(0, 0, GetBounds(5)); 87 | } 88 | else { 89 | DrawSprite(0, 0, GetBounds(4)); 90 | } 91 | } 92 | else { 93 | DrawSprite(0, 0, GetBounds(3)); 94 | } 95 | } 96 | else { 97 | if (IsHovered) { 98 | if (IsPressed) { 99 | DrawSprite(0, 0, GetBounds(2)); 100 | } 101 | else { 102 | DrawSprite(0, 0, GetBounds(1)); 103 | } 104 | } 105 | else { 106 | DrawSprite(0, 0, GetBounds(0)); 107 | } 108 | } 109 | 110 | } 111 | else { 112 | if (Value) { 113 | DrawSprite(0, 0, GetBounds(7)); 114 | } 115 | else { 116 | DrawSprite(0, 0, GetBounds(6)); 117 | } 118 | } 119 | 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Source/UI/CheckboxLabeled.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace WaveTracker.UI { 4 | public class CheckboxLabeled : Clickable { 5 | public bool Value { get; set; } 6 | 7 | private string label; 8 | public bool ShowCheckboxOnRight { get; set; } 9 | 10 | public CheckboxLabeled(string label, int x, int y, int width, Element parent) { 11 | this.x = x; 12 | this.y = y; 13 | this.width = width; 14 | this.label = label; 15 | height = 11; 16 | SetParent(parent); 17 | } 18 | 19 | public CheckboxLabeled(string label, int x, int y, Element parent) { 20 | this.x = x; 21 | this.y = y; 22 | width = 16 + Helpers.GetWidthOfText(label); 23 | this.label = label; 24 | height = 11; 25 | SetParent(parent); 26 | } 27 | 28 | public void Update() { 29 | if (Clicked) { 30 | Value = !Value; 31 | } 32 | } 33 | 34 | private Rectangle GetBounds(int num) { 35 | return new Rectangle(400, 144 + num * 9, 9, 9); 36 | } 37 | 38 | public void Draw() { 39 | int checkX = ShowCheckboxOnRight ? width - 9 : 0; 40 | if (enabled) { 41 | if (Value) { 42 | if (IsHovered) { 43 | if (IsPressed) { 44 | DrawSprite(checkX, (height - 9) / 2, GetBounds(5)); 45 | } 46 | else { 47 | DrawSprite(checkX, (height - 9) / 2, GetBounds(4)); 48 | } 49 | } 50 | else { 51 | DrawSprite(checkX, (height - 9) / 2, GetBounds(3)); 52 | } 53 | } 54 | else { 55 | if (IsHovered) { 56 | if (IsPressed) { 57 | DrawSprite(checkX, (height - 9) / 2, GetBounds(2)); 58 | } 59 | else { 60 | DrawSprite(checkX, (height - 9) / 2, GetBounds(1)); 61 | } 62 | } 63 | else { 64 | DrawSprite(checkX, (height - 9) / 2, GetBounds(0)); 65 | } 66 | } 67 | 68 | } 69 | else { 70 | if (Value) { 71 | DrawSprite(checkX, (height - 9) / 2, GetBounds(7)); 72 | } 73 | else { 74 | DrawSprite(checkX, (height - 9) / 2, GetBounds(6)); 75 | } 76 | } 77 | Color labelCol = UIColors.labelDark; 78 | if (IsHovered) { 79 | labelCol = Color.Black; 80 | } 81 | 82 | Write(label + "", ShowCheckboxOnRight ? 0 : 13, (height - 7) / 2, labelCol); 83 | } 84 | 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Source/UI/Clickable.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace WaveTracker.UI { 4 | public abstract class Clickable : Element { 5 | public int width; 6 | public int height; 7 | 8 | public int BoundsBottom { 9 | get { 10 | return y + height; 11 | } 12 | } 13 | 14 | public int BoundsRight { 15 | get { 16 | return x + width; 17 | } 18 | } 19 | 20 | public bool enabled = true; 21 | public string TooltipText { 22 | get; private set; 23 | } 24 | 25 | public string TooltipTextLong { get; private set; } 26 | 27 | public void SetTooltip(string ttshort, string ttlong) { 28 | TooltipText = ttshort; 29 | TooltipTextLong = ttlong; 30 | } 31 | public void SetTooltip(string ttshort) { 32 | TooltipText = ttshort; 33 | TooltipTextLong = ttshort; 34 | } 35 | 36 | public bool IsHovered { 37 | get { 38 | if (!InFocus) { 39 | return false; 40 | } 41 | 42 | if (MouseX < width && MouseY < height && MouseX >= 0 && MouseY >= 0) { 43 | if (TooltipTextLong != "") { 44 | Tooltip.TooltipTextLong = TooltipTextLong; 45 | } 46 | 47 | if (TooltipText != "") { 48 | Tooltip.TooltipText = TooltipText; 49 | } 50 | 51 | return true; 52 | } 53 | return false; 54 | } 55 | } 56 | 57 | /// 58 | /// Returns true if this element is hovered, regardless of if it is in focus or not 59 | /// 60 | public bool IsMouseOverRegion { 61 | get { 62 | return MouseX < width && MouseY < height && MouseX >= 0 && MouseY >= 0; 63 | } 64 | } 65 | public bool IsPressed { get { return IsHovered && Input.GetClick(KeyModifier._Any) && GlobalPointIsInBounds(Input.LastClickLocation) && IsMeOrAParent(Input.lastClickFocus); } } 66 | 67 | public bool Clicked { 68 | get { 69 | return enabled && IsHovered && Input.GetClickUp(KeyModifier._Any) && GlobalPointIsInBounds(Input.LastClickLocation) && GlobalPointIsInBounds(Input.LastClickReleaseLocation) && IsMeOrAParent(Input.lastClickFocus); 70 | } 71 | } 72 | 73 | public bool RightClicked { 74 | get { 75 | return enabled && IsHovered && Input.GetRightClickUp(KeyModifier._Any) && GlobalPointIsInBounds(Input.LastRightClickLocation) && GlobalPointIsInBounds(Input.LastRightClickReleaseLocation); 76 | } 77 | } 78 | 79 | public bool RightClickedDown { 80 | get { 81 | return enabled && IsHovered && Input.GetRightClickDown(KeyModifier._Any) && GlobalPointIsInBounds(Input.LastRightClickLocation); 82 | } 83 | } 84 | 85 | public bool ClickedDown { 86 | get { 87 | return enabled && IsHovered && Input.GetClickDown(KeyModifier._Any) && GlobalPointIsInBounds(Input.LastClickLocation) && IsMeOrAParent(Input.lastClickFocus); 88 | } 89 | } 90 | 91 | public bool DoubleClicked { 92 | get { 93 | return enabled && IsHovered && Input.GetDoubleClickDown(KeyModifier._Any) && GlobalPointIsInBounds(Input.LastClickLocation) && GlobalPointIsInBounds(Input.LastClickReleaseLocation) && IsMeOrAParent(Input.lastClickFocus); 94 | } 95 | } 96 | 97 | public bool ClickedM(KeyModifier modifier) { 98 | return InFocus 99 | && enabled && IsHovered && Input.GetClickUp(modifier) && GlobalPointIsInBounds(Input.LastClickLocation) && GlobalPointIsInBounds(Input.LastClickReleaseLocation) && IsMeOrAParent(Input.lastClickFocus); 100 | } 101 | 102 | public bool SingleClickedM(KeyModifier modifier) { 103 | return InFocus 104 | && enabled && IsHovered && Input.GetSingleClickUp(modifier) && GlobalPointIsInBounds(Input.LastClickLocation) && GlobalPointIsInBounds(Input.LastClickReleaseLocation) && IsMeOrAParent(Input.lastClickFocus); 105 | ; 106 | } 107 | 108 | public bool IsPressedM(KeyModifier modifier) { 109 | return IsHovered && Input.GetClick(modifier) && GlobalPointIsInBounds(Input.LastClickLocation) && IsMeOrAParent(Input.lastClickFocus); 110 | } 111 | 112 | public bool DoubleClickedM(KeyModifier modifier) { 113 | return enabled && IsHovered && Input.GetDoubleClickDown(modifier) && IsMeOrAParent(Input.lastClickFocus); 114 | } 115 | 116 | public bool GlobalPointIsInBounds(Point p) { 117 | return p.X >= GlobalX && p.Y >= GlobalY && p.X < GlobalX + width && p.Y < GlobalY + height; 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Source/UI/ColorButton.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace WaveTracker.UI { 4 | public class ColorButton : Clickable { 5 | public Color Color { get; set; } 6 | public bool NeverShowAlpha { get; set; } 7 | public bool DrawBorder { get; set; } 8 | public string HexValue { 9 | get { return Color.GetHexCode(); } 10 | set { Color = Helpers.HexCodeToColor(value); } 11 | } 12 | 13 | public ColorButton(Color color, int x, int y, Element parent) { 14 | enabled = true; 15 | this.x = x; 16 | this.y = y; 17 | Color = color; 18 | width = 62; 19 | height = 13; 20 | DrawBorder = true; 21 | NeverShowAlpha = false; 22 | SetParent(parent); 23 | } 24 | 25 | public void Update() { 26 | if (Clicked) { 27 | Dialogs.colorPicker.Open(this); 28 | } 29 | } 30 | 31 | public void Draw() { 32 | Color displayColor = Color; 33 | if (IsPressed) { 34 | displayColor = displayColor.ToNegative(); 35 | } 36 | 37 | Color textColor = (displayColor.R * 30 + displayColor.G * 59 + displayColor.B * 11) / 100 < 128 ? Color.White : Color.Black; 38 | if (DrawBorder) { 39 | DrawRect(0, 0, width, height, ButtonColors.backgroundColor); 40 | } 41 | DrawSprite(1, 1, width - 2, height - 2, new Rectangle(416, 208, 4, 2), Color.White); 42 | 43 | // draw color 44 | DrawRect(1, 1, width - 2, height - 2, displayColor); 45 | 46 | Color outlineColor = IsHovered ? textColor : Helpers.Alpha(Color.White, 80); 47 | DrawRect(1, 1, width - 2, 1, outlineColor); 48 | DrawRect(1, 2, 1, height - 4, outlineColor); 49 | DrawRect(1, height - 2, width - 2, 1, outlineColor); 50 | DrawRect(width - 2, 2, 1, height - 4, outlineColor); 51 | 52 | string label = "#" + (NeverShowAlpha ? Color.GetHexCodeIgnoringAlpha() : Color.GetHexCode()); 53 | int labelWidth = Helpers.GetWidthOfText(label); 54 | Write(label, (width - labelWidth) / 2, (height + 1) / 2 - 4, textColor); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Source/UI/ColorButtonList.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace WaveTracker.UI { 6 | public class ColorButtonList : Clickable { 7 | private Dictionary entries; 8 | private Scrollbar scrollbar; 9 | private int numRows; 10 | private const int ROW_HEIGHT = 17; 11 | 12 | public ColorButtonList(int x, int y, int width, int numVisibleRows, Element parent) { 13 | this.x = x; 14 | this.y = y; 15 | this.width = width; 16 | height = numVisibleRows * ROW_HEIGHT; 17 | numRows = numVisibleRows; 18 | scrollbar = new Scrollbar(0, 0, width, height, this); 19 | 20 | SetParent(parent); 21 | } 22 | 23 | /// 24 | /// Sets the list for this box to reference 25 | /// 26 | public void SetDictionary(Dictionary colors) { 27 | entries = []; 28 | foreach (KeyValuePair pair in colors) { 29 | ColorButton button = new ColorButton(pair.Value, 0, 0, this); 30 | button.DrawBorder = false; 31 | entries.Add(pair.Key, button); 32 | } 33 | } 34 | 35 | public void ResetView() { 36 | scrollbar.ScrollValue = 0; 37 | } 38 | 39 | /// 40 | /// Saves the list in this box to the given dictionary 41 | /// 42 | public void SaveDictionaryInto(Dictionary colors) { 43 | foreach (KeyValuePair pair in entries) { 44 | colors[pair.Key] = pair.Value.Color; 45 | } 46 | } 47 | 48 | public void Update() { 49 | if (InFocus) { 50 | scrollbar.SetSize(entries.Count, numRows); 51 | scrollbar.UpdateScrollValue(); 52 | scrollbar.Update(); 53 | int rowNum = numRows - 1; 54 | for (int i = numRows + scrollbar.ScrollValue - 1; i >= scrollbar.ScrollValue; i--) { 55 | entries.ElementAt(i).Value.x = 2; 56 | entries.ElementAt(i).Value.y = 2 + rowNum * ROW_HEIGHT; 57 | entries.ElementAt(i).Value.Update(); 58 | rowNum--; 59 | } 60 | } 61 | } 62 | 63 | public void Draw() { 64 | scrollbar.Draw(); 65 | 66 | Color bgColor = new Color(43, 49, 81); 67 | Color bgColor2 = new Color(59, 68, 107); 68 | 69 | int rowNum = numRows - 1; 70 | for (int i = numRows + scrollbar.ScrollValue - 1; i >= scrollbar.ScrollValue; i--) { 71 | DrawRect(0, rowNum * ROW_HEIGHT, width - 6, ROW_HEIGHT, i % 2 == 0 ? bgColor2 : bgColor); 72 | entries.ElementAt(i).Value.Draw(); 73 | Write(entries.ElementAt(i).Key, 68, rowNum * ROW_HEIGHT + 5, Color.White); 74 | rowNum--; 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Source/UI/ContextMenu.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework.Input; 2 | 3 | namespace WaveTracker.UI { 4 | public static class ContextMenu { 5 | private static Menu contextMenu; 6 | 7 | public static void Open(Menu menu) { 8 | if (contextMenu != menu) { 9 | contextMenu?.Close(); 10 | contextMenu = menu; 11 | } 12 | contextMenu.SetPositionClamped(Input.MousePositionX, Input.MousePositionY); 13 | contextMenu.Open(); 14 | } 15 | 16 | public static void CloseCurrent() { 17 | contextMenu?.Close(); 18 | } 19 | 20 | public static void Update() { 21 | if (contextMenu != null) { 22 | if (Input.GetKeyDown(Keys.Escape, KeyModifier.None)) { 23 | contextMenu.Close(); 24 | return; 25 | } 26 | contextMenu.Update(); 27 | if (contextMenu != null) { 28 | if (contextMenu.enabled == false) { 29 | contextMenu = null; 30 | } 31 | } 32 | } 33 | } 34 | 35 | public static void Draw() { 36 | contextMenu?.Draw(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Source/UI/Dialog.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace WaveTracker.UI { 4 | /// 5 | /// A window with buttons along the bottom 6 | /// 7 | public abstract class Dialog : Window { 8 | private List