diff --git a/PitchTracker/Helper.h b/PitchTracker/Helper.h index ae13e309..1c27221c 100644 --- a/PitchTracker/Helper.h +++ b/PitchTracker/Helper.h @@ -4,7 +4,7 @@ #include float* short2FloatArray(short* in, size_t len){ - const float maxShort = 32768.0; //maximum abs value of a short + const float maxShort = 32767.0; float* result = static_cast(malloc(len * sizeof(float))); float* curOut = result; @@ -17,7 +17,7 @@ float* short2FloatArray(short* in, size_t len){ } double* short2DoubleArray(short* in, size_t len, bool scale = true){ - const double maxShort = 32768.0; //maximum abs value of a short + const double maxShort = 32767.0; double* result = static_cast(malloc(len * sizeof(double))); double* curOut = result; diff --git a/PitchTracker/PitchWrapper.cpp b/PitchTracker/PitchWrapper.cpp index 3d725b15..d0f6d1f4 100644 --- a/PitchTracker/PitchWrapper.cpp +++ b/PitchTracker/PitchWrapper.cpp @@ -75,7 +75,11 @@ DllExport void PtAKF_InputByte(PtAKF* analyzer, char* data, int sampleCt){ if(sampleCt <= 0 || !analyzer) return; short* dataShort = static_cast(static_cast(data)); - analyzer->input(dataShort, dataShort + sampleCt); + float* dataFloat = short2FloatArray(static_cast(static_cast(data)), sampleCt); + analyzer->input(dataFloat, dataFloat + sampleCt); + freeFloatArray(dataFloat); + + //analyzer->input(dataShort, dataShort + sampleCt); } DllExport int PtAKF_GetNote(PtAKF* analyzer, double* maxVolume, float* weights){ diff --git a/PitchTracker/TestSounds.rar b/PitchTracker/TestSounds.rar new file mode 100644 index 00000000..463321f4 Binary files /dev/null and b/PitchTracker/TestSounds.rar differ diff --git a/PitchTracker/ptAKF.cpp b/PitchTracker/ptAKF.cpp index b5e8bccb..d8a73088 100644 --- a/PitchTracker/ptAKF.cpp +++ b/PitchTracker/ptAKF.cpp @@ -24,9 +24,9 @@ float* PtAKF::_SamplesPerPeriodPerTone = NULL; PtAKF::PtAKF(unsigned step){ _InitCount++; if(_InitCount == 1){ - _SamplesPerPeriodPerTone = new float[_MaxHalfTone + 1]; + _SamplesPerPeriodPerTone = new float[_MaxHalfTone + 1 + _HalfTonesAdd]; //Init Array to avoid costly calculations - for (int toneIndex = 0; toneIndex <= _MaxHalfTone; toneIndex++) + for (int toneIndex = 0; toneIndex <= _MaxHalfTone + _HalfTonesAdd; toneIndex++) { double freq = BaseToneFrequency * pow(HalftoneBase, toneIndex); _SamplesPerPeriodPerTone[toneIndex] = static_cast(44100.0 / freq); // samples in one period @@ -61,12 +61,23 @@ int PtAKF::_GetNote(float* samples, double* maxVolume, float* weights){ if(vol > maxVolumeL) maxVolumeL = vol; } - maxVolumeL /= 32767.; + //maxVolumeL /= 32767.; *maxVolume = maxVolumeL; + //Scale the samples to have a peak of 1 to better decide between voiced/unvoiced + //float scaleVal = 1 / maxVolumeL; + //for(int i = 0; i < _SampleCt; i++){ + // samples[i] *= scaleVal; + //} + float maxWeight = -1; float minWeight = 1; int maxTone = -1; + + //Attention: We have a peak at lag 0 that might stretch that far, that we detect a wrong "peak" at _MaxHalfTone + //Because of that we filter out all tones that are past the last zero crossing from below but keep tones with decreasing weights (going towards zero crossing from above) + int lastValidTone = 0; + float lastWeight = 1.f; for (int toneIndex = _MinHalfTone; toneIndex <= _MaxHalfTone; toneIndex++) { float curWeight = _AnalyzeToneFunc(samples, toneIndex); @@ -81,18 +92,61 @@ int PtAKF::_GetNote(float* samples, double* maxVolume, float* weights){ minWeight = curWeight; weights[toneIndex - _MinHalfTone] = curWeight; + + //Filter: + if(lastWeight > curWeight || (lastWeight > 0.f && curWeight <= 0.f)) + lastValidTone = toneIndex - 1; + lastWeight = curWeight; } - if(maxWeight - minWeight > 0.01){ + if(maxTone==_MaxHalfTone){ + //We might have caught the lag 0 peak so go a bit further to check for other zero crossings (or we won't be able to detect _MaxHalfTone) + float lastWeight = _FastAKF(samples, _SamplesPerPeriodPerTone[_MaxHalfTone]); + for(int toneIndex = _MaxHalfTone+1; toneIndex <= _MaxHalfTone + _HalfTonesAdd; toneIndex++){ + float curWeight = _FastAKF(samples, _SamplesPerPeriodPerTone[toneIndex]); + if(lastWeight > curWeight || (lastWeight > 0.f && curWeight <= 0.f)){ + lastValidTone = toneIndex - 1; + break; + } + lastWeight = curWeight; + } + if(lastValidTone < maxTone){ + maxWeight = -1.f; + minWeight = 1.f; + for(int toneIndex = _MinHalfTone; toneIndex <= lastValidTone; toneIndex++){ + float curWeight = weights[toneIndex-_MinHalfTone]; + if (curWeight > maxWeight) + { + maxWeight = curWeight; + maxTone = toneIndex; + } + + if (curWeight < minWeight) + minWeight = curWeight; + } + //Set all invalid weights to 0 + for(int toneIndex = lastValidTone + 1;toneIndex < _MaxHalfTone; toneIndex++){ + weights[toneIndex - _MinHalfTone]=0.f; + } + } + } + + float energy = _FastAKF(samples, 0); + float maxAKF = _FastAKF(samples, _SamplesPerPeriodPerTone[maxTone]); + //printf("%g %g\n",energy, maxAKF); + + //if(maxWeight - minWeight > 0.025){ + if(maxAKF >= 0.3 * energy){ return maxTone; }else return -1; } float PtAKF::_AnalyzeToneFunc(float* samples, int toneIndex){ - // Use method by Kobayashi and Shimamura (1995): Combine AKF and AMDF to a new f(z)=AKF(z)/(AMDF(z)+k) with k=1 + // Use method by Kobayashi and Shimamura (2001): Combine AKF and AMDF to a new f(z)=AKF(z)/(AMDF(z)+k) with k=1 float samplesPerPeriodD = _SamplesPerPeriodPerTone[toneIndex]; // samples in one period int samplesPerPeriod = static_cast(samplesPerPeriodD); + int maxSampleCt = static_cast(_SamplesPerPeriodPerTone[0]); float fHigh = samplesPerPeriodD - samplesPerPeriod; float fLow = 1.0f - fHigh; @@ -114,7 +168,34 @@ float PtAKF::_AnalyzeToneFunc(float* samples, int toneIndex){ accumDistAKF /= _SampleCt; accumDistAMDF /= sampleIndex; - return accumDistAKF / (accumDistAMDF * 32767.f + 32767.f); // Need to scale AKF by MAX^2 and AMDF by MAX, so do some maths to divide only once + //return accumDistAKF / (accumDistAMDF * 32767.f + 32767.f * 32767.f); // Need to scale AKF by MAX^2 and AMDF by MAX, so do some maths to divide only once + float result = accumDistAKF / (accumDistAMDF + 1.f); + return result; // Need to scale AKF by MAX^2 and AMDF by MAX, so do some maths to divide only once + //{toneIndex}: {accumDistAKF} ; {accumDistAMDF}; {result} +} + +float PtAKF::_FastAKF(float* samples, float samplesPerPeriodD){ + // Do a fast, non-scaled AKF (used for Lag 0 peak removal) + + int samplesPerPeriod = static_cast(samplesPerPeriodD); + int maxSampleCt = static_cast(_SamplesPerPeriodPerTone[0]); + float fHigh = samplesPerPeriodD - samplesPerPeriod; + float fLow = 1.0f - fHigh; + + float accumDistAKF = 0; // accumulated distances + + // compare correlating samples + int sampleIndex = 0; // index of sample to analyze + // Start value= index of sample one period ahead + for (int correlatingSampleIndex = sampleIndex + samplesPerPeriod; correlatingSampleIndex + 1 < _SampleCt; correlatingSampleIndex++, sampleIndex++) + { + // calc distance to corresponding sample in next period + float xn = samples[sampleIndex]; + float xnt = samples[correlatingSampleIndex] * fLow + samples[correlatingSampleIndex + 1] * fHigh; + accumDistAKF += xn * xnt; + } + + return accumDistAKF; } /* diff --git a/PitchTracker/ptAKF.h b/PitchTracker/ptAKF.h index 8ddc7c97..6d5fd609 100644 --- a/PitchTracker/ptAKF.h +++ b/PitchTracker/ptAKF.h @@ -7,7 +7,8 @@ public: PtAKF(unsigned step); ~PtAKF(); - void input(short* begin, short* end){ + /** Add input data to buffer. This is thread-safe (against other functions). **/ + template void input(InIt begin, InIt end) { _AnalysisBuf.insert(begin, end); } @@ -17,12 +18,14 @@ private: static int _InitCount; static float* _SamplesPerPeriodPerTone; static constexpr int _MinHalfTone = 0; //C2 - static constexpr int _MaxHalfTone = 47; //B5 + static constexpr int _MaxHalfTone = 55;//47; //B5 + static constexpr int _HalfTonesAdd = 4; //Additonal half tones to analyze to remove the peak at lag 0 - constexpr static size_t _SampleCt = 4096; + constexpr static size_t _SampleCt = 2048; RingBuffer<_SampleCt * 2> _AnalysisBuf; unsigned _Step; static int _GetNote(float* samples, double* maxVolume, float* weights); static float _AnalyzeToneFunc(float* samples, int toneIndex); + static float _FastAKF(float* samples, float samplesPerPeriodD); }; \ No newline at end of file diff --git a/Vocaluxe/Base/CLog.cs b/Vocaluxe/Base/CLog.cs index a0484670..f90b7043 100644 --- a/Vocaluxe/Base/CLog.cs +++ b/Vocaluxe/Base/CLog.cs @@ -164,6 +164,9 @@ namespace Vocaluxe.Base public static void LogDebug(string text) { _DebugLog.Add(String.Format("{0:HH:mm:ss.ffff}", DateTime.Now) + ":" + text); +#if DEBUG + Console.WriteLine(text); +#endif } public static void LogSongInfo(string text) diff --git a/Vocaluxe/Lib/Sound/Record/CBuffer.cs b/Vocaluxe/Lib/Sound/Record/CBuffer.cs index f2114109..394af07b 100644 --- a/Vocaluxe/Lib/Sound/Record/CBuffer.cs +++ b/Vocaluxe/Lib/Sound/Record/CBuffer.cs @@ -19,9 +19,12 @@ using System; #if TEST_PITCH +using System.Collections.Generic; +using System.IO; using System.Windows.Forms; using System.Diagnostics; using VocaluxeLib; +using VocaluxeLib.Utils; #endif @@ -30,11 +33,12 @@ namespace Vocaluxe.Lib.Sound.Record class CBuffer : IDisposable { #if TEST_PITCH - //Half tones: C C♯ D D# E F F♯ G G♯ A A# B + //Half tones: C C# D D# E F F# G G# A A# B private const double _BaseToneFreq = 65.4064; // lowest (half-)tone to analyze (C2 = 65.4064 Hz) private const double _HalftoneBase = 1.05946309436; // 2^(1/12) -> HalftoneBase^12 = 2 (one octave) #endif private CPtAKF _Analyzer = new CPtAKF(); + private CAnalyzer _Analyzer2 = new CAnalyzer(); private double _MaxVolume; @@ -94,11 +98,12 @@ namespace Vocaluxe.Lib.Sound.Record //if (assigned(fVoiceStream)) then //fVoiceStream.WriteData(Buffer, BufferSize); _Analyzer.Input(buffer); + _Analyzer2.Input(buffer); } public void AnalyzeBuffer() { - //_Analyzer.Process(); + _Analyzer2.Process(); //_MaxVolume = _Analyzer.GetPeak() / 43 + 1; int tone = _Analyzer.GetNote(out _MaxVolume, ToneWeigths); //(int)Math.Round(_Analyzer.FindNote()); // if (tone >= 0 && _MaxVolume >= MinVolume) @@ -132,7 +137,7 @@ namespace Vocaluxe.Lib.Sound.Record private static string _ToneToNote(int tone, bool withOctave = true) { tone += 24; - string[] notes = {"C", "C♯", "D", "D#", "E", "F", "F♯", "G", "G♯", "A", "A#", "B"}; + string[] notes = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}; string result = notes[tone % 12]; if (withOctave) result += (tone / 12); @@ -145,11 +150,12 @@ namespace Vocaluxe.Lib.Sound.Record return; _PitchTestRun = true; int toneFrom = 0; - const int toneTo = 47; //B5 + const int toneTo = -1; // 47; //B5 Console.WriteLine("Testing notes " + _ToneToNote(toneFrom) + " - " + _ToneToNote(toneTo)); byte[] data; byte[] data2 = new byte[2048]; int ok = 0; + int tests = 0; double angle = 0; for (int distort = 0; distort < 10; distort++) { @@ -167,11 +173,30 @@ namespace Vocaluxe.Lib.Sound.Record if (!ToneValid) CBase.Log.LogDebug("Note " + _ToneToNote(tone) + " not detected (Distortion: " + distort + ")"); else if (Tone != tone % 12) + { CBase.Log.LogDebug("Note " + _ToneToNote(tone) + " wrongly detected as " + _ToneToNote(Tone, false) + " (Distortion: " + distort + ")"); + CWavFile file = new CWavFile(); + file.Create(tone + "-" + distort + ".wav", 1, 44100, 16); + file.Write16BitSamples(data, 1); + file.Close(); + } else ok++; + tests++; } } + if (_TestFile("toneG3.wav", 19)) + ok++; + tests++; + if (_TestFile("toneG3Miss.wav", 19)) + ok++; + tests++; + if (_TestFile("toneG4.wav", 31)) + ok++; + tests++; + _TestFile("whistling3.wav", "whistling3.txt", ref tests, ref ok); + _TestFile("sClausVoc.wav", "sClausVoc.txt", ref tests, ref ok); + _GetSineWave(_BaseToneFreq * Math.Pow(_HalftoneBase, 5), 44100, ref angle, out data); Stopwatch sw = new Stopwatch(); sw.Start(); @@ -182,13 +207,108 @@ namespace Vocaluxe.Lib.Sound.Record AnalyzeBuffer(); } sw.Stop(); - string msg = "Analyser: Errors: " + ((toneTo - toneFrom + 1) * 10 - ok) + "; Speed: " + + string msg = "Analyser: Errors: " + (tests - ok) + "/" + tests + "; Speed: " + (int)(repeats / (sw.ElapsedMilliseconds / 1000.0)) + " buffers/s"; Console.WriteLine(msg); MessageBox.Show(msg); Reset(); } + private struct STimedNote + { + public int Time, Note; + } + + private void _TestFile(string fileName, string testFileName, ref int ct, ref int ok) + { + if (!File.Exists(testFileName)) + { + ok = ct = 0; + return; + } + List tones = new List(); + using (StreamReader reader = new StreamReader(testFileName)) + { + String line; + while ((line = reader.ReadLine()) != null) + { + int p = line.IndexOf(' '); + if (p < 0) + continue; + STimedNote note; + note.Time = int.Parse(line.Substring(0, p)); + note.Note = int.Parse(line.Substring(p + 1)); + tones.Add(note); + } + } + _TestFile(fileName, tones, ref ct, ref ok); + } + + private bool _TestFile(string fileName, int tone) + { + STimedNote note; + note.Note = tone; + note.Time = 46; + List tones = new List {note}; + int ct = 0; + int ok = 0; + _TestFile(fileName, tones, ref ct, ref ok); + return ok >= ct - 4; + } + + private void _TestFile(string fileName, IList tones, ref int ct, ref int ok) + { + CWavFile wavFile = new CWavFile(); + try + { + if (!wavFile.Open(fileName)) + return; + if (wavFile.BitsPerSample != 16) + { + wavFile.Close(); + return; + } + int samplesRead = 0; + int curTimeIndex = -1; + int curNote = -1; + const int maxSamplesPerBatch = 512; + while (wavFile.NumSamplesLeft / wavFile.NumChannels > maxSamplesPerBatch) + { + byte[] samples = wavFile.GetNextSamples16BitAsBytes(maxSamplesPerBatch, 1); + samplesRead += samples.Length / 2; + int time = samplesRead * 1000 / wavFile.SampleRate; + ProcessNewBuffer(samples); + AnalyzeBuffer(); + while (curTimeIndex + 1 < tones.Count && time >= tones[curTimeIndex + 1].Time) + { + curTimeIndex++; + curNote = tones[curTimeIndex].Note; + } + if (curNote < 0) + continue; + ct++; + if (!ToneValid) + CBase.Log.LogDebug("Note " + _ToneToNote(curNote) + " at " + time + "ms not detected"); + else if (ToneAbs % 12 != curNote % 12) + CBase.Log.LogDebug("Note " + _ToneToNote(curNote) + " at " + time + "ms wrongly detected as " + _ToneToNote(ToneAbs)); + else + ok++; + int note2 = (int)Math.Round(_Analyzer2.FindNote()); + if (note2 < 0) + CBase.Log.LogDebug("Note2 " + _ToneToNote(curNote) + " at " + time + "ms not detected"); + else if (note2 % 12 != curNote % 12) + CBase.Log.LogDebug("Note2 " + _ToneToNote(curNote) + " at " + time + "ms wrongly detected as " + _ToneToNote(note2)); + else + ok++; + } + } + catch (Exception e) + { + Console.WriteLine("Error on file " + fileName + ": " + e); + } + wavFile.Close(); + } + private static short[] _GetDistort(int len, int tone, int type) { short[] sdata = new short[len]; diff --git a/VocaluxeLib/Utils/CWavFile.cs b/VocaluxeLib/Utils/CWavFile.cs new file mode 100644 index 00000000..7dea4478 --- /dev/null +++ b/VocaluxeLib/Utils/CWavFile.cs @@ -0,0 +1,295 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace VocaluxeLib.Utils +{ + //Simple class to read and write WAV files + public class CWavFile + { + public short NumChannels { get; private set; } + public int SampleRate { get; private set; } + public short BitsPerSample { get; private set; } + public string FileName { get; private set; } + //Unless this is true, all fields are to be considered as invalid + public int DataSize { get; private set; } + //Number of samples (=SampesPerChannel*NumChannels) + public int NumSamples + { + get { return (DataSize / (BitsPerSample / 8)); } + } + public int NumSamplesLeft { get; private set; } + public bool IsOpen { get; private set; } + private int _DataPos; + private FileStream _File; + private bool _IsWritable; + + public CWavFile() + { + IsOpen = false; + } + + ~CWavFile() + { + Debug.Assert(!IsOpen); + Close(); + } + + public void Create(string fileName, short numChannels, int sampleRate, short bitsPerSample) + { + if (IsOpen) + Close(); + _File = new FileStream(fileName, FileMode.Create); + FileName = fileName; + NumChannels = numChannels; + SampleRate = sampleRate; + BitsPerSample = bitsPerSample; + DataSize = 0; + NumSamplesLeft = 0; + IsOpen = true; + _IsWritable = true; + _WriteHeader(); + } + + public void Close() + { + if (!IsOpen) + return; + if (_IsWritable) + { + BinaryWriter writer = new BinaryWriter(_File); + writer.Seek(4, SeekOrigin.Begin); + writer.Write((int)_File.Length - 8); + writer.Seek(_DataPos - 4, SeekOrigin.Begin); + writer.Write(DataSize); + } + _File.Dispose(); + _File = null; + IsOpen = false; + } + + public bool Open(string fileName) + { + if (IsOpen) + Close(); + if (!File.Exists(fileName)) + return false; + FileName = fileName; + _File = new FileStream(FileName, FileMode.Open); + try + { + BinaryReader reader = new BinaryReader(_File); + if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "RIFF") + return false; + reader.ReadInt32(); //Chunksize (file) + if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "WAVE") + return false; + long curPos = _File.Position; + if (_SeekTo(reader, "fmt ") < 0) + return false; + short audioFormat = reader.ReadInt16(); + if (audioFormat != 1) //PCM + return false; + NumChannels = reader.ReadInt16(); + SampleRate = reader.ReadInt32(); + int bytesPerSec = reader.ReadInt32(); + short blockAlign = reader.ReadInt16(); + BitsPerSample = reader.ReadInt16(); + if (BitsPerSample != 8 && BitsPerSample != 16) + return false; + if (blockAlign != NumChannels * (BitsPerSample / 8)) + return false; + if (bytesPerSec != blockAlign * SampleRate) + return false; + _File.Position = curPos; + DataSize = _SeekTo(reader, "data"); + if (DataSize <= 0) + return false; + _DataPos = (int)_File.Position; + DataSize = Math.Min(DataSize, (int)_File.Length - _DataPos); + NumSamplesLeft = NumSamples; + } + catch (Exception) + { + _File.Dispose(); + throw; + } + IsOpen = true; + return true; + } + + /// + /// Gets an array of numSamples samples per channel for the selected channel(s) + /// + /// Samples per channel + /// Channel for which to get the samples (0=all) + /// + public byte[] GetNextSamples8Bit(int numSamples, int channel = 0) + { + if (channel < 0 || channel > NumChannels || BitsPerSample != 8) + return null; + int samplesPerBlock = (channel == 0 ? NumChannels : 1); + if (NumSamplesLeft < numSamples * samplesPerBlock) + return null; + byte[] result; + BinaryReader reader = new BinaryReader(_File); + if (channel == 0 || NumChannels == 1) + result = reader.ReadBytes(numSamples * samplesPerBlock); + else + { + result = new byte[numSamples]; + for (int i = 0; i < numSamples; i++) + { + _File.Seek(channel - 1, SeekOrigin.Current); + result[i] = reader.ReadByte(); + _File.Seek(NumChannels - channel, SeekOrigin.Current); + } + } + NumSamplesLeft -= numSamples * samplesPerBlock; + return result; + } + + /// + /// Gets an array of numSamples samples per channel for the selected channel(s) + /// + /// Samples per channel + /// Channel for which to get the samples (0=all) + /// + public short[] GetNextSamples16Bit(int numSamples, int channel = 0) + { + if (channel < 0 || channel > NumChannels || BitsPerSample != 16) + return null; + int samplesPerBlock = (channel == 0 ? NumChannels : 1); + if (NumSamplesLeft < numSamples * samplesPerBlock) + return null; + short[] result; + BinaryReader reader = new BinaryReader(_File); + try + { + if (channel == 0 || NumChannels == 1) + { + result = new short[numSamples * NumChannels]; + for (int i = 0; i < numSamples * NumChannels; i++) + result[i] = reader.ReadInt16(); + } + else + { + result = new short[numSamples]; + for (int i = 0; i < numSamples; i++) + { + _File.Seek((channel - 1) * 2, SeekOrigin.Current); + result[i] = reader.ReadInt16(); + _File.Seek((NumChannels - channel) * 2, SeekOrigin.Current); + } + } + NumSamplesLeft -= numSamples * samplesPerBlock; + } + catch (EndOfStreamException e) + { + Console.WriteLine("Error: " + e); + return null; + } + return result; + } + + public byte[] GetNextSamples16BitAsBytes(int numSamples, int channel = 0) + { + short[] samples = GetNextSamples16Bit(numSamples, channel); + if (samples == null) + return null; + byte[] samplesByte = new byte[samples.Length * 2]; + Buffer.BlockCopy(samples, 0, samplesByte, 0, samplesByte.Length); + return samplesByte; + } + + public bool WriteSamples(byte[] samples, int channel = 0) + { + if (!_IsWritable || channel < 0 || channel > NumChannels || BitsPerSample != 8) + return false; + BinaryWriter writer = new BinaryWriter(_File); + if (channel == 0 || NumChannels == 1) + writer.Write(samples); + else + { + foreach (byte sample in samples) + { + _File.Seek(channel - 1, SeekOrigin.Current); + writer.Write(sample); + _File.Seek(NumChannels - channel, SeekOrigin.Current); + } + } + DataSize += samples.Length; + return true; + } + + public bool Write16BitSamples(short[] samples, int channel = 0) + { + if (!_IsWritable || channel < 0 || channel > NumChannels || BitsPerSample != 16) + return false; + BinaryWriter writer = new BinaryWriter(_File); + if (channel == 0 || NumChannels == 1) + { + foreach (short sample in samples) + writer.Write(sample); + } + else + { + foreach (short sample in samples) + { + _File.Seek((channel - 1) * 2, SeekOrigin.Current); + writer.Write(sample); + _File.Seek((NumChannels - channel) * 2, SeekOrigin.Current); + } + } + DataSize += samples.Length * 2; + return true; + } + + public bool Write16BitSamples(byte[] samples, int channel = 0) + { + if (samples.Length == 0) + return true; + short[] samplesShort = new short[samples.Length / 2]; + Buffer.BlockCopy(samples, 0, samplesShort, 0, samples.Length); + return Write16BitSamples(samplesShort, channel); + } + + private void _WriteHeader() + { + if (!IsOpen || !_IsWritable) + return; + BinaryWriter writer = new BinaryWriter(_File); + writer.Write(Encoding.ASCII.GetBytes("RIFF")); + writer.Write(0); + writer.Write(Encoding.ASCII.GetBytes("WAVE")); + writer.Write(Encoding.ASCII.GetBytes("fmt ")); + writer.Write(16); + writer.Write((short)1); // PCM + writer.Write(NumChannels); // Channels + writer.Write(SampleRate); // Sample rate + writer.Write(SampleRate * NumChannels * (BitsPerSample / 8)); // Average bytes per second + writer.Write((short)((BitsPerSample / 8) * NumChannels)); // block align + writer.Write(BitsPerSample); // bits per sample + writer.Write(Encoding.ASCII.GetBytes("data")); + writer.Write(DataSize); + _DataPos = (int)_File.Position; + writer.Seek(4, SeekOrigin.Begin); + writer.Write((int)writer.BaseStream.Length); + writer.Seek(_DataPos, SeekOrigin.Begin); + } + + private static int _SeekTo(BinaryReader reader, string chunkId) + { + do + { + string curChunkId = Encoding.ASCII.GetString(reader.ReadBytes(4)); + int chunkSize = reader.ReadInt32(); + if (curChunkId == chunkId) + return chunkSize; + reader.BaseStream.Seek(chunkSize, SeekOrigin.Current); + } while (reader.BaseStream.Position < reader.BaseStream.Length); + return -1; + } + } +} \ No newline at end of file diff --git a/VocaluxeLib/VocaluxeLib.csproj b/VocaluxeLib/VocaluxeLib.csproj index 22d6a2b1..5a6f5a21 100644 --- a/VocaluxeLib/VocaluxeLib.csproj +++ b/VocaluxeLib/VocaluxeLib.csproj @@ -188,6 +188,7 @@ +