void GenerateClickTrackSamples(CopyContext ctx, QNT_Timestamp currentTick, TempoChange currentTempo, QNT_Duration timeSignatureDuration) { QNT_Timestamp tempoStart = currentTempo.time; QNT_Timestamp nextBeat = timeline.GetClosestBeatSnapped(currentTick, currentTempo.timeSignature.Denominator); UInt64 currentBeatInBar = ((currentTick.tick - tempoStart.tick) / timeSignatureDuration.tick) % currentTempo.timeSignature.Numerator; if (currentBeatInBar != 1) { TryAddClickEvent(currentTick, nextBeat, hihat_hit); } else { TryAddClickEvent(currentTick, nextBeat, hihat_hit3); } TryAddClickEvent(currentTick, nextBeat + Constants.SixteenthNoteDuration, hihat_hit2); TryAddClickEvent(currentTick, nextBeat + Constants.EighthNoteDuration, hihat_open); for (int i = clickTrackEvents.Count - 1; i >= 0; i--) { ClickTrackEvent ev = clickTrackEvents[i]; ev.clip.currentSample = ev.currentSample; ev.clip.CopySampleIntoBuffer(ctx); ev.currentSample = ev.clip.currentSample; if (ev.clip.scaledCurrentSample > ev.clip.samples.Length) { clickTrackEvents.RemoveAt(i); } else { clickTrackEvents[i] = ev; } } }
/// <summary> /// Adds TempoChange Events to the given Chart from the given Dictionary of /// position to tempo values parsed from the Chart or Song. /// </summary> /// <param name="tempos"> /// Dictionary of time to value of tempos parsed from the Song or Chart. /// </param> /// <param name="chart">Chart to add TempoChange Events to.</param> public static void AddTempos(Dictionary <double, double> tempos, Chart chart) { // Insert tempo change events. foreach (var tempo in tempos) { var tempoChangeEvent = new TempoChange(tempo.Value) { Position = new MetricPosition( (int)tempo.Key / NumBeatsPerMeasure, (int)tempo.Key % NumBeatsPerMeasure, FindClosestSMSubDivision(tempo.Key - (int)tempo.Key)) }; // Record the actual doubles. tempoChangeEvent.Extras.AddSourceExtra(TagFumenDoublePosition, tempo.Key); chart.Layers[0].Events.Add(tempoChangeEvent); } }
public void Activate() { if (!timeline.paused) { timeline.TogglePlayback(); } gameObject.GetComponent <CanvasGroup>().DOFade(1.0f, 0.3f); gameObject.SetActive(true); TempoChange tempo = timeline.GetTempoForTime(Timeline.time); dynamicBpmInput.GetComponent <TMP_InputField>().text = Constants.DisplayBPMFromMicrosecondsPerQuaterNote(tempo.microsecondsPerQuarterNote); timeSignatureNumerator.GetComponent <TMP_InputField>().text = tempo.timeSignature.Numerator.ToString(); timeSignatureDenomerator.GetComponent <TMP_InputField>().text = tempo.timeSignature.Denominator.ToString(); dynamicBpmInput.ActivateInputField(); }
void ClickTrackPCMReaderCallback(float[] data) { CopyContext ctx; ctx.bufferData = data; ctx.bufferChannels = 2; ctx.bufferFreq = sampleRate; ctx.playbackSpeed = 1.0f; ctx.outputValue = 0.0f; ctx.volume = volume; for (int dataIndex = 0; dataIndex < data.Length / 2; dataIndex++) { ctx.index = dataIndex; QNT_Timestamp currentTick = timeline.ShiftTick(new QNT_Timestamp(0), (float)((dataIndex) / (float)sampleRate)); TempoChange currentTempo = timeline.GetTempoForTime(currentTick); QNT_Duration timeSignatureDuration = new QNT_Duration(Constants.PulsesPerWholeNote / currentTempo.timeSignature.Denominator); GenerateClickTrackSamples(ctx, currentTick, currentTempo, timeSignatureDuration); } }
void OnAudioFilterRead(float[] bufferData, int bufferChannels) { CopyContext ctx; ctx.bufferData = bufferData; ctx.bufferChannels = bufferChannels; ctx.bufferFreq = sampleRate; ctx.playbackSpeed = speed; ctx.outputValue = 0.0f; if (clearHitsounds) { hitsoundEvents.Clear(); hitSoundEnd = new QNT_Timestamp(0); clearHitsounds = false; } if (clearClicksounds) { clickTrackEvents.Clear(); clearClicksounds = false; } if (newPreviewHitsoundEvents != null) { List <HitsoundEvent> newPreview = newPreviewHitsoundEvents; newPreviewHitsoundEvents = null; previewHitsoundEvents.Clear(); previewHitsoundEvents = newPreview; } if (playPreview) { int dataIndex = 0; while (dataIndex < bufferData.Length / bufferChannels && (preview.currentSample < currentPreviewSongSampleEnd) && (preview.scaledCurrentSample) < song.samples.Length) { ctx.index = dataIndex; ctx.volume = volume; ctx.playbackSpeed = speed; preview.CopySampleIntoBuffer(ctx); PlayHitsounds(ctx, previewHitsoundEvents); ++dataIndex; } if (preview.currentSample >= currentPreviewSongSampleEnd || (preview.scaledCurrentSample) >= song.samples.Length) { playPreview = false; } } if (paused || (song.scaledCurrentSample) > song.samples.Length) { hitsoundEvents.Clear(); //Continue to flush the hitsounds if (!playPreview && previewHitsoundEvents.Count > 0) { int dataIndex = 0; while (dataIndex < bufferData.Length / bufferChannels) { ctx.index = dataIndex; ctx.volume = hitSoundVolume; PlayHitsounds(ctx, previewHitsoundEvents); ++dataIndex; } } return; } if (song != null) { QNT_Timestamp timeStart = timeline.ShiftTick(new QNT_Timestamp(0), (float)song.currentTime); QNT_Timestamp timeEnd = timeline.ShiftTick(new QNT_Timestamp(0), (float)(song.currentTime + (bufferData.Length / bufferChannels) / (float)song.frequency * (song.frequency / (float)sampleRate))); if (timeEnd > hitSoundEnd) { hitSoundEnd = timeStart + Constants.QuarterNoteDuration; AddHitsoundEvents(hitsoundEvents, timeStart, hitSoundEnd); } } for (int dataIndex = 0; dataIndex < bufferData.Length / bufferChannels; dataIndex++) { ctx.volume = volume; ctx.index = dataIndex; ctx.playbackSpeed = speed; song.CopySampleIntoBuffer(ctx); if (songExtra != null) { songExtra.CopySampleIntoBuffer(ctx); } //Play hitsounds (reverse iterate so we can remove) ctx.volume = hitSoundVolume; PlayHitsounds(ctx, hitsoundEvents); ctx.playbackSpeed = speed; if (leftSustain != null) { ctx.volume = leftSustainVolume; leftSustain.CopySampleIntoBuffer(ctx); } if (rightSustain != null) { ctx.volume = rightSustainVolume; rightSustain.CopySampleIntoBuffer(ctx); } QNT_Timestamp currentTick = timeline.ShiftTick(new QNT_Timestamp(0), (float)GetTimeFromCurrentSample()); TempoChange currentTempo = timeline.GetTempoForTime(currentTick); QNT_Duration timeSignatureDuration = new QNT_Duration(Constants.PulsesPerWholeNote / currentTempo.timeSignature.Denominator); if (playMetronome) { if (GetTimeFromCurrentSample() > nextMetronomeTick) { metronomeSamplesLeft = (int)(sampleRate * metronomeTickLength); nextMetronomeTick = 0; } if (nextMetronomeTick == 0) { QNT_Timestamp nextBeat = timeline.GetClosestBeatSnapped(timeline.ShiftTick(new QNT_Timestamp(0), (float)GetTimeFromCurrentSample()) + timeSignatureDuration, currentTempo.timeSignature.Denominator); nextMetronomeTick = timeline.TimestampToSeconds(nextBeat); } } //Metronome if (metronomeSamplesLeft > 0) { const uint MetronomeTickFreq = 817; const uint BigMetronomeTickFreq = 1024; QNT_Timestamp tempoStart = currentTempo.time; uint tickFreq = 0; UInt64 currentBeatInBar = ((currentTick.tick - tempoStart.tick) / timeSignatureDuration.tick) % currentTempo.timeSignature.Numerator; if (currentBeatInBar == 0) { tickFreq = BigMetronomeTickFreq; } else { tickFreq = MetronomeTickFreq; } for (int c = 0; c < bufferChannels; ++c) { int totalMetroSamples = (int)(sampleRate * metronomeTickLength); float metro = Mathf.Sin(Mathf.PI * 2 * tickFreq * ((totalMetroSamples - metronomeSamplesLeft) / (float)totalMetroSamples) * metronomeTickLength) * 0.33f; bufferData[dataIndex * bufferChannels + c] = Mathf.Clamp(bufferData[dataIndex * bufferChannels + c] + metro, -1.0f, 1.0f); } metronomeSamplesLeft -= 1; } if (playClickTrack && currentTick < clickTrackEndTime) { ctx.volume = volume; ctx.index = dataIndex; ctx.playbackSpeed = 1.0f; GenerateClickTrackSamples(ctx, currentTick, currentTempo, timeSignatureDuration); } } }
//======Generate other data from known information=== private void AnalyzeMIDI() { long tempo = 500000; //Default tempo in case none is given //Create lists and arrays //-to put PlayedNotes in per track //-to store notes that have been pressed but not released //-To track what event we're checking, per track List<List<PlayedNote>> lists = new List<List<PlayedNote>>(); List<List<PlayedNote>> unfinishednotes = new List<List<PlayedNote>>(); for (int i = 0; i < tracks.Length; i++) { lists.Add(new List<PlayedNote>()); unfinishednotes.Add(new List<PlayedNote>()); } int[] currentevent = new int[lists.Count]; //Tracks the current event # per list long[] currenttime = new long[lists.Count]; for (int i = 0; i < lists.Count; i++) { currentevent[i] = 0; currenttime[i] = 0; } List<TempoChange> tempoChanges = new List<TempoChange>(); this.tempoChanges = new TempoChange[0]; //Start reading all the lists at the same time, in MIDI timing order bool reading = true; while (reading) { //Check which next MIDI event of all tracks has the lowest delta time long lowesttime = long.MaxValue; int lowestlist = -1; for (int i = 0; i < lists.Count; i++) { if (currentevent[i] < tracks[i].events.Length) //Ignore if at end of track { if (tracks[i].events[currentevent[i]].time + currenttime[i] < lowesttime) { lowestlist = i; lowesttime = tracks[i].events[currentevent[i]].time + currenttime[i]; } } } if (lowestlist == -1) { reading = false; } //All tracks at end of track else //You got the current event, parse it { long newtempo = tempo; Event ev = tracks[lowestlist].events[currentevent[lowestlist]]; if (ev.type == EventType.Midi) { ReanalyzeType: //Jumps here if case NoteOn realizes it's actually a NoteOff switch(ev.midiEvent.midiType) { case MidiType.NoteOn: //Note on event { if (ev.midiEvent.note.velocity == 0) { ev.midiEvent.midiType = MidiType.NoteOff; goto ReanalyzeType; //Return to start of switch to fall to NoteOff instead } PlayedNote pn = new PlayedNote(); pn.note = ev.midiEvent.note; pn.time = currenttime[lowestlist] + TicksToMicroSeconds(ev.time, tempo, currenttime[lowestlist]); unfinishednotes[lowestlist].Add(pn); break; } case MidiType.NoteOff: //Note off event { PlayedNote pn = new PlayedNote(); for (int i = 0; i < unfinishednotes[lowestlist].Count; i++) { if (unfinishednotes[lowestlist][i].note.number == ev.midiEvent.note.number) { pn = unfinishednotes[lowestlist][i]; } } unfinishednotes[lowestlist].Remove(pn); //TODO: Improve, causes 33% of CPU usage of entire LoadMIDI(), has to look for IndexOf() which causes slowdown pn.length = (currenttime[lowestlist] + TicksToMicroSeconds(ev.time, tempo, currenttime[lowestlist])) - pn.time; float division = pn.length / tempo; Msg("Division: " + division); if (division > 7.9 && division < 8.1) pn.lengthtype = LengthType.DoubleWhole; else if (division > 3.9 && division < 4.1) pn.lengthtype = LengthType.Whole; else if (division > 1.9 && division < 2.1) pn.lengthtype = LengthType.Half; else if (division > 0.9 && division < 1.1) pn.lengthtype = LengthType.Quarter; else if (division > 0.45 && division < 0.55) pn.lengthtype = LengthType.Eighth; else if (division > 0.20 && division < 0.30) pn.lengthtype = LengthType.Sixteenth; else if (division > 0.1125 && division < 0.1375) pn.lengthtype = LengthType.ThirtySecond; else if (division > 0.0620 && division < 0.0630) pn.lengthtype = LengthType.SixtyFourth; //Why hello there Beethoven! else if (division > 0.03120 && division < 0.03130) pn.lengthtype = LengthType.HundredTwentyEight; else pn.lengthtype = LengthType.Unknown; lists[lowestlist].Add(pn); break; } } } else if (ev.type == EventType.Meta) { switch (ev.metaType) { case MetaType.Tempo: TempoChange tc = new TempoChange(); tc.time = currenttime[lowestlist] + TicksToMicroSeconds(ev.time, tempo, currenttime[lowestlist]); tc.oldTempo = tempo; newtempo = ConvertThreeByteInt(ev.data); if (currenttime[lowestlist] == 0) this.tempo = newtempo; tc.newTempo = newtempo; tempoChanges.Add(tc); this.tempoChanges = tempoChanges.ToArray(); break; } } currenttime[lowestlist] += TicksToMicroSeconds(ev.time, tempo, currenttime[lowestlist]); tempo = newtempo; currentevent[lowestlist]++; } } for (int i = 0; i < tracks.Length; i++) { tracks[i].notes = lists[i].ToArray(); } this.tempoChanges = tempoChanges.ToArray(); }
//Converts delta-time ticks (used for MIDI Event timing) to microseconds //Microseconds per tick = tempo / ticksPerQuarterNote public long TicksToMicroSeconds(long deltatime, long tempo, long trackLastEventTime) { long returnvalue = 0; long remainingDeltaTime = deltatime; if (tempoChanges.Length > 0) { TempoChange[] haxTempoChanges = new TempoChange[tempoChanges.Length + 1]; for (int i = 0; i < tempoChanges.Length; i++) { haxTempoChanges[i] = tempoChanges[i]; } haxTempoChanges[haxTempoChanges.Length - 1] = new TempoChange() { oldTempo = haxTempoChanges[haxTempoChanges.Length - 2].newTempo, newTempo = haxTempoChanges[haxTempoChanges.Length - 2].newTempo, time = long.MaxValue }; long currentTime = trackLastEventTime; for (int i = 0; i < tempoChanges.Length - 1; i++) { if (tempoChanges[i + 1].time <= currentTime) { //Skip this change; the next one starts before this is relevant } else { long endValue = tempoChanges[i + 1].time; long timeToGrab = remainingDeltaTime * (tempoChanges[i].newTempo / header.ticksPerQuarterNote); long deltaToRemove = remainingDeltaTime; //Take all by default if (currentTime + timeToGrab > endValue) { //Whoa, you got too much, take a bit less float percentageYouShouldGetInstead = (float)((float)endValue - (float)currentTime) / (float)timeToGrab; deltaToRemove = Convert.ToInt64((float)deltaToRemove * (float)percentageYouShouldGetInstead); timeToGrab = deltaToRemove * (tempoChanges[i].newTempo / header.ticksPerQuarterNote); } returnvalue += timeToGrab; remainingDeltaTime -= deltaToRemove; } } } returnvalue += remainingDeltaTime * (tempo / header.ticksPerQuarterNote); return returnvalue; }
public void SetInfoFromData(TempoChange tempoData) { this.tempoData = tempoData; bpm.text = $"{Constants.DisplayBPMFromMicrosecondsPerQuaterNote(tempoData.microsecondsPerQuarterNote)} bpm"; timestamp.text = GetTimestampFromSeconds(tempoData.secondsFromStart); }