private static void CreateSlideNotes(Song s, Dictionary <Pattern, RowFxData[, ]> patternFxData) { // Convert slide notes + portamento to our format. foreach (var c in s.Channels) { if (!c.SupportsSlideNotes) { continue; } var lastNoteInstrument = (Instrument)null; var lastNoteValue = (byte)Note.NoteInvalid; var portamentoSpeed = 0; for (int p = 0; p < s.Length; p++) { var pattern = c.PatternInstances[p]; var fxData = patternFxData[pattern]; for (int n = 0; n < s.PatternLength; n++) { var note = pattern.Notes[n]; var slideSpeed = 0; var slideTarget = 0; for (int i = 0; i < fxData.GetLength(1); i++) { var fx = fxData[n, i]; if (fx.param != 0) { // When the effect it turned on, we need to add a note. if ((fx.fx == '1' || fx.fx == '2' || fx.fx == 'Q' || fx.fx == 'R') && lastNoteValue >= Note.MusicalNoteMin && lastNoteValue <= Note.MusicalNoteMax && !note.IsValid) { pattern.Notes[n].Value = lastNoteValue; pattern.Notes[n].Instrument = lastNoteInstrument; pattern.Notes[n].HasAttack = false; note = pattern.Notes[n]; } if (fx.fx == '1') { slideSpeed = -fx.param; } if (fx.fx == '2') { slideSpeed = fx.param; } if (fx.fx == '3') { portamentoSpeed = fx.param; } if (fx.fx == 'Q') { slideTarget = note.Value + (fx.param & 0xf); slideSpeed = -((fx.param >> 4) * 2 + 1); } if (fx.fx == 'R') { slideTarget = note.Value - (fx.param & 0xf); slideSpeed = ((fx.param >> 4) * 2 + 1); } } else if (fx.fx == '3') { portamentoSpeed = 0; } } // Create a slide note. if (!note.IsSlideNote) { if (note.IsMusical) { var noteTable = NesApu.GetNoteTableForChannelType(c.Type, false); var pitchLimit = NesApu.GetPitchLimitForChannelType(c.Type); // If we have a new note with auto-portamento enabled, we need to // swap the notes since our slide notes work backward compared to // FamiTracker. if (portamentoSpeed != 0) { if (lastNoteValue >= Note.MusicalNoteMin && lastNoteValue <= Note.MusicalNoteMax) { pattern.Notes[n].SlideNoteTarget = pattern.Notes[n].Value; pattern.Notes[n].Value = lastNoteValue; } } else if (slideTarget != 0) { var numFrames = Math.Abs((noteTable[note.Value] - noteTable[slideTarget]) / (slideSpeed * s.Speed)); pattern.Notes[n].SlideNoteTarget = (byte)slideTarget; var nn = n + numFrames; var np = p; while (nn >= s.PatternLength) { nn -= s.PatternLength; np++; } if (np >= s.Length) { np = s.Length; nn = 0; } // Still to see if there is a note between the current one and the // next note, this could append if you add a note before the slide // is supposed to finish. if (FindNextNoteForSlide(c, p, n, out var np2, out var nn2, patternFxData)) { if (np2 < np) { np = np2; nn = nn2; } else if (np2 == np) { nn = Math.Min(nn, nn2); } } // Add an extra note with no attack to stop the slide. if (!c.PatternInstances[np].Notes[nn].IsValid) { c.PatternInstances[np].Notes[nn].Instrument = note.Instrument; c.PatternInstances[np].Notes[nn].Value = (byte)slideTarget; c.PatternInstances[np].Notes[nn].HasAttack = false; } } // Find the next note that would stop the slide or change the FX settings. else if (slideSpeed != 0 && FindNextNoteForSlide(c, p, n, out var np, out var nn, patternFxData)) { // Compute the pitch delta and find the closest target note. var numFrames = ((np * s.PatternLength + nn) - (p * s.PatternLength + n)) * s.Speed; // TODO: PAL. var newNotePitch = Utils.Clamp(noteTable[note.Value] + numFrames * slideSpeed, 0, pitchLimit); var newNote = FindBestMatchingNote(noteTable, newNotePitch, Math.Sign(slideSpeed)); pattern.Notes[n].SlideNoteTarget = (byte)newNote; // If the FX was turned off, we need to add an extra note. if (!c.PatternInstances[np].Notes[nn].IsMusical && !c.PatternInstances[np].Notes[nn].IsStop) { c.PatternInstances[np].Notes[nn].Instrument = note.Instrument; c.PatternInstances[np].Notes[nn].Value = (byte)newNote; c.PatternInstances[np].Notes[nn].HasAttack = false; } } } } if (note.IsMusical || note.IsStop) { lastNoteValue = note.Value; lastNoteInstrument = note.Instrument; } } } } }
private bool UpdateChannel(int p, int n, Channel channel, ChannelState state) { var project = channel.Song.Project; var channelIdx = Channel.ChannelTypeToIndex(channel.Type); var hasNote = false; if (channel.Type == Channel.Dpcm) { var len = NsfGetState(nsf, channel.Type, STATE_DPCMSAMPLELENGTH, 0) - 1; if (len > 0) { var sampleData = new byte[len]; for (int i = 0; i < len; i++) { sampleData[i] = (byte)NsfGetState(nsf, channel.Type, STATE_DPCMSAMPLEDATA, i); } var sample = project.FindMatchingSample(sampleData); if (sample == null) { sample = project.CreateDPCMSample($"Sample {project.Samples.Count + 1}", sampleData); } var loop = NsfGetState(nsf, channel.Type, STATE_DPCMLOOP, 0) != 0; var pitch = NsfGetState(nsf, channel.Type, STATE_DPCMPITCH, 0); var note = project.FindDPCMSampleMapping(sample, pitch, loop); if (note == -1) { for (int i = Note.DPCMNoteMin + 1; i <= Note.DPCMNoteMax; i++) { if (project.GetDPCMMapping(i) == null) { note = i; project.MapDPCMSample(i, sample, pitch, loop); break; } } } if (note != -1) { var pattern = GetOrCreatePattern(channel, p).GetOrCreateNoteAt(n).Value = (byte)note; hasNote = true; } } } else { var period = NsfGetState(nsf, channel.Type, STATE_PERIOD, 0); var volume = NsfGetState(nsf, channel.Type, STATE_VOLUME, 0); var duty = NsfGetState(nsf, channel.Type, STATE_DUTYCYCLE, 0); var force = false; var stop = false; var release = false; var octave = -1; // VRC6 has a much larger volume range (6-bit) than our volume (4-bit). if (channel.Type == Channel.Vrc6Saw) { volume >>= 2; } else if (channel.Type == Channel.FdsWave) { volume = Math.Min(Note.VolumeMax, volume >> 1); } else if (channel.Type >= Channel.Vrc7Fm1 && channel.Type <= Channel.Vrc7Fm6) { volume = 15 - volume; } var hasTrigger = true; var hasPeriod = true; var hasOctave = channel.Type >= Channel.Vrc7Fm1 && channel.Type <= Channel.Vrc7Fm6; var hasVolume = channel.Type != Channel.Triangle; var hasPitch = channel.Type != Channel.Noise; var hasDuty = channel.Type == Channel.Square1 || channel.Type == Channel.Square2 || channel.Type == Channel.Noise || channel.Type == Channel.Vrc6Square1 || channel.Type == Channel.Vrc6Square2 || channel.Type == Channel.Mmc5Square1 || channel.Type == Channel.Mmc5Square2; if (channel.Type >= Channel.Vrc7Fm1 && channel.Type <= Channel.Vrc7Fm6) { var trigger = NsfGetState(nsf, channel.Type, STATE_VRC7TRIGGER, 0) != 0; var sustain = NsfGetState(nsf, channel.Type, STATE_VRC7SUSTAIN, 0) != 0; var triggerState = trigger ? ChannelState.Triggered : (sustain ? ChannelState.Released : ChannelState.Stopped); if (triggerState != state.trigger) { stop = triggerState == ChannelState.Stopped; release = triggerState == ChannelState.Released; force |= true; state.trigger = triggerState; } octave = NsfGetState(nsf, channel.Type, STATE_VRC7OCTAVE, 0); } else { if (hasTrigger) { var trigger = volume != 0 ? ChannelState.Triggered : ChannelState.Stopped; if (trigger != state.trigger) { stop = trigger == ChannelState.Stopped; force |= true; state.trigger = trigger; } } } if (hasVolume) { if (state.volume != volume && volume != 0) { var pattern = GetOrCreatePattern(channel, p).GetOrCreateNoteAt(n).Volume = (byte)volume; state.volume = volume; } } Instrument instrument = null; if (hasDuty) { instrument = GetDutyInstrument(channel, duty); } else if (channel.Type == Channel.FdsWave) { var wavEnv = new sbyte[64]; var modEnv = new sbyte[32]; for (int i = 0; i < 64; i++) { wavEnv[i] = (sbyte)NsfGetState(nsf, channel.Type, STATE_FDSWAVETABLE, i); } for (int i = 0; i < 32; i++) { modEnv[i] = (sbyte)NsfGetState(nsf, channel.Type, STATE_FDSMODULATIONTABLE, i); } Envelope.ConvertFdsModulationToAbsolute(modEnv); var masterVolume = (byte)NsfGetState(nsf, channel.Type, STATE_FDSMASTERVOLUME, 0); instrument = GetFdsInstrument(wavEnv, modEnv, masterVolume); int modDepth = NsfGetState(nsf, channel.Type, STATE_FDSMODULATIONDEPTH, 0); int modSpeed = NsfGetState(nsf, channel.Type, STATE_FDSMODULATIONSPEED, 0); if (state.fdsModDepth != modDepth) { var pattern = GetOrCreatePattern(channel, p).GetOrCreateNoteAt(n).FdsModDepth = (byte)modDepth; state.fdsModDepth = modDepth; } if (state.fdsModSpeed != modSpeed) { var pattern = GetOrCreatePattern(channel, p).GetOrCreateNoteAt(n).FdsModSpeed = (ushort)modSpeed; state.fdsModSpeed = modSpeed; } } else if (channel.Type >= Channel.N163Wave1 && channel.Type <= Channel.N163Wave8) { var wavePos = (byte)NsfGetState(nsf, channel.Type, STATE_N163WAVEPOS, 0); var waveLen = (byte)NsfGetState(nsf, channel.Type, STATE_N163WAVESIZE, 0); if (waveLen > 0) { var waveData = new sbyte[waveLen]; for (int i = 0; i < waveLen; i++) { waveData[i] = (sbyte)NsfGetState(nsf, channel.Type, STATE_N163WAVE, wavePos + i); } instrument = GetN163Instrument(waveData, wavePos); } period >>= 2; } else if (channel.Type >= Channel.Vrc7Fm1 && channel.Type <= Channel.Vrc7Fm6) { var patch = (byte)NsfGetState(nsf, channel.Type, STATE_VRC7PATCH, 0); var regs = new byte[8]; if (patch == 0) { for (int i = 0; i < 8; i++) { regs[i] = (byte)NsfGetState(nsf, channel.Type, STATE_VRC7PATCHREG, i); } } instrument = GetVrc7Instrument(patch, regs); } else if (channel.Type >= Channel.S5BSquare1 && channel.Type <= Channel.S5BSquare3) { instrument = GetS5BInstrument(); } else { instrument = GetDutyInstrument(channel, 0); } if ((hasPeriod && state.period != period) || (hasOctave && state.octave != octave) || (instrument != state.instrument) || force) { var noteTable = NesApu.GetNoteTableForChannelType(channel.Type, false, project.ExpansionNumChannels); var note = release ? Note.NoteRelease : (stop ? Note.NoteStop : state.note); var finePitch = 0; if (!stop && !release && state.trigger != ChannelState.Stopped) { if (channel.Type == Channel.Noise) { note = (period ^ 0x0f) + 32; } else { note = (byte)GetBestMatchingNote(period, noteTable, out finePitch); } if (hasOctave) { while (note > 12) { note -= 12; octave++; } note += octave * 12; period *= (1 << octave); finePitch = period - noteTable[note]; } } if (note < Note.MusicalNoteMin || note > Note.MusicalNoteMax) { instrument = null; } if ((state.note != note) || (state.instrument != instrument && instrument != null) || force) { var pattern = GetOrCreatePattern(channel, p); var newNote = pattern.GetOrCreateNoteAt(n); newNote.Value = (byte)note; newNote.Instrument = instrument; state.note = note; state.octave = octave; if (instrument != null) { state.instrument = instrument; } hasNote = note != 0; } if (hasPitch && !stop) { Channel.GetShiftsForType(channel.Type, project.ExpansionNumChannels, out int pitchShift, out _); // We scale all pitches changes (slides, fine pitch, pitch envelopes) for // some channels with HUGE pitch values (N163, VRC7). finePitch >>= pitchShift; var pitch = (sbyte)Utils.Clamp(finePitch, Note.FinePitchMin, Note.FinePitchMax); if (pitch != state.pitch) { var pattern = GetOrCreatePattern(channel, p).GetOrCreateNoteAt(n).FinePitch = pitch; state.pitch = pitch; } } state.period = period; } } return(hasNote); }
public static void Load() { var ini = new IniFile(); ini.Load(GetConfigFileName()); Version = ini.GetInt("General", "Version", 0); // General CheckUpdates = ini.GetBool(Version < 2 ? "UI" : "General", "CheckUpdates", true); // At version 2 (FamiStudio 3.0.0, changed section) TrackPadControls = ini.GetBool(Version < 2 ? "UI" : "General", "TrackPadControls", false); // At version 2 (FamiStudio 3.0.0, changed section) ShowTutorial = ini.GetBool(Version < 2 ? "UI" : "General", "ShowTutorial", true); // At version 2 (FamiStudio 3.0.0, changed section) ClearUndoRedoOnSave = ini.GetBool("General", "ClearUndoRedoOnSave", true); OpenLastProjectOnStart = ini.GetBool("General", "OpenLastProjectOnStart", true); AutoSaveCopy = ini.GetBool("General", "AutoSaveCopy", true); LastProjectFile = OpenLastProjectOnStart ? ini.GetString("General", "LastProjectFile", "") : ""; // UI DpiScaling = ini.GetInt("UI", "DpiScaling", 0); TimeFormat = ini.GetInt("UI", "TimeFormat", 0); FollowMode = ini.GetInt("UI", "FollowMode", FollowModeContinuous); FollowSync = ini.GetInt("UI", "FollowSync", FollowSyncBoth); ShowNoteLabels = ini.GetBool("UI", "ShowNoteLabels", true); ScrollBars = Version < 3 ? (ini.GetBool("UI", "ShowScrollBars", false) ? ScrollBarsThin : ScrollBarsNone) : ini.GetInt("UI", "ScrollBars", ScrollBarsNone); ShowOscilloscope = ini.GetBool("UI", "ShowOscilloscope", true); ForceCompactSequencer = ini.GetBool("UI", "ForceCompactSequencer", false); ShowPianoRollViewRange = ini.GetBool("UI", "ShowPianoRollViewRange", true); ReverseTrackPad = ini.GetBool("UI", "ReverseTrackPad", false); TrackPadMoveSensitity = ini.GetInt("UI", "TrackPadMoveSensitity", 1); TrackPadZoomSensitity = ini.GetInt("UI", "TrackPadZoomSensitity", 8); ShowImplicitStopNotes = ini.GetBool("UI", "ShowImplicitStopNotes", true); // Audio NumBufferedAudioFrames = ini.GetInt("Audio", "NumBufferedFrames", DefaultNumBufferedAudioFrames); InstrumentStopTime = ini.GetInt("Audio", "InstrumentStopTime", 2); SquareSmoothVibrato = ini.GetBool("Audio", "SquareSmoothVibrato", true); NoDragSoungWhenPlaying = ini.GetBool("Audio", "NoDragSoungWhenPlaying", false); MetronomeVolume = ini.GetInt("Audio", "MetronomeVolume", 50); SeparateChannelsExportTndMode = ini.GetInt("Audio", "SeparateChannelsExportTndMode", NesApu.TND_MODE_SINGLE); // MIDI MidiDevice = ini.GetString("MIDI", "Device", ""); // Folders LastFileFolder = ini.GetString("Folders", "LastFileFolder", ""); LastInstrumentFolder = ini.GetString("Folders", "LastInstrumentFolder", ""); LastSampleFolder = ini.GetString("Folders", "LastSampleFolder", ""); LastExportFolder = ini.GetString("Folders", "LastExportFolder", ""); // FFmpeg FFmpegExecutablePath = ini.GetString("FFmpeg", "ExecutablePath", ""); // Mixer. GlobalVolume = ini.GetFloat("Mixer", "GlobalVolume", -3.0f); Array.Copy(DefaultExpansionMixerSettings, ExpansionMixerSettings, ExpansionMixerSettings.Length); for (int i = 0; i < ExpansionType.Count; i++) { var section = "Mixer" + ExpansionType.ShortNames[i]; ExpansionMixerSettings[i].volume = ini.GetFloat(section, "Volume", DefaultExpansionMixerSettings[i].volume); ExpansionMixerSettings[i].treble = ini.GetFloat(section, "Treble", DefaultExpansionMixerSettings[i].treble); } // QWERTY Array.Copy(DefaultQwertyKeys, QwertyKeys, DefaultQwertyKeys.Length); // Stop note. { if (ini.HasKey("QWERTY", "StopNote")) { QwertyKeys[0, 0] = ini.GetInt("QWERTY", "StopNote", QwertyKeys[0, 0]); } if (ini.HasKey("QWERTY", "StopNoteAlt")) { QwertyKeys[0, 1] = ini.GetInt("QWERTY", "StopNoteAlt", QwertyKeys[0, 1]); } } // Regular notes. for (int idx = 1; idx < QwertyKeys.GetLength(0); idx++) { var octave = (idx - 1) / 12; var note = (idx - 1) % 12; var keyName0 = $"Octave{octave}Note{note}"; var keyName1 = $"Octave{octave}Note{note}Alt"; if (ini.HasKey("QWERTY", keyName0)) { QwertyKeys[idx, 0] = ini.GetInt("QWERTY", keyName0, QwertyKeys[idx, 0]); } if (ini.HasKey("QWERTY", keyName1)) { QwertyKeys[idx, 1] = ini.GetInt("QWERTY", keyName1, QwertyKeys[idx, 1]); } } UpdateKeyCodeMaps(); if (Array.IndexOf(global::FamiStudio.DpiScaling.GetAvailableScalings(), DpiScaling) < 0) { DpiScaling = 0; } InstrumentStopTime = Utils.Clamp(InstrumentStopTime, 0, 10); if (MidiDevice == null) { MidiDevice = ""; } if (!Directory.Exists(LastInstrumentFolder)) { LastInstrumentFolder = ""; } if (!Directory.Exists(LastSampleFolder)) { LastSampleFolder = ""; } if (!Directory.Exists(LastExportFolder)) { LastExportFolder = ""; } // Try to point to the demo songs initially. if (string.IsNullOrEmpty(LastFileFolder) || !Directory.Exists(LastFileFolder)) { var appPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); var demoSongsPath = Path.Combine(appPath, "Demo Songs"); LastFileFolder = Directory.Exists(demoSongsPath) ? demoSongsPath : ""; } // Clamp to something reasonable. NumBufferedAudioFrames = Utils.Clamp(NumBufferedAudioFrames, 2, 16); // Linux or Mac is more likely to have standard path for ffmpeg. if (PlatformUtils.IsLinux || PlatformUtils.IsMacOS) { if (string.IsNullOrEmpty(FFmpegExecutablePath) || !File.Exists(FFmpegExecutablePath)) { if (File.Exists("/usr/bin/ffmpeg")) { FFmpegExecutablePath = "/usr/bin/ffmpeg"; } else if (File.Exists("/usr/local/bin/ffmpeg")) { FFmpegExecutablePath = "/usr/local/bin/ffmpeg"; } else { FFmpegExecutablePath = "ffmpeg"; // Hope for the best! } } } // Mobile section AllowVibration = ini.GetBool("Mobile", "AllowVibration", true); DoubleClickDelete = ini.GetBool("Mobile", "DoubleClickDelete", false); // At 3.2.0, we added a new Discord screen to the tutorial. if (Version < 4) { ShowTutorial = true; } // No deprecation at the moment. Version = SettingsVersion; }
public bool ComputeSlideNoteParams(Note note, int patternIdx, int noteIdx, int famitrackerSpeed, ushort[] noteTable, bool pal, bool applyShifts, out int pitchDelta, out int stepSize, out float stepSizeFloat) { Debug.Assert(note.IsMusical); var slideShift = 0; if (applyShifts) { GetShiftsForType(type, song.Project.ExpansionNumChannels, out _, out slideShift); } pitchDelta = noteTable[note.Value] - noteTable[note.SlideNoteTarget]; if (pitchDelta != 0) { pitchDelta = slideShift < 0 ? (pitchDelta << -slideShift) : (pitchDelta >> slideShift); // Find the next note to calculate the slope. FindNextNoteForSlide(patternIdx, noteIdx, 256, out var nextPatternIdx, out var nextNoteIdx); // 256 is kind of arbitrary. // Approximate how many frames separates these 2 notes. var frameCount = 0.0f; if (patternIdx != nextPatternIdx || noteIdx != nextNoteIdx) { // Take delayed notes/cuts into account. var delayFrames = -(note.HasNoteDelay ? note.NoteDelay : 0); if (Song.UsesFamiTrackerTempo) { var nextNote = GetNoteAt(nextPatternIdx, nextNoteIdx); if (nextNote != null) { if (nextNote.HasNoteDelay) { if (nextNote.HasCutDelay) { delayFrames += Math.Min(nextNote.NoteDelay, nextNote.CutDelay); } else { delayFrames += nextNote.NoteDelay; } } else if (nextNote.HasCutDelay) { delayFrames += nextNote.CutDelay; } } } frameCount = Song.CountFramesBetween(patternIdx, noteIdx, nextPatternIdx, nextNoteIdx, famitrackerSpeed, pal) + delayFrames; } else { Debug.Assert(note.HasCutDelay && Song.UsesFamiTrackerTempo); // Slide note starts and end on same note, this mean we have a delayed cut. frameCount = note.HasCutDelay ? note.CutDelay : 0; } var absStepPerFrame = Math.Abs(pitchDelta) / Math.Max(1, frameCount); stepSize = Utils.Clamp((int)Math.Ceiling(absStepPerFrame) * -Math.Sign(pitchDelta), sbyte.MinValue, sbyte.MaxValue); stepSizeFloat = pitchDelta / Math.Max(1, frameCount); return(true); } else { stepSize = 0; stepSizeFloat = 0.0f; return(false); } }
protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); bool left = e.Button.HasFlag(MouseButtons.Left); bool middle = e.Button.HasFlag(MouseButtons.Middle) || (e.Button.HasFlag(MouseButtons.Left) && ModifierKeys.HasFlag(Keys.Alt)); bool right = e.Button.HasFlag(MouseButtons.Right); CancelDragSelection(); UpdateCursor(); if (middle) { mouseLastX = e.X; mouseLastY = e.Y; return; } // Track muting, soloing. else if ((left || right) && e.X < TrackNameSizeX) { for (int i = 0; i < 5; i++) { int bit = (1 << i); if (GetTrackIconRect(i).Contains(e.X, e.Y)) { if (left) { // Toggle muted App.ChannelMask ^= bit; } else { // Toggle Solo if (App.ChannelMask == (1 << i)) { App.ChannelMask = 0x1f; } else { App.ChannelMask = (1 << i); } } ConditionalInvalidate(); break; } if (GetTrackGhostRect(i).Contains(e.X, e.Y)) { App.GhostChannelMask ^= bit; ConditionalInvalidate(); break; } } } if (left && e.X > TrackNameSizeX && e.Y < HeaderSizeY) { int frame = (int)Math.Round((e.X - TrackNameSizeX + scrollX) / (float)PatternSizeX * Song.PatternLength); App.Seek(frame); return; } if (left || right) { if (e.Y > HeaderSizeY) { selectedChannel = Utils.Clamp((e.Y - HeaderSizeY) / TrackSizeY, 0, Channel.Count - 1); ConditionalInvalidate(); } } bool inPatternZone = GetPatternForCoord(e.X, e.Y, out int channelIdx, out int patternIdx); if (inPatternZone) { var channel = Song.Channels[channelIdx]; var pattern = channel.PatternInstances[patternIdx]; if (left) { bool shift = ModifierKeys.HasFlag(Keys.Shift); if (pattern == null && !shift) { App.UndoRedoManager.BeginTransaction(TransactionScope.Song, Song.Id); channel.PatternInstances[patternIdx] = channel.CreatePattern(); App.UndoRedoManager.EndTransaction(); ConditionalInvalidate(); } else { if (shift && minSelectedChannelIdx >= 0 && minSelectedPatternIdx >= 0) { if (channelIdx < minSelectedChannelIdx) { maxSelectedChannelIdx = minSelectedChannelIdx; minSelectedChannelIdx = channelIdx; } else { maxSelectedChannelIdx = channelIdx; } if (patternIdx < minSelectedPatternIdx) { maxSelectedPatternIdx = minSelectedPatternIdx; minSelectedPatternIdx = patternIdx; } else { maxSelectedPatternIdx = patternIdx; } } else if (!IsPatternSelected(channelIdx, patternIdx) && pattern != null) { minSelectedChannelIdx = channelIdx; maxSelectedChannelIdx = channelIdx; minSelectedPatternIdx = patternIdx; maxSelectedPatternIdx = patternIdx; } selectionDragAnchorX = e.X - TrackNameSizeX + scrollX - minSelectedPatternIdx * PatternSizeX; selectionDragStartX = e.X; if (pattern != null) { PatternClicked?.Invoke(channelIdx, patternIdx); } ConditionalInvalidate(); } } else if (right && pattern != null) { App.UndoRedoManager.BeginTransaction(TransactionScope.Song, Song.Id); channel.PatternInstances[patternIdx] = null; App.UndoRedoManager.EndTransaction(); ConditionalInvalidate(); } } }
private void ComputeChannelsScroll(VideoFrameMetadata[] frames, int channelMask, int numVisibleNotes) { var numFrames = frames.Length; var numChannels = frames[0].channelNotes.Length; for (int c = 0; c < numChannels; c++) { if ((channelMask & (1 << c)) == 0) { continue; } // Go through all the frames and split them in segments. // A segment is a section of the song where all the notes fit in the view. var segments = new List <ScrollSegment>(); var currentSegment = (ScrollSegment)null; var minOverallNote = int.MaxValue; var maxOverallNote = int.MinValue; for (int f = 0; f < numFrames; f++) { var frame = frames[f]; var note = frame.channelNotes[c]; if (frame.scroll == null) { frame.scroll = new float[numChannels]; } if (note.IsMusical) { if (currentSegment == null) { currentSegment = new ScrollSegment(); segments.Add(currentSegment); } // If its the start of a new pattern and we've been not moving for ~10 sec, let's start a new segment. bool forceNewSegment = frame.playNote == 0 && (f - currentSegment.startFrame) > 600; var minNoteValue = note.Value - 1; var maxNoteValue = note.Value + 1; // Only consider slides if they arent too large. if (note.IsSlideNote && Math.Abs(note.SlideNoteTarget - note.Value) < numVisibleNotes / 2) { minNoteValue = Math.Min(note.Value, note.SlideNoteTarget) - 1; maxNoteValue = Math.Max(note.Value, note.SlideNoteTarget) + 1; } // Only consider arpeggios if they are not too big. if (note.IsArpeggio && note.Arpeggio.GetChordMinMaxOffset(out var minArp, out var maxArp) && maxArp - minArp < numVisibleNotes / 2) { minNoteValue = note.Value + minArp; maxNoteValue = note.Value + maxArp; } minOverallNote = Math.Min(minOverallNote, minNoteValue); maxOverallNote = Math.Max(maxOverallNote, maxNoteValue); var newMinNote = Math.Min(currentSegment.minNote, minNoteValue); var newMaxNote = Math.Max(currentSegment.maxNote, maxNoteValue); // If we cant fit the next note in the view, start a new segment. if (forceNewSegment || newMaxNote - newMinNote + 1 > numVisibleNotes) { currentSegment.endFrame = f; currentSegment = new ScrollSegment(); currentSegment.startFrame = f; segments.Add(currentSegment); currentSegment.minNote = minNoteValue; currentSegment.maxNote = maxNoteValue; } else { currentSegment.minNote = newMinNote; currentSegment.maxNote = newMaxNote; } } } // Not a single notes in this channel... if (currentSegment == null) { currentSegment = new ScrollSegment(); currentSegment.minNote = Note.FromFriendlyName("C4"); currentSegment.maxNote = currentSegment.minNote; segments.Add(currentSegment); } currentSegment.endFrame = numFrames; // Remove very small segments, these make the camera move too fast, looks bad. var shortestAllowedSegment = SegmentTransitionNumFrames * 2; bool removed = false; do { var sortedSegment = new List <ScrollSegment>(segments); sortedSegment.Sort((s1, s2) => s1.NumFrames.CompareTo(s2.NumFrames)); if (sortedSegment[0].NumFrames >= shortestAllowedSegment) { break; } for (int s = 0; s < sortedSegment.Count; s++) { var seg = sortedSegment[s]; if (seg.NumFrames >= shortestAllowedSegment) { break; } var thisSegmentIndex = segments.IndexOf(seg); // Segment is too short, see if we can merge with previous/next one. var mergeSegmentIndex = -1; var mergeSegmentLength = -1; if (thisSegmentIndex > 0) { mergeSegmentIndex = thisSegmentIndex - 1; mergeSegmentLength = segments[thisSegmentIndex - 1].NumFrames; } if (thisSegmentIndex != segments.Count - 1 && segments[thisSegmentIndex + 1].NumFrames > mergeSegmentLength) { mergeSegmentIndex = thisSegmentIndex + 1; mergeSegmentLength = segments[thisSegmentIndex + 1].NumFrames; } if (mergeSegmentIndex >= 0) { // Merge. var mergeSeg = segments[mergeSegmentIndex]; mergeSeg.startFrame = Math.Min(mergeSeg.startFrame, seg.startFrame); mergeSeg.endFrame = Math.Max(mergeSeg.endFrame, seg.endFrame); segments.RemoveAt(thisSegmentIndex); removed = true; break; } } }while (removed); // Build the actually scrolling data. var minScroll = (float)Math.Ceiling(Note.MusicalNoteMin + numVisibleNotes * 0.5f); var maxScroll = (float)Math.Floor(Note.MusicalNoteMax - numVisibleNotes * 0.5f); Debug.Assert(maxScroll >= minScroll); foreach (var segment in segments) { segment.scroll = Utils.Clamp(segment.minNote + (segment.maxNote - segment.minNote) * 0.5f, minScroll, maxScroll); } for (var s = 0; s < segments.Count; s++) { var segment0 = segments[s + 0]; var segment1 = s == segments.Count - 1 ? null : segments[s + 1]; for (int f = segment0.startFrame; f < segment0.endFrame - (segment1 == null ? 0 : SegmentTransitionNumFrames); f++) { frames[f].scroll[c] = segment0.scroll; } if (segment1 != null) { // Smooth transition to next segment. for (int f = segment0.endFrame - SegmentTransitionNumFrames, a = 0; f < segment0.endFrame; f++, a++) { var lerp = a / (float)SegmentTransitionNumFrames; frames[f].scroll[c] = Utils.Lerp(segment0.scroll, segment1.scroll, Utils.SmootherStep(lerp)); } } } } }
private void CreateSlideNotes(Song s, Dictionary <Pattern, RowFxData[, ]> patternFxData) { var processedPatterns = new HashSet <Pattern>(); // Convert slide notes + portamento to our format. foreach (var c in s.Channels) { if (!c.SupportsSlideNotes) { continue; } var songSpeed = s.FamitrackerSpeed; var lastNoteInstrument = (Instrument)null; var lastNoteArpeggio = (Arpeggio)null; var lastNoteValue = (byte)Note.NoteInvalid; var portamentoSpeed = 0; var slideSpeed = 0; var slideShift = c.IsN163WaveChannel ? 2 : 0; var slideSign = c.IsN163WaveChannel || c.IsFdsWaveChannel ? -1 : 1; // Inverted channels. for (int p = 0; p < s.Length; p++) { var pattern = c.PatternInstances[p]; if (pattern == null || !patternFxData.ContainsKey(pattern) || processedPatterns.Contains(pattern)) { continue; } processedPatterns.Add(pattern); var fxData = patternFxData[pattern]; var patternLen = s.GetPatternLength(p); for (var it = pattern.GetNoteIterator(0, patternLen); !it.Done; it.Next()) { var n = it.CurrentTime; var note = it.CurrentNote; // Look for speed changes. foreach (var c2 in s.Channels) { var pattern2 = c2.PatternInstances[p]; if (pattern2 != null && pattern2.Notes.TryGetValue(n, out var note2) && note2.HasSpeed) { songSpeed = note2.Speed; } } var slideTarget = 0; for (int i = 0; i < fxData.GetLength(1); i++) { var fx = fxData[n, i]; if (fx.param != 0) { // When the effect it turned on, we need to add a note. if ((fx.fx == Effect_PortaUp || fx.fx == Effect_PortaDown || fx.fx == Effect_SlideUp || fx.fx == Effect_SlideDown) && lastNoteValue >= Note.MusicalNoteMin && lastNoteValue <= Note.MusicalNoteMax && (note == null || !note.IsValid)) { if (note == null) { note = pattern.GetOrCreateNoteAt(n); it.Resync(); } note.Value = lastNoteValue; note.Instrument = lastNoteInstrument; note.Arpeggio = lastNoteArpeggio; note.HasAttack = false; } } if (fx.fx == Effect_PortaUp) { // If we have a Qxx/Rxx on the same row as a 1xx/2xx, things get weird. if (slideTarget == 0) { slideSpeed = (-fx.param * slideSign) << slideShift; } } if (fx.fx == Effect_PortaDown) { // If we have a Qxx/Rxx on the same row as a 1xx/2xx, things get weird. if (slideTarget == 0) { slideSpeed = (fx.param * slideSign) << slideShift; } } if (fx.fx == Effect_Portamento) { portamentoSpeed = fx.param; } if (fx.fx == Effect_SlideUp) { slideTarget = note.Value + (fx.param & 0xf); slideSpeed = (-((fx.param >> 4) * 2 + 1)) << slideShift; } if (fx.fx == Effect_SlideDown) { slideTarget = note.Value - (fx.param & 0xf); slideSpeed = (((fx.param >> 4) * 2 + 1)) << slideShift; } } // Create a slide note. if (note != null && !note.IsSlideNote) { if (note.IsMusical) { var slideSource = note.Value; var noteTable = NesApu.GetNoteTableForChannelType(c.Type, false, s.Project.ExpansionNumChannels); var pitchLimit = NesApu.GetPitchLimitForChannelType(c.Type); // If we have a new note with auto-portamento enabled, we need to // swap the notes since our slide notes work backward compared to // FamiTracker. if (portamentoSpeed != 0) { // Ignore notes with no attack since we created them to handle a previous slide. if (note.HasAttack && lastNoteValue >= Note.MusicalNoteMin && lastNoteValue <= Note.MusicalNoteMax) { slideSpeed = portamentoSpeed; slideTarget = note.Value; slideSource = lastNoteValue; note.Value = lastNoteValue; } } // Our implementation of VRC7 pitches is quite different from FamiTracker. // Compensate for larger pitches in higher octaves by shifting. We cant shift by // a large amount because the period is 9-bit and FamiTracker is restricted to // this for slides (octave never changes). var octaveSlideShift = c.IsVrc7FmChannel && note.Value >= 12 ? 1 : 0; if (slideTarget != 0) { // TODO: We assume a tempo of 150 here. This is wrong. var numFrames = Math.Max(1, Math.Abs((noteTable[slideSource] - noteTable[slideTarget]) / ((slideSpeed << octaveSlideShift) * songSpeed))); note.SlideNoteTarget = (byte)slideTarget; var nn = n + numFrames; var np = p; while (nn >= s.GetPatternLength(np)) { nn -= s.GetPatternLength(np); np++; } if (np >= s.Length) { np = s.Length; nn = 0; } // Still to see if there is a note between the current one and the // next note, this could append if you add a note before the slide // is supposed to finish. if (FindNextNoteForSlide(c, p, n, out var np2, out var nn2, patternFxData)) { if (np2 < np) { np = np2; nn = nn2; } else if (np2 == np) { nn = Math.Min(nn, nn2); } } if (np < s.Length) { // Add an extra note with no attack to stop the slide. var nextPattern = c.PatternInstances[np]; if (!nextPattern.Notes.TryGetValue(nn, out var nextNote) || !nextNote.IsValid) { nextNote = nextPattern.GetOrCreateNoteAt(nn); nextNote.Instrument = note.Instrument; nextNote.Value = (byte)slideTarget; nextNote.HasAttack = false; it.Resync(); } } // 3xx, Qxx and Rxx stops when its done. slideSpeed = 0; } // Find the next note that would stop the slide or change the FX settings. else if (slideSpeed != 0 && FindNextNoteForSlide(c, p, n, out var np, out var nn, patternFxData)) { // Compute the pitch delta and find the closest target note. var numFrames = (s.GetPatternStartNote(np, nn) - s.GetPatternStartNote(p, n)) * songSpeed; // TODO: PAL. var newNotePitch = Utils.Clamp(noteTable[slideSource] + numFrames * (slideSpeed << octaveSlideShift), 0, pitchLimit); var newNote = FindBestMatchingNote(noteTable, newNotePitch, Math.Sign(slideSpeed)); note.SlideNoteTarget = (byte)newNote; // If the FX was turned off, we need to add an extra note. var nextPattern = c.PatternInstances[np]; if (!nextPattern.Notes.TryGetValue(nn, out var nextNote) || !nextNote.IsValid) { nextNote = nextPattern.GetOrCreateNoteAt(nn); nextNote.Instrument = note.Instrument; nextNote.Value = (byte)newNote; nextNote.HasAttack = false; it.Resync(); } } } } if (note != null && (note.IsMusical || note.IsStop)) { lastNoteValue = note.IsSlideNote ? note.SlideNoteTarget : note.Value; lastNoteInstrument = note.Instrument; lastNoteArpeggio = note.Arpeggio; } } } } }
protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); ControlActivated?.Invoke(); bool left = e.Button.HasFlag(MouseButtons.Left); bool middle = e.Button.HasFlag(MouseButtons.Middle) || (e.Button.HasFlag(MouseButtons.Left) && ModifierKeys.HasFlag(Keys.Alt)); bool right = e.Button.HasFlag(MouseButtons.Right); bool canCapture = captureOperation == CaptureOperation.None; CancelDragSelection(); UpdateCursor(); if (middle) { mouseLastX = e.X; mouseLastY = e.Y; Capture = true; return; } // Track muting, soloing. else if ((left || right) && e.X < trackNameSizeX) { var trackIcon = GetTrackIconForPos(e); var ghostIcon = GetTrackGhostForPos(e); if (trackIcon >= 0) { int bit = (1 << trackIcon); if (left) { // Toggle muted App.ChannelMask ^= bit; } else { // Toggle Solo if (App.ChannelMask == bit) { App.ChannelMask = 0xff; } else { App.ChannelMask = bit; } } ConditionalInvalidate(); return; } else if (ghostIcon >= 0) { App.GhostChannelMask ^= (1 << ghostIcon); ConditionalInvalidate(); return; } } if (IsMouseInHeader(e)) { if (left) { int frame = (int)Math.Round((e.X - trackNameSizeX + scrollX) / (float)patternSizeX * Song.PatternLength); App.Seek(frame); } else if (right && canCapture) { StartCaptureOperation(e, CaptureOperation.Select); UpdateSelection(e.X, true); } } else if (e.Y > headerSizeY && (left || right)) { if (e.Y > headerSizeY) { var newChannel = Utils.Clamp((e.Y - headerSizeY) / trackSizeY, 0, Song.Channels.Length - 1); if (newChannel != selectedChannel) { selectedChannel = newChannel; SelectedChannelChanged?.Invoke(selectedChannel); ConditionalInvalidate(); } } } bool inPatternZone = GetPatternForCoord(e.X, e.Y, out int channelIdx, out int patternIdx); if (inPatternZone) { var channel = Song.Channels[channelIdx]; var pattern = channel.PatternInstances[patternIdx]; if (left) { bool shift = ModifierKeys.HasFlag(Keys.Shift); if (pattern == null && !shift) { App.UndoRedoManager.BeginTransaction(TransactionScope.Song, Song.Id); channel.PatternInstances[patternIdx] = channel.CreatePattern(); PatternClicked?.Invoke(channelIdx, patternIdx); App.UndoRedoManager.EndTransaction(); ClearSelection(); ConditionalInvalidate(); } else if (canCapture) { if (pattern != null) { PatternClicked?.Invoke(channelIdx, patternIdx); } if (shift && minSelectedChannelIdx >= 0 && minSelectedPatternIdx >= 0) { if (channelIdx < minSelectedChannelIdx) { maxSelectedChannelIdx = minSelectedChannelIdx; minSelectedChannelIdx = channelIdx; } else { maxSelectedChannelIdx = channelIdx; } if (patternIdx < minSelectedPatternIdx) { maxSelectedPatternIdx = minSelectedPatternIdx; minSelectedPatternIdx = patternIdx; } else { maxSelectedPatternIdx = patternIdx; } return; } else if (!IsPatternSelected(channelIdx, patternIdx) && pattern != null) { minSelectedChannelIdx = channelIdx; maxSelectedChannelIdx = channelIdx; minSelectedPatternIdx = patternIdx; maxSelectedPatternIdx = patternIdx; } selectionDragAnchorX = e.X - trackNameSizeX + scrollX - minSelectedPatternIdx * patternSizeX; StartCaptureOperation(e, CaptureOperation.ClickPattern); ConditionalInvalidate(); } } else if (right && pattern != null) { App.UndoRedoManager.BeginTransaction(TransactionScope.Song, Song.Id); channel.PatternInstances[patternIdx] = null; App.UndoRedoManager.EndTransaction(); ClearSelection(); ConditionalInvalidate(); } } }
private void OscilloscopeThread() { var waitEvents = new WaitHandle[] { stopEvent, samplesEvent }; while (true) { int idx = WaitHandle.WaitAny(waitEvents); if (idx == 0) { break; } do { if (sampleQueue.TryDequeue(out var samples)) { Debug.Assert(samples.Length <= NumSamples); // Append to circular buffer. if (bufferPos + samples.Length < sampleBuffer.Length) { Array.Copy(samples, 0, sampleBuffer, bufferPos, samples.Length); bufferPos += samples.Length; } else { int batchSize1 = sampleBuffer.Length - bufferPos; int batchSize2 = samples.Length - batchSize1; Array.Copy(samples, 0, sampleBuffer, bufferPos, batchSize1); Array.Copy(samples, batchSize1, sampleBuffer, 0, batchSize2); bufferPos = batchSize2; } bool updateGeometry = (bufferPos + sampleBuffer.Length - renderIdx) % sampleBuffer.Length >= NumSamples; if (updateGeometry) { int lookback = 0; int maxLookback = NumSamples / 2; int centerIdx = (renderIdx + NumSamples / 2) % sampleBuffer.Length; int orig = sampleBuffer[centerIdx]; // If sample is negative, go back until positive. if (orig < 0) { while (lookback < maxLookback) { if (--centerIdx < 0) { centerIdx += sampleBuffer.Length; } if (sampleBuffer[centerIdx] > 0) { break; } lookback++; } orig = sampleBuffer[centerIdx]; } // Then look for a zero crossing. if (orig > 0) { while (lookback < maxLookback) { if (--centerIdx < 0) { centerIdx += sampleBuffer.Length; } if (sampleBuffer[centerIdx] < 0) { break; } lookback++; } } var newHasNonZeroData = false; // Build geometry, 8:1 sample to vertex ratio (4:1 if 2x scaling). var vertices = new float[numVertices, 2]; var samplesPerVertex = NumSamples / numVertices; // Assumed to be perfectly divisible. int j = centerIdx - NumSamples / 2; if (j < 0) { j += sampleBuffer.Length; } for (int i = 0; i < numVertices; i++) { int avg = 0; for (int k = 0; k < samplesPerVertex; k++, j = (j + 1) % sampleBuffer.Length) { avg += sampleBuffer[j]; } avg /= samplesPerVertex; vertices[i, 0] = i / (float)(numVertices - 1); vertices[i, 1] = Utils.Clamp(avg / 32768.0f * SampleScale, -1.0f, 1.0f); newHasNonZeroData |= Math.Abs(avg) > 1024.0f; } renderIdx = j; geometry = vertices; hasNonZeroData = newHasNonZeroData; } } else { break; } }while (!sampleQueue.IsEmpty); } }
public static void Load() { var ini = new IniFile(); ini.Load(GetConfigFileName()); Version = ini.GetInt("General", "Version", 0); ShowTutorial = ini.GetBool("UI", "ShowTutorial", true); DpiScaling = ini.GetInt("UI", "DpiScaling", 0); TimeFormat = ini.GetInt("UI", "TimeFormat", 0); FollowMode = ini.GetInt("UI", "FollowMode", FollowModeContinuous); FollowSync = ini.GetInt("UI", "FollowSync", FollowSyncBoth); CheckUpdates = ini.GetBool("UI", "CheckUpdates", true); ShowNoteLabels = ini.GetBool("UI", "ShowNoteLabels", true); ShowPianoRollViewRange = ini.GetBool("UI", "ShowPianoRollViewRange", true); TrackPadControls = ini.GetBool("UI", "TrackPadControls", false); ReverseTrackPad = ini.GetBool("UI", "ReverseTrackPad", false); InstrumentStopTime = ini.GetInt("Audio", "InstrumentStopTime", 2); MidiDevice = ini.GetString("MIDI", "Device", ""); SquareSmoothVibrato = ini.GetBool("Audio", "SquareSmoothVibrato", true); NoDragSoungWhenPlaying = ini.GetBool("Audio", "NoDragSoungWhenPlaying", false); LastFileFolder = ini.GetString("Folders", "LastFileFolder", ""); LastInstrumentFolder = ini.GetString("Folders", "LastInstrumentFolder", ""); LastSampleFolder = ini.GetString("Folders", "LastSampleFolder", ""); LastExportFolder = ini.GetString("Folders", "LastExportFolder", ""); FFmpegExecutablePath = ini.GetString("FFmpeg", "ExecutablePath", ""); if (DpiScaling != 100 && DpiScaling != 150 && DpiScaling != 200) { DpiScaling = 0; } InstrumentStopTime = Utils.Clamp(InstrumentStopTime, 0, 10); if (MidiDevice == null) { MidiDevice = ""; } if (!Directory.Exists(LastInstrumentFolder)) { LastInstrumentFolder = ""; } if (!Directory.Exists(LastSampleFolder)) { LastSampleFolder = ""; } if (!Directory.Exists(LastExportFolder)) { LastExportFolder = ""; } // Try to point to the demo songs initially. if (string.IsNullOrEmpty(LastFileFolder) || !Directory.Exists(LastFileFolder)) { var appPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); var demoSongsPath = Path.Combine(appPath, "Demo Songs"); LastFileFolder = Directory.Exists(demoSongsPath) ? demoSongsPath : ""; } #if FAMISTUDIO_LINUX || FAMISTUDIO_MACOS // Linux or Mac is more likely to have standard path for ffmpeg. if (string.IsNullOrEmpty(FFmpegExecutablePath) || !File.Exists(FFmpegExecutablePath)) { if (File.Exists("/usr/bin/ffmpeg")) { FFmpegExecutablePath = "/usr/bin/ffmpeg"; } else if (File.Exists("/usr/local/bin/ffmpeg")) { FFmpegExecutablePath = "/usr/local/bin/ffmpeg"; } else { FFmpegExecutablePath = "ffmpeg"; // Hope for the best! } } #endif // No deprecation at the moment. Version = SettingsVersion; }
private void CreateSlideNotes(Song s, Dictionary <Pattern, RowFxData[, ]> patternFxData) { var processedPatterns = new HashSet <Pattern>(); // Convert slide notes + portamento to our format. foreach (var c in s.Channels) { if (!c.SupportsSlideNotes) { continue; } var songSpeed = s.FamitrackerSpeed; var lastNoteInstrument = (Instrument)null; var lastNoteArpeggio = (Arpeggio)null; var lastNoteValue = (byte)Note.NoteInvalid; var portamentoSpeed = 0; var slideSpeed = 0; var slideShift = c.IsN163WaveChannel ? 2 : 0; var slideSign = c.IsN163WaveChannel || c.IsFdsWaveChannel ? -1 : 1; // Inverted channels. for (int p = 0; p < s.Length; p++) { var pattern = c.PatternInstances[p]; if (pattern == null) { continue; } var patternLen = s.GetPatternLength(p); for (var it = pattern.GetNoteIterator(0, patternLen); !it.Done; it.Next()) { var n = it.CurrentTime; var note = it.CurrentNote; // Look for speed changes. s.ApplySpeedEffectAt(p, n, ref songSpeed); if (!patternFxData.ContainsKey(pattern) || processedPatterns.Contains(pattern)) { continue; } var fxData = patternFxData[pattern]; var slideTarget = 0; for (int i = 0; i < fxData.GetLength(1); i++) { var fx = fxData[n, i]; if (fx.param != 0) { // When the effect it turned on, we need to add a note. if ((fx.fx == Effect_PortaUp || fx.fx == Effect_PortaDown || fx.fx == Effect_SlideUp || fx.fx == Effect_SlideDown) && lastNoteValue >= Note.MusicalNoteMin && lastNoteValue <= Note.MusicalNoteMax && (note == null || !note.IsValid)) { if (note == null) { note = pattern.GetOrCreateNoteAt(n); it.Resync(); } note.Value = lastNoteValue; note.Instrument = lastNoteInstrument; note.Arpeggio = lastNoteArpeggio; note.HasAttack = false; } } if (fx.fx == Effect_PortaUp) { // If we have a Qxx/Rxx on the same row as a 1xx/2xx, things get weird. if (slideTarget == 0) { slideSpeed = (-fx.param * slideSign) << slideShift; } } if (fx.fx == Effect_PortaDown) { // If we have a Qxx/Rxx on the same row as a 1xx/2xx, things get weird. if (slideTarget == 0) { slideSpeed = (fx.param * slideSign) << slideShift; } } if (fx.fx == Effect_Portamento) { portamentoSpeed = fx.param; } if (fx.fx == Effect_SlideUp && note != null && note.IsMusical) { slideTarget = Utils.Clamp(note.Value + (fx.param & 0xf), Note.MusicalNoteMin, Note.MusicalNoteMax); slideSpeed = (-((fx.param >> 4) * 2 + 1)) << slideShift; } if (fx.fx == Effect_SlideDown && note != null && note.IsMusical) { slideTarget = Utils.Clamp(note.Value - (fx.param & 0xf), Note.MusicalNoteMin, Note.MusicalNoteMax); slideSpeed = (((fx.param >> 4) * 2 + 1)) << slideShift; } } // Create a slide note. if (note != null && !note.IsSlideNote) { if (note.IsMusical) { var slideSource = note.Value; var noteTable = NesApu.GetNoteTableForChannelType(c.Type, s.Project.PalMode, s.Project.ExpansionNumChannels); var pitchLimit = NesApu.GetPitchLimitForChannelType(c.Type); // If we have a new note with auto-portamento enabled, we need to // swap the notes since our slide notes work backward compared to // FamiTracker. if (portamentoSpeed != 0) { // Ignore notes with no attack since we created them to handle a previous slide. if (note.HasAttack && lastNoteValue >= Note.MusicalNoteMin && lastNoteValue <= Note.MusicalNoteMax) { slideSpeed = portamentoSpeed; slideTarget = note.Value; slideSource = lastNoteValue; note.Value = lastNoteValue; } } // Our implementation of VRC7 pitches is quite different from FamiTracker. // Compensate for larger pitches in higher octaves by shifting. We cant shift by // a large amount because the period is 9-bit and FamiTracker is restricted to // this for slides (octave never changes). var octaveSlideShift = c.IsVrc7FmChannel && note.Value >= 12 ? 1 : 0; // 3xx/Qxy/Rxy : We know which note we are sliding to and the speed, but we // don't know how many frames it will take to get there. if (slideTarget != 0) { // Advance in the song until we have the correct number of frames. var numFrames = Math.Max(1, Math.Abs((noteTable[slideSource] - noteTable[slideTarget]) / (slideSpeed << octaveSlideShift))); note.SlideNoteTarget = (byte)slideTarget; // TODO: Here we consider if the start note has a delay, but ignore the end note. It might have one too. var np = p; var nn = n; s.AdvanceNumberOfFrames(numFrames, note.HasNoteDelay ? -note.NoteDelay : 0, songSpeed, s.Project.PalMode, ref np, ref nn); // Still to see if there is a note between the current one and the // next note, this could append if you add a note before the slide // is supposed to finish. if (FindNextSlideEffect(c, p, n, out var np2, out var nn2, patternFxData)) { if (np2 < np) { np = np2; nn = nn2; } else if (np2 == np) { nn = Math.Min(nn, nn2); } // If the slide is interrupted by another slide effect, we will not reach // the final target, but rather some intermediate note. Let's do our best // to interpolate and figure out the best note. var numFramesUntilNextSlide = s.CountFramesBetween(p, n, np, nn, songSpeed, s.Project.PalMode); var ratio = Utils.Clamp(numFramesUntilNextSlide / numFrames, 0.0f, 1.0f); var intermediatePitch = (int)Math.Round(Utils.Lerp(noteTable[slideSource], noteTable[slideTarget], ratio)); slideTarget = FindBestMatchingNote(noteTable, intermediatePitch, Math.Sign(slideSpeed)); note.SlideNoteTarget = (byte)slideTarget; } if (np < s.Length) { // Add an extra note with no attack to stop the slide. var nextPattern = c.PatternInstances[np]; if (!nextPattern.Notes.TryGetValue(nn, out var nextNote) || !nextNote.IsValid) { nextNote = nextPattern.GetOrCreateNoteAt(nn); nextNote.Instrument = note.Instrument; nextNote.Value = (byte)slideTarget; nextNote.HasAttack = false; it.Resync(); } else if (nextNote != null && nextNote.IsRelease) { Log.LogMessage(LogSeverity.Warning, $"A slide note ends on a release note. This is currently unsupported and will require manual correction. {GetPatternString(nextPattern, nn)}"); } } // 3xx, Qxx and Rxx stops when its done. slideSpeed = 0; } // 1xx/2xy : We know the speed at which we are sliding, but need to figure out what makes it stop. else if (slideSpeed != 0 && FindNextSlideEffect(c, p, n, out var np, out var nn, patternFxData)) { // See how many frames until the slide stops. var numFrames = (int)Math.Round(s.CountFramesBetween(p, n, np, nn, songSpeed, s.Project.PalMode)); // TODO: Here we consider if the start note has a delay, but ignore the end note. It might have one too. numFrames = Math.Max(1, numFrames - (note.HasNoteDelay ? note.NoteDelay : 0)); // Compute the pitch delta and find the closest target note. var newNotePitch = Utils.Clamp(noteTable[slideSource] + numFrames * (slideSpeed << octaveSlideShift), 0, pitchLimit); var newNote = FindBestMatchingNote(noteTable, newNotePitch, Math.Sign(slideSpeed)); note.SlideNoteTarget = (byte)newNote; // If the FX was turned off, we need to add an extra note. var nextPattern = c.PatternInstances[np]; if (!nextPattern.Notes.TryGetValue(nn, out var nextNote) || !nextNote.IsValid) { nextNote = nextPattern.GetOrCreateNoteAt(nn); nextNote.Instrument = note.Instrument; nextNote.Value = (byte)newNote; nextNote.HasAttack = false; it.Resync(); } else if (nextNote != null && nextNote.IsRelease) { Log.LogMessage(LogSeverity.Warning, $"A slide note ends on a release note. This is currently unsupported and will require manual correction. {GetPatternString(nextPattern, nn)}"); } } } } if (note != null && (note.IsMusical || note.IsStop)) { lastNoteValue = note.IsSlideNote ? note.SlideNoteTarget : note.Value; lastNoteInstrument = note.Instrument; lastNoteArpeggio = note.Arpeggio; } } processedPatterns.Add(pattern); } } }
void TreeView_ButtonPressEvent(object o, ButtonPressEventArgs args) { for (int i = 0; i < properties.Count; i++) { var prop = properties[i]; if (prop.control is ScrolledWindow scroll) { if (scroll.Child is TreeView treeView) { if (treeView.GetPathAtPos((int)args.Event.X, (int)args.Event.Y, out var path, out var col, out var ix, out var iy)) { var columnIndex = Array.IndexOf(treeView.Columns, col); var columnDesc = prop.columns[columnIndex]; if (columnDesc.Enabled) { if (columnDesc.Type == ColumnType.Slider && args.Event.Button == 1) { var area = treeView.GetCellArea(path, col); dragPath = path; dragColumn = col; dragPropertyIndex = i; dragRowIndex = path.Indices[0]; dragColIndex = columnIndex; var percent = (int)Utils.Clamp(Math.Round((args.Event.X - area.Left) / (float)area.Width * 100.0f), 0.0f, 100.0f); if (treeView.Model.GetIter(out var iter, path)) { treeView.Model.SetValue(iter, columnIndex, percent); } var propIdx = GetPropertyIndex(treeView.Parent); PropertyChanged?.Invoke(this, propIdx, dragRowIndex, dragColIndex, percent); } else if (columnDesc.Type == ColumnType.Button) { var cellArea = treeView.GetBackgroundArea(path, col); var button = treeView.Columns[columnIndex].CellRenderers[0] as CellRendererButton; var buttonRect = button.GetButtonRectangle(cellArea); if (buttonRect.Contains((int)args.Event.X, (int)args.Event.Y)) { PropertyClicked?.Invoke(this, ClickType.Button, i, path.Indices[0], columnIndex); } } else if (columnDesc.Type == ColumnType.DropDown) { // Open the combo box right away, otherwise we need to click twice. var column = treeView.Columns[columnIndex]; var combo = column.CellRenderers[0] as CellRendererCombo; treeView.SetCursorOnCell(path, column, combo, true); } else { if (args.Event.Type == EventType.TwoButtonPress) { PropertyClicked?.Invoke(this, ClickType.Double, i, path.Indices[0], columnIndex); } else if (args.Event.Button == 3) { PropertyClicked?.Invoke(this, ClickType.Right, i, path.Indices[0], columnIndex); } } } } } } } }