コード例 #1
0
ファイル: VideoFile.cs プロジェクト: LevelASMer/FamiStudio
        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));
                        }
                    }
                }
            }
        }
コード例 #2
0
        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);
                }
            }
        }