/// <summary> /// Creates a new <see cref="Waveform"/> containing a specific number of data points by selecting the average value of each sampled group. /// </summary> /// <param name="pointCount">The number of points the resulting <see cref="Waveform"/> should contain.</param> /// <param name="cancellationToken">The token to cancel the task.</param> /// <returns>An async task for the generation of the <see cref="Waveform"/>.</returns> public async Task <Waveform> GenerateResampledAsync(int pointCount, CancellationToken cancellationToken = default) { if (pointCount < 0) { throw new ArgumentOutOfRangeException(nameof(pointCount)); } if (readTask == null) { return(new Waveform()); } await readTask; return(await Task.Run(() => { var generatedPoints = new List <WaveformPoint>(); float pointsPerGeneratedPoint = (float)points.Count / pointCount; for (float i = 0; i < points.Count; i += pointsPerGeneratedPoint) { if (cancellationToken.IsCancellationRequested) { break; } int startIndex = (int)i; int endIndex = (int)Math.Min(points.Count, Math.Ceiling(i + pointsPerGeneratedPoint)); var point = new WaveformPoint(channels); for (int j = startIndex; j < endIndex; j++) { for (int c = 0; c < channels; c++) { point.Amplitude[c] += points[j].Amplitude[c]; } point.LowIntensity += points[j].LowIntensity; point.MidIntensity += points[j].MidIntensity; point.HighIntensity += points[j].HighIntensity; } // Means for (int c = 0; c < channels; c++) { point.Amplitude[c] /= endIndex - startIndex; } point.LowIntensity /= endIndex - startIndex; point.MidIntensity /= endIndex - startIndex; point.HighIntensity /= endIndex - startIndex; generatedPoints.Add(point); } return new Waveform { points = generatedPoints, channels = channels }; }, cancellationToken)); }
/// <summary> /// Constructs a new <see cref="Waveform"/> from provided audio data. /// </summary> /// <param name="data">The sample data stream. If null, an empty waveform is constructed.</param> public Waveform(Stream data = null) { if (data == null) { return; } readTask = Task.Run(() => { var procs = new DataStreamFileProcedures(data); int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Float, procs.BassProcedures, IntPtr.Zero); ChannelInfo info; Bass.ChannelGetInfo(decodeStream, out info); long length = Bass.ChannelGetLength(decodeStream); // Each "point" is generated from a number of samples, each sample contains a number of channels int sampleDataPerPoint = (int)(info.Frequency * resolution * info.Channels); points.Capacity = (int)(length / sampleDataPerPoint); int bytesPerIteration = sampleDataPerPoint * points_per_iteration; var dataBuffer = new float[bytesPerIteration / bytes_per_sample]; while (length > 0) { length = Bass.ChannelGetData(decodeStream, dataBuffer, bytesPerIteration); int samplesRead = (int)(length / bytes_per_sample); // Process a sequence of samples for each point for (int i = 0; i < samplesRead; i += sampleDataPerPoint) { // Process each sample in the sequence var point = new WaveformPoint(info.Channels); for (int j = i; j < i + sampleDataPerPoint; j += info.Channels) { // Process each channel in the sample for (int c = 0; c < info.Channels; c++) { point.Amplitude[c] = Math.Max(point.Amplitude[c], Math.Abs(dataBuffer[j + c])); } } for (int c = 0; c < info.Channels; c++) { point.Amplitude[c] = Math.Min(1, point.Amplitude[c]); } points.Add(point); } } channels = info.Channels; }, cancelSource.Token); }
/// <summary> /// Constructs a new <see cref="Waveform"/> from provided audio data. /// </summary> /// <param name="data">The sample data stream. If null, an empty waveform is constructed.</param> public Waveform(Stream data) { if (data == null) { return; } readTask = Task.Run(() => { // for the time being, this code cannot run if there is no bass device available. if (Bass.CurrentDevice <= 0) { return; } procedures = CreateDataStreamFileProcedures(data); if (!RuntimeInfo.SupportsIL) { pinnedProcedures = GCHandle.Alloc(procedures, GCHandleType.Pinned); } int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Float, procedures.BassProcedures, RuntimeInfo.SupportsIL ? IntPtr.Zero : GCHandle.ToIntPtr(pinnedProcedures)); Bass.ChannelGetInfo(decodeStream, out ChannelInfo info); long length = Bass.ChannelGetLength(decodeStream); // Each "point" is generated from a number of samples, each sample contains a number of channels int samplesPerPoint = (int)(info.Frequency * resolution * info.Channels); int bytesPerPoint = samplesPerPoint * bytes_per_sample; points.Capacity = (int)(length / bytesPerPoint); // Each iteration pulls in several samples int bytesPerIteration = bytesPerPoint * points_per_iteration; var sampleBuffer = new float[bytesPerIteration / bytes_per_sample]; // Read sample data while (length > 0) { length = Bass.ChannelGetData(decodeStream, sampleBuffer, bytesPerIteration); int samplesRead = (int)(length / bytes_per_sample); // Each point is composed of multiple samples for (int i = 0; i < samplesRead; i += samplesPerPoint) { // Channels are interleaved in the sample data (data[0] -> channel0, data[1] -> channel1, data[2] -> channel0, etc) // samplesPerPoint assumes this interleaving behaviour var point = new WaveformPoint(info.Channels); for (int j = i; j < i + samplesPerPoint; j += info.Channels) { // Find the maximum amplitude for each channel in the point for (int c = 0; c < info.Channels; c++) { point.Amplitude[c] = Math.Max(point.Amplitude[c], Math.Abs(sampleBuffer[j + c])); } } // BASS may provide unclipped samples, so clip them ourselves for (int c = 0; c < info.Channels; c++) { point.Amplitude[c] = Math.Min(1, point.Amplitude[c]); } points.Add(point); } } Bass.ChannelSetPosition(decodeStream, 0); length = Bass.ChannelGetLength(decodeStream); // Read FFT data float[] bins = new float[fft_bins]; int currentPoint = 0; long currentByte = 0; while (length > 0) { length = Bass.ChannelGetData(decodeStream, bins, (int)fft_samples); currentByte += length; double lowIntensity = computeIntensity(info, bins, low_min, mid_min); double midIntensity = computeIntensity(info, bins, mid_min, high_min); double highIntensity = computeIntensity(info, bins, high_min, high_max); // In general, the FFT function will read more data than the amount of data we have in one point // so we'll be setting intensities for all points whose data fits into the amount read by the FFT // We know that each data point required sampleDataPerPoint amount of data for (; currentPoint < points.Count && currentPoint * bytesPerPoint < currentByte; currentPoint++) { points[currentPoint].LowIntensity = lowIntensity; points[currentPoint].MidIntensity = midIntensity; points[currentPoint].HighIntensity = highIntensity; } } channels = info.Channels; }, cancelSource.Token); }
/// <summary> /// Creates a new <see cref="Waveform"/> containing a specific number of data points by selecting the average value of each sampled group. /// </summary> /// <param name="pointCount">The number of points the resulting <see cref="Waveform"/> should contain.</param> /// <param name="cancellationToken">The token to cancel the task.</param> /// <returns>An async task for the generation of the <see cref="Waveform"/>.</returns> public async Task <Waveform> GenerateResampledAsync(int pointCount, CancellationToken cancellationToken = default) { if (pointCount < 0) { throw new ArgumentOutOfRangeException(nameof(pointCount)); } if (readTask == null) { return(new Waveform(null)); } await readTask; return(await Task.Run(() => { var generatedPoints = new List <WaveformPoint>(); float pointsPerGeneratedPoint = (float)points.Count / pointCount; // Determines at which width (relative to the resolution) our smoothing filter is truncated. // Should not effect overall appearance much, except when the value is too small. // A gaussian contains almost all its mass within its first 3 standard deviations, // so a factor of 3 is a very good choice here. const int kernel_width_factor = 3; int kernelWidth = (int)(pointsPerGeneratedPoint *kernel_width_factor) + 1; float[] filter = new float[kernelWidth + 1]; for (int i = 0; i < filter.Length; ++i) { filter[i] = (float)Blur.EvalGaussian(i, pointsPerGeneratedPoint); } for (float i = 0; i < points.Count; i += pointsPerGeneratedPoint) { if (cancellationToken.IsCancellationRequested) { break; } int startIndex = (int)i - kernelWidth; int endIndex = (int)i + kernelWidth; var point = new WaveformPoint(channels); float totalWeight = 0; for (int j = startIndex; j < endIndex; j++) { if (j < 0 || j >= points.Count) { continue; } float weight = filter[Math.Abs(j - startIndex - kernelWidth)]; totalWeight += weight; for (int c = 0; c < channels; c++) { point.Amplitude[c] += weight *points[j].Amplitude[c]; } point.LowIntensity += weight *points[j].LowIntensity; point.MidIntensity += weight *points[j].MidIntensity; point.HighIntensity += weight *points[j].HighIntensity; } // Means for (int c = 0; c < channels; c++) { point.Amplitude[c] /= totalWeight; } point.LowIntensity /= totalWeight; point.MidIntensity /= totalWeight; point.HighIntensity /= totalWeight; generatedPoints.Add(point); } return new Waveform(null) { points = generatedPoints, channels = channels }; }, cancellationToken)); }