Exemple #1
0
        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;
                        }
                    }
                }
            }
        }
Exemple #2
0
        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);
        }
Exemple #3
0
        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;
        }
Exemple #4
0
        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);
            }
        }
Exemple #5
0
        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();
                }
            }
        }
Exemple #6
0
        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;
                        }
                    }
                }
            }
        }
Exemple #8
0
        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();
                }
            }
        }
Exemple #9
0
        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);
            }
        }
Exemple #10
0
        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);
                }
            }
        }
Exemple #12
0
        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);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }