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 || c.IsVrc7FmChannel ? -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.GetDenseNoteIterator(0, patternLen); !it.Done; it.Next()) { var location = new NoteLocation(p, it.CurrentTime); var note = it.CurrentNote; // Look for speed changes. s.ApplySpeedEffectAt(location, 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[location.NoteIndex, 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(location.NoteIndex); 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 nextLocation = location; s.AdvanceNumberOfFrames(ref nextLocation, numFrames, note.HasNoteDelay ? -note.NoteDelay : 0, songSpeed, s.Project.PalMode); // 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, location, out var nextLocation2, patternFxData)) { nextLocation = NoteLocation.Min(nextLocation, nextLocation2); // 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(location, nextLocation, 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 (nextLocation.PatternIndex < s.Length) { // Add an extra note with no attack to stop the slide. var nextPattern = c.PatternInstances[nextLocation.PatternIndex]; if (!nextPattern.Notes.TryGetValue(nextLocation.NoteIndex, out var nextNote) || !nextNote.IsValid) { nextNote = nextPattern.GetOrCreateNoteAt(nextLocation.NoteIndex); 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, nextLocation.NoteIndex)}"); } } // 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, location, out var nextLocation, patternFxData)) { // See how many frames until the slide stops. var numFrames = (int)Math.Round(s.CountFramesBetween(location, nextLocation, 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[nextLocation.PatternIndex]; if (!nextPattern.Notes.TryGetValue(nextLocation.NoteIndex, out var nextNote) || !nextNote.IsValid) { nextNote = nextPattern.GetOrCreateNoteAt(nextLocation.NoteIndex); 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, nextLocation.NoteIndex)}"); } } } } if (note != null && (note.IsMusical || note.IsStop)) { lastNoteValue = note.IsSlideNote ? note.SlideNoteTarget : note.Value; lastNoteInstrument = note.Instrument; lastNoteArpeggio = note.Arpeggio; } } processedPatterns.Add(pattern); } } }