private void Props_PropertyChanged(PropertyPage props, int propIdx, int rowIdx, int colIdx, object value) { if (song.UsesFamiTrackerTempo) { var tempo = song.FamitrackerTempo; var speed = song.FamitrackerSpeed; if (propIdx == famitrackerTempoPropIdx || propIdx == famitrackerSpeedPropIdx) { tempo = props.GetPropertyValue <int>(famitrackerTempoPropIdx); speed = props.GetPropertyValue <int>(famitrackerSpeedPropIdx); } var beatLength = props.GetPropertyValue <int>(notesPerBeatPropIdx); props.SetLabelText(bpmLabelPropIdx, Song.ComputeFamiTrackerBPM(song.Project.PalMode, speed, tempo, beatLength).ToString("n1")); } else { var notesPerBeat = props.GetPropertyValue <int>(notesPerBeatPropIdx); // Changing the number of notes in a beat will affect the list of available BPMs. if (propIdx == notesPerBeatPropIdx) { tempoList = FamiStudioTempoUtils.GetAvailableTempos(song.Project.PalMode, notesPerBeat); tempoStrings = tempoList.Select(t => t.bpm.ToString("n1") + (t.groove.Length == 1 ? " *" : "")).ToArray(); props.UpdateDropDownListItems(famistudioBpmPropIdx, tempoStrings); } // Changing the BPM affects the grooves and note length. if (propIdx == famistudioBpmPropIdx || propIdx == notesPerBeatPropIdx) { var tempoIndex = Array.IndexOf(tempoStrings, props.GetPropertyValue <string>(famistudioBpmPropIdx)); var tempoInfo = tempoList[tempoIndex]; var framesPerNote = Utils.Min(tempoInfo.groove); props.UpdateIntegerRange(notesPerPatternPropIdx, 1, Pattern.MaxLength / framesPerNote); var grooveList = FamiStudioTempoUtils.GetAvailableGrooves(tempoInfo.groove); grooveStrings = grooveList.Select(g => string.Join("-", g)).ToArray(); props.UpdateDropDownListItems(groovePropIdx, grooveStrings); props.SetLabelText(framesPerNotePropIdx, framesPerNote.ToString()); } } UpdateWarnings(); }
public static byte[] GetTempoEnvelope(int[] groove, int groovePadMode, bool palSource) { // Look in the cache first. var key = new CachedTempoEnvelopeKey() { groove = groove, groovePadMode = groovePadMode, palSource = palSource }; if (cachedTempoEnvelopes.TryGetValue(key, out var env)) { return(env); } // Otherwise build. var dstFactor = palSource ? 6 : 5; var srcFactor = palSource ? 5 : 6; var noteLength = Utils.Min(groove); var grooveNumFrames = Utils.Sum(groove); var grooveRepeatCount = 1; // Repeat the groove until we have something perfectly divisible by 6 (5 on PAL). while ((grooveNumFrames % srcFactor) != 0) { grooveNumFrames += Utils.Sum(groove); grooveRepeatCount++; } // Figure out how many frames that is on the playback machine. var adaptedNumFrames = grooveNumFrames / srcFactor * dstFactor; // Mark some frames as "important", this will typically be the first // and last frame of the note. This will preserve the attack and // 1-frame silence between notes. var importantFrames = new bool[grooveNumFrames]; var frameIndex = 0; for (int i = 0; i < grooveRepeatCount; i++) { for (int j = 0; j < groove.Length; j++) { if (groove[j] == noteLength) { importantFrames[frameIndex] = true; importantFrames[frameIndex + noteLength - 1] = true; } else { if (groovePadMode != GroovePaddingType.Beginning || noteLength == 1) { importantFrames[frameIndex] = true; } else { importantFrames[frameIndex + 1] = true; } if (groovePadMode != GroovePaddingType.End || noteLength == 1) { importantFrames[frameIndex + noteLength] = true; } else { importantFrames[frameIndex + noteLength - 1] = true; } } frameIndex += groove[j]; } } #if FALSE var numSkipFrames = palSource ? adaptedNumFrames - grooveNumFrames : grooveNumFrames - adaptedNumFrames; var bestScore = int.MaxValue; var bestOffset = -1; for (int i = 0; i < srcFactor; i++) { var score = 0; frameIndex = i; for (int j = 0; j < numSkipFrames; j++) { if (importantFrames[frameIndex]) { score++; } frameIndex += srcFactor; } if (score < bestScore) { bestScore = score; bestOffset = i; } } #else // Start by distributing the skip (or double) frames evenly. var numSkipFrames = palSource ? adaptedNumFrames - grooveNumFrames : grooveNumFrames - adaptedNumFrames; var skipFrames = new bool[grooveNumFrames]; frameIndex = srcFactor / 2; for (int i = 0; i < numSkipFrames; i++) { skipFrames[frameIndex] = true; frameIndex += srcFactor; } int GetFrameCost(int idx) { if (!skipFrames[idx]) { return(0); } var cost = 0; // Penalize important frames if (importantFrames[idx]) { cost += srcFactor; } // Look right for another skipped frame. for (int i = 1; i < srcFactor; i++) { var nextIdx = idx + i; if (nextIdx >= skipFrames.Length) { nextIdx -= skipFrames.Length; } if (skipFrames[nextIdx]) { // The closer we are, the higher the cost. cost += (srcFactor - i); break; } } // Look left for another skipped frame. for (int i = 1; i < srcFactor; i++) { var prevIdx = idx - i; if (prevIdx < 0) { prevIdx += skipFrames.Length; } // The closer we are, the higher the cost. if (skipFrames[prevIdx]) { cost += (srcFactor - i); break; } } return(cost); } var frameCosts = new int[grooveNumFrames]; // Optimize. for (int i = 0; i < 100; i++) { // Update costs. var maxCost = -10; var maxCostIndex = -1; var totalCost = 0; for (int j = 0; j < frameCosts.Length; j++) { var cost = GetFrameCost(j); frameCosts[j] = cost; totalCost += cost; if (cost > maxCost) { maxCost = cost; maxCostIndex = j; } } if (maxCost == 0) { break; } var currentFrameCost = GetFrameCost(maxCostIndex); // Try to optimize the most expensive frame by moving it to the left. if (maxCostIndex > 0 && !skipFrames[maxCostIndex - 1] && !importantFrames[maxCostIndex - 1]) { Utils.Swap(ref skipFrames[maxCostIndex], ref skipFrames[maxCostIndex - 1]); if (GetFrameCost(maxCostIndex - 1) < currentFrameCost) { continue; } Utils.Swap(ref skipFrames[maxCostIndex], ref skipFrames[maxCostIndex - 1]); } // Try to optimize the most expensive frame by moving it to the right. if (maxCostIndex < skipFrames.Length - 1 && !skipFrames[maxCostIndex + 1] && !importantFrames[maxCostIndex + 1]) { Utils.Swap(ref skipFrames[maxCostIndex], ref skipFrames[maxCostIndex + 1]); if (GetFrameCost(maxCostIndex + 1) < currentFrameCost) { continue; } Utils.Swap(ref skipFrames[maxCostIndex], ref skipFrames[maxCostIndex + 1]); } break; } #endif // Build the actual envelope. var lastFrameIndex = -1; var firstFrameIndex = -1; var envelope = new List <byte>(); var sum = 0; for (int i = 0; i < skipFrames.Length; i++) { if (skipFrames[i]) { var frameDelta = i - lastFrameIndex; envelope.Add((byte)(frameDelta + (palSource ? 1 : -1))); sum += frameDelta; lastFrameIndex = i; if (firstFrameIndex < 0) { firstFrameIndex = i; } } } if (palSource) { envelope[0]--; } var remainingFrames = skipFrames.Length - sum; if (remainingFrames != 0) { envelope.Add((byte)(remainingFrames + firstFrameIndex + 1 + (palSource ? 1 : -1))); } envelope.Add(0x80); env = envelope.ToArray(); cachedTempoEnvelopes[key] = env; return(env); }
public Project Load(string filename) { #if !DEBUG try #endif { var lines = File.ReadAllLines(filename); var parameters = new Dictionary <string, string>(); var project = (Project)null; var instrument = (Instrument)null; var arpeggio = (Arpeggio)null; var song = (Song)null; var channel = (Channel)null; var pattern = (Pattern)null; SetInvariantCulture(); foreach (var line in lines) { var cmd = SplitLine(line.Trim(), ref parameters); switch (cmd) { case "Project": { project = new Project(); parameters.TryGetValue("Version", out var version); if (parameters.TryGetValue("Name", out var name)) { project.Name = name; } if (parameters.TryGetValue("Author", out var author)) { project.Author = author; } if (parameters.TryGetValue("Copyright", out var copyright)) { project.Copyright = copyright; } if (parameters.TryGetValue("TempoMode", out var tempoMode)) { project.TempoMode = TempoType.GetValueForName(tempoMode); } if (parameters.TryGetValue("PAL", out var pal)) { project.PalMode = bool.Parse(pal); } if (parameters.TryGetValue("Expansions", out var expansions)) { var expansionMask = 0; var expansionStrings = expansions.Split(','); foreach (var s in expansionStrings) { var exp = ExpansionType.GetValueForShortName(s.Trim()); expansionMask |= ExpansionType.GetMaskFromValue(exp); } var numN163Channels = 1; if ((expansionMask & ExpansionType.N163Mask) != 0 && parameters.TryGetValue("NumN163Channels", out var numN163ChannelsStr)) { numN163Channels = int.Parse(numN163ChannelsStr); } project.SetExpansionAudioMask(expansionMask, numN163Channels); } if (!version.StartsWith("3.2")) { Log.LogMessage(LogSeverity.Error, "File was created with an incompatible version of FamiStudio. The text format is only compatible with the current version."); return(null); } break; } case "DPCMSample": { var str = parameters["Data"]; var data = new byte[str.Length / 2]; for (int i = 0; i < data.Length; i++) { data[i] = Convert.ToByte(str.Substring(i * 2, 2), 16); } var sample = project.CreateDPCMSampleFromDmcData(parameters["Name"], data); break; } case "DPCMMapping": { var pitch = 15; var loop = false; if (parameters.TryGetValue("Pitch", out var pitchStr)) { pitch = int.Parse(pitchStr); } if (parameters.TryGetValue("Loop", out var loopStr)) { loop = bool.Parse(loopStr); } project.MapDPCMSample(Note.FromFriendlyName(parameters["Note"]), project.GetSample(parameters["Sample"]), pitch, loop); break; } case "Instrument": { var instrumentExp = ExpansionType.None; if (parameters.TryGetValue("Expansion", out var instrumentExpStr)) { instrumentExp = ExpansionType.GetValueForShortName(instrumentExpStr); } instrument = project.CreateInstrument(instrumentExp, parameters["Name"]); if (instrument.IsFdsInstrument) { if (parameters.TryGetValue("FdsWavePreset", out var wavPresetStr)) { instrument.FdsWavePreset = (byte)WavePresetType.GetValueForName(wavPresetStr); } if (parameters.TryGetValue("FdsModPreset", out var modPresetStr)) { instrument.FdsWavePreset = (byte)WavePresetType.GetValueForName(modPresetStr); } if (parameters.TryGetValue("FdsMasterVolume", out var masterVolumeStr)) { instrument.FdsMasterVolume = byte.Parse(masterVolumeStr); } if (parameters.TryGetValue("FdsModSpeed", out var fdsModSpeedStr)) { instrument.FdsModSpeed = ushort.Parse(fdsModSpeedStr); } if (parameters.TryGetValue("FdsModDepth", out var fdsModDepthStr)) { instrument.FdsModDepth = byte.Parse(fdsModDepthStr); } if (parameters.TryGetValue("FdsModDelay", out var fdsModDelayStr)) { instrument.FdsModDelay = byte.Parse(fdsModDelayStr); } } else if (instrument.IsN163Instrument) { if (parameters.TryGetValue("N163WavePreset", out var wavPresetStr)) { instrument.N163WavePreset = (byte)WavePresetType.GetValueForName(wavPresetStr); } if (parameters.TryGetValue("N163WaveSize", out var n163WavSizeStr)) { instrument.N163WaveSize = byte.Parse(n163WavSizeStr); } if (parameters.TryGetValue("N163WavePos", out var n163WavPosStr)) { instrument.N163WavePos = byte.Parse(n163WavPosStr); } } else if (instrument.IsVrc6Instrument) { if (parameters.TryGetValue("Vrc6SawMasterVolume", out var vrc6SawVolumeStr)) { instrument.Vrc6SawMasterVolume = (byte)Vrc6SawMasterVolumeType.GetValueForName(vrc6SawVolumeStr); } } else if (instrument.IsVrc7Instrument) { if (parameters.TryGetValue("Vrc7Patch", out var vrc7PatchStr)) { instrument.Vrc7Patch = byte.Parse(vrc7PatchStr); } if (instrument.Vrc7Patch == Vrc7InstrumentPatch.Custom) { for (int i = 0; i < 8; i++) { if (parameters.TryGetValue($"Vrc7Reg{i}", out var regStr)) { instrument.Vrc7PatchRegs[i] = byte.Parse(regStr); } } } } break; } case "Arpeggio": { arpeggio = project.CreateArpeggio(parameters["Name"]); arpeggio.Envelope.Length = int.Parse(parameters["Length"]); if (parameters.TryGetValue("Loop", out var loopStr)) { arpeggio.Envelope.Loop = int.Parse(loopStr); } var values = parameters["Values"].Split(','); for (int j = 0; j < values.Length; j++) { arpeggio.Envelope.Values[j] = sbyte.Parse(values[j]); } break; } case "Envelope": { var env = instrument.Envelopes[EnvelopeType.GetValueForShortName(parameters["Type"])]; if (env != null) { if (env.CanResize) { env.Length = int.Parse(parameters["Length"]); } if (parameters.TryGetValue("Loop", out var loopStr)) { env.Loop = int.Parse(loopStr); } if (parameters.TryGetValue("Release", out var releaseStr)) { env.Release = int.Parse(releaseStr); } if (parameters.TryGetValue("Relative", out var relativeStr)) { env.Relative = bool.Parse(relativeStr); } var values = parameters["Values"].Split(','); for (int j = 0; j < values.Length; j++) { env.Values[j] = sbyte.Parse(values[j]); } } break; } case "Song": { song = project.CreateSong(parameters["Name"]); song.SetLength(int.Parse(parameters["Length"])); song.SetBeatLength(int.Parse(parameters["BeatLength"])); song.SetLoopPoint(int.Parse(parameters["LoopPoint"])); if (song.UsesFamiTrackerTempo) { song.SetDefaultPatternLength(int.Parse(parameters["PatternLength"])); song.FamitrackerTempo = int.Parse(parameters["FamiTrackerTempo"]); song.FamitrackerSpeed = int.Parse(parameters["FamiTrackerSpeed"]); } else { var noteLength = int.Parse(parameters["NoteLength"]); var groove = parameters["Groove"].Split('-').Select(Int32.Parse).ToArray(); var groovePaddingMode = GroovePaddingType.GetValueForName(parameters["GroovePaddingMode"]); if (!FamiStudioTempoUtils.ValidateGroove(groove) || Utils.Min(groove) != noteLength) { Log.LogMessage(LogSeverity.Error, "Invalid tempo settings."); return(null); } song.ChangeFamiStudioTempoGroove(groove, false); song.SetBeatLength(song.BeatLength * noteLength); song.SetDefaultPatternLength(int.Parse(parameters["PatternLength"]) * noteLength); song.SetGroovePaddingMode(groovePaddingMode); } break; } case "PatternCustomSettings": { if (project.UsesFamiTrackerTempo) { var beatLength = song.BeatLength; if (parameters.TryGetValue("BeatLength", out var beatLengthStr)) { beatLength = int.Parse(beatLengthStr); } song.SetPatternCustomSettings(int.Parse(parameters["Time"]), int.Parse(parameters["Length"]), beatLength); } else { var patternLength = int.Parse(parameters["Length"]); var noteLength = int.Parse(parameters["NoteLength"]); var beatLength = int.Parse(parameters["BeatLength"]); var groove = parameters["Groove"].Split('-').Select(Int32.Parse).ToArray(); var groovePaddingMode = GroovePaddingType.GetValueForName(parameters["GroovePaddingMode"]); if (!FamiStudioTempoUtils.ValidateGroove(groove) || Utils.Min(groove) != noteLength) { Log.LogMessage(LogSeverity.Error, "Invalid tempo settings."); return(null); } song.SetPatternCustomSettings(int.Parse(parameters["Time"]), patternLength * noteLength, beatLength * noteLength, groove, groovePaddingMode); } break; } case "Channel": { var channelType = ChannelType.GetValueForShortName(parameters["Type"]); channel = song.GetChannelByType(channelType); break; } case "Pattern": { pattern = channel.CreatePattern(parameters["Name"]); break; } case "Note": { var time = int.Parse(parameters["Time"]); var note = pattern.GetOrCreateNoteAt(time); if (parameters.TryGetValue("Value", out var valueStr)) { note.Value = (byte)Note.FromFriendlyName(valueStr); } if (note.IsMusical && parameters.TryGetValue("Duration", out var durationStr)) { note.Duration = int.Parse(durationStr); } else if (note.IsStop) { note.Duration = 1; } if (note.IsMusical && parameters.TryGetValue("Release", out var releaseStr)) { note.Release = int.Parse(releaseStr); } if (note.IsMusical && parameters.TryGetValue("Instrument", out var instStr) && channel.SupportsInstrument(project.GetInstrument(instStr))) { note.Instrument = project.GetInstrument(instStr); } if (note.IsMusical && parameters.TryGetValue("Arpeggio", out var arpStr) && channel.SupportsArpeggios) { note.Arpeggio = project.GetArpeggio(arpStr); } if (note.IsMusical && parameters.TryGetValue("SlideTarget", out var slideStr) && channel.SupportsSlideNotes) { note.SlideNoteTarget = (byte)Note.FromFriendlyName(slideStr); } if (note.IsMusical && parameters.TryGetValue("Attack", out var attackStr)) { note.HasAttack = bool.Parse(attackStr); } if (parameters.TryGetValue("Volume", out var volumeStr) && channel.SupportsEffect(Note.EffectVolume)) { note.Volume = byte.Parse(volumeStr); } if (parameters.TryGetValue("VolumeSlideTarget", out var volumeSlideStr) && channel.SupportsEffect(Note.EffectVolumeSlide)) { note.VolumeSlideTarget = byte.Parse(volumeSlideStr); } if (parameters.TryGetValue("VibratoSpeed", out var vibSpeedStr) && channel.SupportsEffect(Note.EffectVibratoSpeed)) { note.VibratoSpeed = byte.Parse(vibSpeedStr); } if (parameters.TryGetValue("VibratoDepth", out var vibDepthStr) && channel.SupportsEffect(Note.EffectVibratoDepth)) { note.VibratoDepth = byte.Parse(vibDepthStr); } if (parameters.TryGetValue("Speed", out var speedStr) && channel.SupportsEffect(Note.EffectSpeed)) { note.Speed = byte.Parse(speedStr); } if (parameters.TryGetValue("FinePitch", out var finePitchStr) && channel.SupportsEffect(Note.EffectFinePitch)) { note.FinePitch = sbyte.Parse(finePitchStr); } if (parameters.TryGetValue("FdsModSpeed", out var modSpeedStr) && channel.SupportsEffect(Note.EffectFdsModSpeed)) { note.FdsModSpeed = ushort.Parse(modSpeedStr); } if (parameters.TryGetValue("FdsModDepth", out var modDepthStr) && channel.SupportsEffect(Note.EffectFdsModDepth)) { note.FdsModDepth = byte.Parse(modDepthStr); } if (parameters.TryGetValue("DutyCycle", out var dutyCycleStr) && channel.SupportsEffect(Note.EffectDutyCycle)) { note.DutyCycle = byte.Parse(dutyCycleStr); } if (parameters.TryGetValue("NoteDelay", out var noteDelayStr) && channel.SupportsEffect(Note.EffectNoteDelay)) { note.NoteDelay = byte.Parse(noteDelayStr); } if (parameters.TryGetValue("CutDelay", out var cutDelayStr) && channel.SupportsEffect(Note.EffectCutDelay)) { note.CutDelay = byte.Parse(cutDelayStr); } break; } case "PatternInstance": { var time = int.Parse(parameters["Time"]); channel.PatternInstances[time] = channel.GetPattern(parameters["Pattern"]); break; } } } project.SortEverything(false); ResetCulture(); return(project); } #if !DEBUG catch (Exception e) { Log.LogMessage(LogSeverity.Error, "Please contact the developer on GitHub!"); Log.LogMessage(LogSeverity.Error, e.Message); Log.LogMessage(LogSeverity.Error, e.StackTrace); ResetCulture(); return(null); } #endif }
public void ApplyAsync(bool custom, Action callback) { if (song.UsesFamiTrackerTempo) { if (patternIdx == -1) { if (famitrackerTempoPropIdx >= 0) { song.FamitrackerTempo = props.GetPropertyValue <int>(famitrackerTempoPropIdx); song.FamitrackerSpeed = props.GetPropertyValue <int>(famitrackerSpeedPropIdx); } song.SetBeatLength(props.GetPropertyValue <int>(notesPerBeatPropIdx)); song.SetDefaultPatternLength(props.GetPropertyValue <int>(notesPerPatternPropIdx)); } else { for (int i = minPatternIdx; i <= maxPatternIdx; i++) { var beatLength = props.GetPropertyValue <int>(notesPerBeatPropIdx); var patternLength = props.GetPropertyValue <int>(notesPerPatternPropIdx); if (custom) { song.SetPatternCustomSettings(i, patternLength, beatLength); } else { song.ClearPatternCustomSettings(i); } } } FinishApply(callback); } else { var tempoIndex = Array.IndexOf(tempoStrings, props.GetPropertyValue <string>(famistudioBpmPropIdx)); var tempoInfo = tempoList[tempoIndex]; var beatLength = props.GetPropertyValue <int>(notesPerBeatPropIdx); var patternLength = props.GetPropertyValue <int>(notesPerPatternPropIdx); var noteLength = Utils.Min(tempoInfo.groove); var grooveIndex = Array.IndexOf(grooveStrings, props.GetPropertyValue <string>(groovePropIdx)); var groovePadMode = GroovePaddingType.GetValueForName(props.GetPropertyValue <string>(groovePadPropIdx)); var grooveList = FamiStudioTempoUtils.GetAvailableGrooves(tempoInfo.groove); var groove = grooveList[grooveIndex]; props.UpdateIntegerRange(notesPerPatternPropIdx, 1, Pattern.MaxLength / noteLength); props.SetLabelText(framesPerNotePropIdx, noteLength.ToString()); if (patternIdx == -1) { ShowConvertTempoDialogAsync(noteLength != originalNoteLength, (c) => { song.ChangeFamiStudioTempoGroove(groove, c); song.SetBeatLength(beatLength * song.NoteLength); song.SetDefaultPatternLength(patternLength * song.NoteLength); song.SetGroovePaddingMode(groovePadMode); FinishApply(callback); }); } else { var actualNoteLength = song.NoteLength; var actualPatternLength = song.PatternLength; var actualBeatLength = song.BeatLength; if (custom) { actualNoteLength = noteLength; actualBeatLength = beatLength * noteLength; actualPatternLength = patternLength * noteLength; } var patternsToResize = new List <int>(); for (int i = minPatternIdx; i <= maxPatternIdx; i++) { if (actualNoteLength != song.GetPatternNoteLength(patternIdx)) { patternsToResize.Add(i); } } ShowConvertTempoDialogAsync(patternsToResize.Count > 0, (c) => { if (c) { foreach (var p in patternsToResize) { song.ResizePatternNotes(p, actualNoteLength); } } for (int i = minPatternIdx; i <= maxPatternIdx; i++) { if (custom) { song.SetPatternCustomSettings(i, actualPatternLength, actualBeatLength, groove, groovePadMode); } else { song.ClearPatternCustomSettings(i); } } FinishApply(callback); }); } } }
private void UpdateWarnings() { var numFramesPerPattern = 0; if (song.UsesFamiStudioTempo) { var tempoIndex = Array.IndexOf(tempoStrings, props.GetPropertyValue <string>(famistudioBpmPropIdx)); var tempoInfo = tempoList[tempoIndex]; var notesPerBeat = props.GetPropertyValue <int>(notesPerBeatPropIdx); var notesPerPattern = props.GetPropertyValue <int>(notesPerPatternPropIdx); if (tempoInfo.groove.Length == 1) { props.SetPropertyWarning(famistudioBpmPropIdx, CommentType.Good, "Ideal tempo : notes will be perfectly evenly divided."); } else if ((tempoInfo.groove.Length % notesPerBeat) == 0 || (notesPerBeat % tempoInfo.groove.Length) == 0) { props.SetPropertyWarning(famistudioBpmPropIdx, CommentType.Warning, "Beat-aligned groove : notes will be slightly uneven, but well aligned with the beat."); } else { props.SetPropertyWarning(famistudioBpmPropIdx, CommentType.Error, "Unaligned groove : notes will be slightly uneven and not aligned to the beat."); } if (notesPerBeat != 4) { props.SetPropertyWarning(notesPerBeatPropIdx, CommentType.Error, "A value of 4 is strongly recommended as it gives the best range of available BPMs."); } else { props.SetPropertyWarning(notesPerBeatPropIdx, CommentType.Good, "4 is the recommended value."); } var groovePadMode = GroovePaddingType.GetValueForName(props.GetPropertyValue <string>(groovePadPropIdx)); numFramesPerPattern = FamiStudioTempoUtils.ComputeNumberOfFrameForGroove(notesPerPattern * Utils.Min(tempoInfo.groove), tempoInfo.groove, groovePadMode); } else if (famitrackerSpeedPropIdx >= 0) { var speed = props.GetPropertyValue <int>(famitrackerSpeedPropIdx); var tempo = props.GetPropertyValue <int>(famitrackerTempoPropIdx); if (speed == 1) { props.SetPropertyWarning(famitrackerSpeedPropIdx, CommentType.Warning, $"A speed of 1 will not produce the same BPM between platforms (PAL/NTSC)."); } else { props.SetPropertyWarning(famitrackerSpeedPropIdx, CommentType.Good, ""); } if (tempo != 150) { props.SetPropertyWarning(famitrackerTempoPropIdx, CommentType.Warning, "A tempo of 150 is strongly recommended as it produces even notes on all platforms (NTSC/PAL)."); } else { props.SetPropertyWarning(famitrackerTempoPropIdx, CommentType.Good, "150 is the recommended value."); } } if (patternIdx >= 0 && numFramesPerPattern > song.PatternLength) { props.SetPropertyWarning(notesPerPatternPropIdx, CommentType.Warning, $"Pattern is longer than the song pattern length and FamiTracker does not support this. Ignore this if you are not planning to export to FamiTracker."); } else if (numFramesPerPattern >= 256) { props.SetPropertyWarning(notesPerPatternPropIdx, CommentType.Warning, $"Pattern is longer than what FamiTracker supports. Ignore this if you are not planning to export to FamiTracker."); } else { props.SetPropertyWarning(notesPerPatternPropIdx, CommentType.Good, ""); } }