public static void ExportHitsounds(IEnumerable <HitsoundEvent> hitsounds, string baseBeatmap, string exportFolder, bool useGreenlines = true, bool useStoryboard = false) { EditorReaderStuff.TryGetNewestVersion(baseBeatmap, out var editor); Beatmap beatmap = editor.Beatmap; if (useStoryboard) { beatmap.StoryboardSoundSamples = hitsounds.Select(h => new StoryboardSoundSample(h.Time, 0, h.Filename, h.Volume)) .ToList(); } else { // Make new timing points // Add red lines List <TimingPoint> timingPoints = beatmap.BeatmapTiming.GetAllRedlines(); List <TimingPointsChange> timingPointsChanges = timingPoints.Select(tp => new TimingPointsChange(tp, mpb: true, meter: true, inherited: true, omitFirstBarLine: true)) .ToList(); // Add hitsound stuff // Replace all hitobjects with the hitsounds beatmap.HitObjects.Clear(); foreach (HitsoundEvent h in hitsounds) { if (useGreenlines) { TimingPoint tp = beatmap.BeatmapTiming.GetTimingPointAtTime(h.Time + 5).Copy(); tp.Offset = h.Time; tp.SampleIndex = h.CustomIndex; h.CustomIndex = 0; // Set it to default value because it gets handled by greenlines now tp.Volume = Math.Round(tp.Volume * h.Volume); h.Volume = 0; // Set it to default value because it gets handled by greenlines now timingPointsChanges.Add(new TimingPointsChange(tp, index: true, volume: true)); } beatmap.HitObjects.Add(new HitObject(h.Pos, h.Time, 5, h.GetHitsounds(), h.SampleSet, h.Additions, h.CustomIndex, h.Volume * 100, h.Filename)); } // Replace the old timingpoints beatmap.BeatmapTiming.TimingPoints.Clear(); TimingPointsChange.ApplyChanges(beatmap.BeatmapTiming, timingPointsChanges); } // Change version to hitsounds beatmap.General["StackLeniency"] = new TValue("0.0"); beatmap.General["Mode"] = new TValue("0"); beatmap.Metadata["Version"] = new TValue("Hitsounds"); beatmap.Difficulty["CircleSize"] = new TValue("4"); // Save the file to the export folder editor.SaveFile(Path.Combine(exportFolder, beatmap.GetFileName())); }
public static List <double> TimesFromStack(string path, double x, double y) { List <double> times = new List <double>(); EditorReaderStuff.TryGetNewestVersion(path, out var editor); bool xIgnore = x == -1; bool yIgnore = y == -1; foreach (HitObject ho in editor.Beatmap.HitObjects) { if ((Math.Abs(ho.Pos.X - x) < 3 || xIgnore) && (Math.Abs(ho.Pos.Y - y) < 3 || yIgnore)) { times.Add(ho.Time); } } return(times); }
public static void ExportHitsounds(List<HitsoundEvent> hitsounds, string baseBeatmap, string exportFolder) { EditorReaderStuff.TryGetNewestVersion(baseBeatmap, out var editor); Beatmap beatmap = editor.Beatmap; // Make new timing points List<TimingPointsChange> timingPointsChanges = new List<TimingPointsChange>(); // Add red lines List<TimingPoint> timingPoints = beatmap.BeatmapTiming.GetAllRedlines(); foreach (TimingPoint tp in timingPoints) { timingPointsChanges.Add(new TimingPointsChange(tp, mpb: true, meter: true, inherited: true, omitFirstBarLine: true)); } // Add hitsound stuff foreach (HitsoundEvent h in hitsounds) { TimingPoint tp = beatmap.BeatmapTiming.GetTimingPointAtTime(h.Time + 5).Copy(); tp.Offset = h.Time; tp.SampleIndex = h.CustomIndex; tp.Volume = Math.Round(tp.Volume * h.Volume); timingPointsChanges.Add(new TimingPointsChange(tp, index: true, volume: true)); } // Replace the old timingpoints beatmap.BeatmapTiming.TimingPoints.Clear(); TimingPointsChange.ApplyChanges(beatmap.BeatmapTiming, timingPointsChanges); // Replace all hitobjects with the hitsounds beatmap.HitObjects.Clear(); foreach (HitsoundEvent h in hitsounds) { beatmap.HitObjects.Add(new HitObject(h.Time, h.GetHitsounds(), h.SampleSet, h.Additions)); } // Change version to hitsounds beatmap.General["StackLeniency"] = new TValue("0.0"); beatmap.General["Mode"] = new TValue("0"); beatmap.Metadata["Version"] = new TValue("Hitsounds"); beatmap.Difficulty["CircleSize"] = new TValue("4"); // Save the file to the export folder editor.SaveFile(Path.Combine(exportFolder, beatmap.GetFileName())); }
private void PeriodicBackupTimerOnTick(object sender, EventArgs e) { try { // Get the newest beatmap, save a temp version, get the hash and compare it to the previous hash, backup temp file var path = IOHelper.GetCurrentBeatmap(); if (string.IsNullOrEmpty(path)) { return; } EditorReaderStuff.TryGetNewestVersion(path, out var editor); // Save temp version var tempPath = Path.Combine(MainWindow.AppDataPath, "temp.osu"); if (!File.Exists(tempPath)) { File.Create(tempPath).Dispose(); } File.WriteAllLines(tempPath, editor.Beatmap.GetLines()); // Get MD5 from temp file var currentMapHash = EditorReaderStuff.GetMD5FromPath(tempPath); // Comparing with previously made periodic backup if (currentMapHash == previousPeriodicBackupHash) { return; } // Saving backup of the map IOHelper.SaveMapBackup(tempPath, true, Path.GetFileName(path)); previousPeriodicBackupHash = currentMapHash; } catch (Exception ex) { Console.WriteLine(ex.Message); } }
/// <summary> /// Extract every used sample in a beatmap and return them as hitsound layers. /// </summary> /// <param name="path">The path to the beatmap.</param> /// <param name="volumes">Taking the volumes from the map and making different layers for different volumes.</param> /// <returns>The hitsound layers</returns> public static List <HitsoundLayer> ImportHitsounds(string path, bool volumes = false) { EditorReaderStuff.TryGetNewestVersion(path, out var editor); Beatmap beatmap = editor.Beatmap; Timeline timeline = beatmap.GetTimeline(); GameMode mode = (GameMode)beatmap.General["Mode"].Value; string mapDir = editor.GetBeatmapFolder(); Dictionary <string, string> firstSamples = AnalyzeSamples(mapDir); List <HitsoundLayer> hitsoundLayers = new List <HitsoundLayer>(); foreach (TimelineObject tlo in timeline.TimelineObjects) { if (!tlo.HasHitsound) { continue; } double volume = volumes ? tlo.FenoSampleVolume / 100 : 1; List <string> samples = tlo.GetPlayingFilenames(mode); foreach (string filename in samples) { bool isFilename = tlo.UsesFilename; SampleSet sampleSet = isFilename ? tlo.FenoSampleSet : GetSamplesetFromFilename(filename); Hitsound hitsound = isFilename ? tlo.GetHitsound() : GetHitsoundFromFilename(filename); string samplePath = Path.Combine(mapDir, filename); string fullPathExtLess = Path.Combine( Path.GetDirectoryName(samplePath) ?? throw new InvalidOperationException(), Path.GetFileNameWithoutExtension(samplePath)); // Get the first occurence of this sound to not get duplicated if (firstSamples.Keys.Contains(fullPathExtLess)) { samplePath = firstSamples[fullPathExtLess]; } else { // Sample doesn't exist if (!isFilename) { samplePath = Path.Combine( Path.GetDirectoryName(samplePath) ?? throw new InvalidOperationException(), $"{sampleSet.ToString().ToLower()}-hit{hitsound.ToString().ToLower()}-1.wav"); } } string extLessFilename = Path.GetFileNameWithoutExtension(samplePath); var importArgs = new LayerImportArgs(ImportType.Hitsounds) { Path = path, SamplePath = samplePath, Volume = volume }; // Find the hitsoundlayer with this path HitsoundLayer layer = hitsoundLayers.Find(o => o.ImportArgs == importArgs); if (layer != null) { // Find hitsound layer with this path and add this time layer.Times.Add(tlo.Time); } else { // Add new hitsound layer with this path HitsoundLayer newLayer = new HitsoundLayer(extLessFilename, sampleSet, hitsound, new SampleGeneratingArgs(samplePath) { Volume = volume }, importArgs); newLayer.Times.Add(tlo.Time); hitsoundLayers.Add(newLayer); } } } // Sort layers by name hitsoundLayers = hitsoundLayers.OrderBy(o => o.Name).ToList(); return(hitsoundLayers); }
private string Sliderate(SlideratorVm arg, BackgroundWorker worker) { // Make a position function for Sliderator Sliderator.PositionFunctionDelegate positionFunction; // Test if the function is a constant velocity bool constantVelocity; // We convert the graph GetValue function to a function that works like ms -> px // d is a value representing the number of milliseconds into the slider if (arg.GraphMode == GraphMode.Velocity) { // Here we use SvGraphMultiplier to get an accurate conversion from SV to slider completion per beat // Completion = (100 * SliderMultiplier / PixelLength) * SV * Beats positionFunction = d => arg.GraphState.GetIntegral(0, d * arg.BeatsPerMinute / 60000) * arg.SvGraphMultiplier * arg.PixelLength; constantVelocity = Precision.AlmostEquals(AnchorCollection.GetMaxValue(arg.GraphState.Anchors), AnchorCollection.GetMinValue(arg.GraphState.Anchors)); } else { positionFunction = d => arg.GraphState.GetValue(d * arg.BeatsPerMinute / 60000) * arg.PixelLength; constantVelocity = Precision.AlmostEquals(AnchorCollection.GetMaxDerivative(arg.GraphState.Anchors), AnchorCollection.GetMinDerivative(arg.GraphState.Anchors)); } // Dont do Sliderator if the velocity is constant AND equal to the new velocity var simplifyShape = constantVelocity && Precision.AlmostEquals( arg.PixelLength / arg.GraphBeats / arg.GlobalSv / 100, arg.NewVelocity); // Get the highest velocity occuring in the graph double velocity = arg.NewVelocity; // Velocity is in SV // Do bad stuff to the velocity to make sure its the same SV as after writing it to .osu code velocity = -100 / double.Parse((-100 / velocity).ToInvariant(), CultureInfo.InvariantCulture); // Other velocity is in px / ms var otherVelocity = velocity * arg.SvGraphMultiplier * arg.PixelLength * arg.BeatsPerMinute / 60000; // Time between timeline ticks for stream export var deltaT = 60000 / arg.BeatsPerMinute / arg.BeatSnapDivisor; // Update progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(10); } List <Vector2> slideration = new List <Vector2>(); var sliderator = new Sliderator { PositionFunction = positionFunction, MaxT = arg.GraphBeats / arg.BeatsPerMinute * 60000, Velocity = otherVelocity, MinDendriteLength = arg.MinDendrite }; if (!simplifyShape) { // Get slider path like from the hit object preview var sliderPath = new SliderPath(arg.VisibleHitObject.SliderType, arg.VisibleHitObject.GetAllCurvePoints().ToArray(), GetMaxCompletion(arg, arg.GraphState.Anchors) * arg.PixelLength); var path = new List <Vector2>(); sliderPath.GetPathToProgress(path, 0, 1); // Update progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(20); } // Do Sliderator sliderator.SetPath(path); slideration = arg.ExportAsStream ? sliderator.SliderateStream(deltaT) : sliderator.Sliderate(); // Check for some illegal output if (double.IsInfinity(sliderator.MaxS) || double.IsNaN(sliderator.MaxS) || slideration.Any(v => double.IsNaN(v.X) || double.IsNaN(v.Y))) { return("Encountered unexpected values from Sliderator. Please check your input."); } } // Update progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(60); } // Exporting stuff BeatmapEditor editor; bool editorRead = false; if (arg.DoEditorRead) { editorRead = EditorReaderStuff.TryGetNewestVersion(arg.Path, out editor); arg.DoEditorRead = false; } else { editor = new BeatmapEditor(arg.Path); } var beatmap = editor.Beatmap; var timing = beatmap.BeatmapTiming; // Get hit object that might be present at the export time or make a new one var hitObjectHere = beatmap.HitObjects.FirstOrDefault(o => Math.Abs(arg.ExportTime - o.Time) < 5) ?? new HitObject(arg.ExportTime, 0, SampleSet.Auto, SampleSet.Auto); // Clone the hit object to not affect the already existing hit object instance with changes var clone = new HitObject(hitObjectHere.GetLine()) { IsCircle = arg.ExportAsStream, IsSpinner = false, IsHoldNote = false, IsSlider = !arg.ExportAsStream }; // Update progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(70); } if (!arg.ExportAsStream) { // Give the new hit object the sliderated anchors if (simplifyShape) { // The velocity is constant, so you can simplify to the original slider shape clone.SetAllCurvePoints(arg.VisibleHitObject.GetAllCurvePoints()); clone.SliderType = arg.VisibleHitObject.SliderType; } else { clone.SetAllCurvePoints(slideration); clone.SliderType = PathType.Bezier; } clone.PixelLength = sliderator.MaxS; clone.SliderVelocity = -100 / velocity; // Add hit object if (arg.ExportMode == ExportMode.Add) { beatmap.HitObjects.Add(clone); } else { beatmap.HitObjects.Remove(hitObjectHere); beatmap.HitObjects.Add(clone); } // Add SV var timingPointsChanges = new List <TimingPointsChange>(); if (arg.DelegateToBpm) { var tpAfter = timing.GetRedlineAtTime(clone.Time).Copy(); var tpOn = tpAfter.Copy(); tpAfter.Offset = clone.Time; tpOn.Offset = clone.Time - 1; // This one will be on the slider tpAfter.OmitFirstBarLine = true; tpOn.OmitFirstBarLine = true; // Express velocity in BPM tpOn.MpB /= -100 / clone.SliderVelocity; // NaN SV results in removal of slider ticks clone.SliderVelocity = arg.RemoveSliderTicks ? double.NaN : -100; // Add redlines timingPointsChanges.Add(new TimingPointsChange(tpOn, mpb: true, inherited: true, omitFirstBarLine: true, fuzzyness: 0)); timingPointsChanges.Add(new TimingPointsChange(tpAfter, mpb: true, inherited: true, omitFirstBarLine: true, fuzzyness: 0)); clone.Time -= 1; } // Add SV for every hit object so the SV doesnt change for anything else than the sliderated slider timingPointsChanges.AddRange(beatmap.HitObjects.Select(ho => { var sv = ho == clone ? ho.SliderVelocity : timing.GetSvAtTime(ho.Time); var tp = timing.GetTimingPointAtTime(ho.Time).Copy(); tp.MpB = sv; tp.Offset = ho.Time; return(new TimingPointsChange(tp, mpb: true, fuzzyness: 0)); })); TimingPointsChange.ApplyChanges(timing, timingPointsChanges); } else { // Add hit objects if (arg.ExportMode == ExportMode.Override) { beatmap.HitObjects.Remove(hitObjectHere); } double t = arg.ExportTime; foreach (var pos in slideration) { clone.Pos = pos; clone.Time = t; beatmap.HitObjects.Add(clone); clone = new HitObject(clone.GetLine()) { IsCircle = true, IsSpinner = false, IsHoldNote = false, IsSlider = false, NewCombo = false }; t += deltaT; } } // Update progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(80); } beatmap.SortHitObjects(); editor.SaveFile(); // Complete progressbar if (worker != null && worker.WorkerReportsProgress) { worker.ReportProgress(100); } // Do stuff if (arg.Quick) { RunFinished?.Invoke(this, new RunToolCompletedEventArgs(true, editorRead)); } return(arg.Quick ? string.Empty : "Done!"); }