public ChannelState(int apu, int type, bool pal, int numN163Channels = 1) { apuIdx = apu; channelType = type; maximumPeriod = NesApu.GetPitchLimitForChannelType(channelType); noteTable = NesApu.GetNoteTableForChannelType(channelType, pal, numN163Channels); note.Value = Note.NoteStop; note.Volume = Note.VolumeMax; note.FinePitch = 0; Channel.GetShiftsForType(type, numN163Channels, out pitchShift, out slideShift); }
public ChannelState(IPlayerInterface play, int apu, int type, bool pal, int numN163Channels = 1) { player = play; apuIdx = apu; channelType = type; palPlayback = pal; maximumPeriod = NesApu.GetPitchLimitForChannelType(channelType); noteTable = NesApu.GetNoteTableForChannelType(channelType, pal, numN163Channels); note.Value = Note.NoteStop; note.FinePitch = 0; Channel.GetShiftsForType(type, numN163Channels, out pitchShift, out slideShift); }
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); } } }
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 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.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; } } } } }
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; } } } } }