Merge lp:~fenugrec/mixxx/wavpack into lp:~mixxxdevelopers/mixxx/trunk
- wavpack
- Merge into trunk
Status: | Merged |
---|---|
Merge reported by: | Albert Santoni |
Merged at revision: | not available |
Proposed branch: | lp:~fenugrec/mixxx/wavpack |
Merge into: | lp:~mixxxdevelopers/mixxx/trunk |
Diff against target: |
941 lines (+454/-220) 8 files modified
mixxx/src/SConscript (+20/-0) mixxx/src/defs_audiofiles.h (+4/-4) mixxx/src/soundsourceoggvorbis.cpp (+39/-57) mixxx/src/soundsourceproxy.cpp (+69/-44) mixxx/src/soundsourcewv.cpp (+194/-0) mixxx/src/soundsourcewv.h (+33/-0) mixxx/src/test/soundFileFormats/README.txt (+6/-3) mixxx/src/test/soundFileFormats/generateFiles.sh (+89/-112) |
To merge this branch: | bzr merge lp:~fenugrec/mixxx/wavpack |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Albert Santoni | Approve | ||
Sean M. Pappalardo | Needs Fixing | ||
Review via email: mp+20183@code.launchpad.net |
Commit message
Added Wavpack support
Description of the change
fenugrec (fenugrec) wrote : | # |
Sean M. Pappalardo (pegasus-renegadetech) wrote : | # |
I know you've been waiting patiently for this to be merged, but can I possibly ask you to re-merge trunk into this branch again and test? Trunk has diverged since the SoundSource plugins merge which does away with the hard-coded SUPPORTED_FILETYPES #define. (It auto-detects at runtime based on what libs are loaded.) Once you get it working with that, getting it approved for merging to trunk will be much quicker.
Albert Santoni (gamegod) wrote : | # |
Last time I talked to fenugrec on IRC, we sort of toyed with the idea
of turning wavpack into another plugin, but we never really decided
what the best course of action was. Merging trunk into the branch and
fixing everything is probably a good start though...
On Thu, Jun 17, 2010 at 1:02 AM, Pegasus <email address hidden> wrote:
> Review: Needs Fixing
> I know you've been waiting patiently for this to be merged, but can I possibly ask you to re-merge trunk into this branch again and test? Trunk has diverged since the SoundSource plugins merge which does away with the hard-coded SUPPORTED_FILETYPES #define. (It auto-detects at runtime based on what libs are loaded.) Once you get it working with that, getting it approved for merging to trunk will be much quicker.
> --
> https:/
> Your team Mixxx Development Team is subscribed to branch lp:mixxx.
>
Albert Santoni (gamegod) wrote : | # |
Hey fenugrec,
If you can merge trunk into your branch (we changed our SCons files around), I'll approve a merge back into trunk and we can finally close this. I looked over your code when I was working on the gme plugin, since I forked your code.
When you merge trunk into your branch, take a look at build/depends.py and build/features.py.
Thanks!
Albert
Albert Santoni (gamegod) wrote : | # |
Wait, I'm totally confused... we already merged this!
Preview Diff
1 | === modified file 'mixxx/src/SConscript' |
2 | --- mixxx/src/SConscript 2010-02-25 10:14:57 +0000 |
3 | +++ mixxx/src/SConscript 2010-02-25 22:20:32 +0000 |
4 | @@ -217,6 +217,7 @@ |
5 | vars.Add('midiscript', 'Set to 1 to enable MIDI Scripting support.', 1) |
6 | vars.Add('tonal', 'Set to 1 to enable tonal analysis', 0) |
7 | vars.Add('m4a','Set to 1 to enable support for M4A audio (Apple non-drm''d music format)', 0) |
8 | +vars.Add('wv','Set to 1 to enable support for WavPack audio (lossless & hybrid lossy, open codec)',0) |
9 | vars.Add('qdebug', 'Set to 1 to enable verbose console debug output.', 1) |
10 | vars.Add('test', 'Set to 1 to build Mixxx test fixtures.', 0) |
11 | if not 'win' in platform: |
12 | @@ -582,6 +583,9 @@ |
13 | print 'Did not find libfaad or the libfaad development headers, exiting!' |
14 | Exit(1) |
15 | |
16 | +#hack^2 |
17 | +have_wv = conf.CheckLib('wavpack') |
18 | + |
19 | ## Check for OpenGL (it's messy to do it for all three platforms) |
20 | ## XXX this should *NOT* have hardcoded paths like this |
21 | if not conf.CheckLib('GL') and not conf.CheckLib('opengl32') and not conf.CheckCHeader('/System/Library/Frameworks/OpenGL.framework/Versions/A/Headers/gl.h') and not conf.CheckCHeader('GL/gl.h'): |
22 | @@ -1063,6 +1067,22 @@ |
23 | print "Apple M4A audio file support... disabled" |
24 | |
25 | |
26 | +flags_wv = getFlags(env,'wv',0) |
27 | +if int(flags_wv): |
28 | + print "WavPack audio file support...", |
29 | + if have_wv: |
30 | + print "enabled" |
31 | + env.Append(CPPDEFINES = '__WV__') |
32 | + build_flags += 'wv ' |
33 | + sources += Split("""soundsourcewv.cpp"""); |
34 | + env.Append(LIBS='libwavpack') |
35 | + else: |
36 | + print "not found" |
37 | +else: |
38 | + print "WavPack audio file support disabled" |
39 | + |
40 | + |
41 | + |
42 | def build_gtest(): |
43 | gtest_dir = env.Dir("#lib/gtest-1.3.0") |
44 | gtest_dir.addRepository(env.Dir('#lib/gtest-1.3.0')) |
45 | |
46 | === modified file 'mixxx/src/defs_audiofiles.h' |
47 | --- mixxx/src/defs_audiofiles.h 2010-02-25 10:14:57 +0000 |
48 | +++ mixxx/src/defs_audiofiles.h 2010-02-25 22:20:33 +0000 |
49 | @@ -9,14 +9,14 @@ |
50 | |
51 | #ifdef __M4A__ |
52 | /** The types of audio files we support */ |
53 | -#define MIXXX_SUPPORTED_AUDIO_FILETYPES "*.wav *.mp3 *.m4a *.mp4 *.ogg *.oga *.aiff *.aif *.flac" |
54 | +#define MIXXX_SUPPORTED_AUDIO_FILETYPES "*.wav *.mp3 *.m4a *.mp4 *.ogg *.oga *.aiff *.aif *.flac *.wv" |
55 | /** A regex for the types of audio files we support */ |
56 | -#define MIXXX_SUPPORTED_AUDIO_FILETYPES_REGEX "\\.(mp3|m4a|mp4|ogg|oga|aiff|aif|wav|flac)$" |
57 | +#define MIXXX_SUPPORTED_AUDIO_FILETYPES_REGEX "\\.(mp3|m4a|mp4|ogg|oga|aiff|aif|wav|flac|wv)$" |
58 | #else //If the __M4A__ symbol isn't defined, then we can't use .m4a as a supported audio filetype :) |
59 | /** The types of audio files we support */ |
60 | -#define MIXXX_SUPPORTED_AUDIO_FILETYPES "*.wav *.mp3 *.ogg *.aiff *.aif *.flac" |
61 | +#define MIXXX_SUPPORTED_AUDIO_FILETYPES "*.wav *.mp3 *.ogg *.aiff *.aif *.flac *.wv" |
62 | /** A regex for the types of audio files we support */ |
63 | -#define MIXXX_SUPPORTED_AUDIO_FILETYPES_REGEX "\\.(mp3|ogg|oga|aiff|aif|wav|flac)$" |
64 | +#define MIXXX_SUPPORTED_AUDIO_FILETYPES_REGEX "\\.(mp3|ogg|oga|aiff|aif|wav|flac|wv)$" |
65 | #endif //__M4A__ |
66 | |
67 | #endif |
68 | |
69 | === modified file 'mixxx/src/soundsourceoggvorbis.cpp' |
70 | --- mixxx/src/soundsourceoggvorbis.cpp 2010-02-25 10:14:57 +0000 |
71 | +++ mixxx/src/soundsourceoggvorbis.cpp 2010-02-25 22:20:33 +0000 |
72 | @@ -45,18 +45,18 @@ |
73 | Class for reading Ogg Vorbis |
74 | */ |
75 | |
76 | -SoundSourceOggVorbis::SoundSourceOggVorbis(QString qFilename) |
77 | +SoundSourceOggVorbis::SoundSourceOggVorbis(QString qFilename) |
78 | : SoundSource(qFilename) |
79 | { |
80 | QByteArray qBAFilename = qFilename.toUtf8(); |
81 | + filelength = 0; |
82 | |
83 | #ifdef __WINDOWS__ |
84 | if(ov_fopen(qBAFilename.data(), &vf) < 0) { |
85 | qDebug() << "oggvorbis: Input does not appear to be an Ogg bitstream."; |
86 | - filelength = 0; |
87 | return; |
88 | } |
89 | -#else |
90 | +#else |
91 | FILE *vorbisfile = fopen(qBAFilename.data(), "r"); |
92 | |
93 | if (!vorbisfile) { |
94 | @@ -66,7 +66,6 @@ |
95 | |
96 | if(ov_open(vorbisfile, &vf, NULL, 0) < 0) { |
97 | qDebug() << "oggvorbis: Input does not appear to be an Ogg bitstream."; |
98 | - filelength = 0; |
99 | return; |
100 | } |
101 | #endif |
102 | @@ -80,7 +79,6 @@ |
103 | if(channels > 2){ |
104 | qDebug() << "oggvorbis: No support for more than 2 channels!"; |
105 | ov_clear(&vf); |
106 | - filelength = 0; |
107 | return; |
108 | } |
109 | |
110 | @@ -90,10 +88,10 @@ |
111 | // hand. a 30 second long 48khz mono ogg and a 48khz stereo ogg both report |
112 | // 1440000 for ov_pcm_total. |
113 | ogg_int64_t ret = ov_pcm_total(&vf, -1); |
114 | - |
115 | + |
116 | if (ret >= 0) { |
117 | // We pretend that the file is stereo to the rest of the world. |
118 | - filelength = ret * 2; |
119 | + filelength = ret * 2; |
120 | } |
121 | else //error |
122 | { |
123 | @@ -120,15 +118,15 @@ |
124 | // In our speak, filepos is a sample in the file abstraction (i.e. it's |
125 | // stereo no matter what). filepos/2 is the frame we want to seek to. |
126 | Q_ASSERT(filepos%2==0); |
127 | - |
128 | + |
129 | if (ov_seekable(&vf)){ |
130 | if(ov_pcm_seek(&vf, filepos/2) != 0) { |
131 | // This is totally common (i.e. you're at EOF). Let's not leave this |
132 | // qDebug on. |
133 | - |
134 | + |
135 | // qDebug() << "ogg vorbis: Seek ERR on seekable."; |
136 | } |
137 | - |
138 | + |
139 | // Even if an error occured, return them the current position because |
140 | // that's what we promised. (Double it because ov_pcm_tell returns |
141 | // frames and we pretend to the world that everything is stereo) |
142 | @@ -149,18 +147,18 @@ |
143 | { |
144 | |
145 | Q_ASSERT(size%2==0); |
146 | - |
147 | + |
148 | char *pRead = (char*) destination; |
149 | SAMPLE *dest = (SAMPLE*) destination; |
150 | - |
151 | - |
152 | + |
153 | + |
154 | |
155 | // 'needed' is size of buffer in bytes. 'size' is size in SAMPLEs, |
156 | // which is 2 bytes. If the stream is mono, we read 'size' bytes, |
157 | // so that half the buffer is full, then below we double each |
158 | // sample on the left and right channel. If the stream is stereo, |
159 | // then ov_read interleaves the samples into the full length of |
160 | - // the buffer. |
161 | + // the buffer. |
162 | |
163 | // ov_read speaks bytes, we speak words. needed is the bytes to |
164 | // read, not words to read. |
165 | @@ -172,7 +170,7 @@ |
166 | unsigned int needed = size * channels; |
167 | |
168 | unsigned int index=0,ret=0; |
169 | - |
170 | + |
171 | // loop until requested number of samples has been retrieved |
172 | while (needed > 0) { |
173 | // read samples into buffer |
174 | @@ -203,11 +201,11 @@ |
175 | dest[i*2] = dest[i]; |
176 | dest[(i*2)+1] = dest[i]; |
177 | } |
178 | - |
179 | + |
180 | // Pretend we read twice as many bytes as we did, since we just repeated |
181 | // each pair of bytes. |
182 | index *= 2; |
183 | - } |
184 | + } |
185 | |
186 | // index is the total bytes read, so the words read is index/2 |
187 | return index / 2; |
188 | @@ -225,7 +223,7 @@ |
189 | |
190 | #ifdef __WINDOWS__ |
191 | if (ov_fopen(qBAFilename.data(), &vf) < 0) { |
192 | - qDebug() << "oggvorbis::ParseHeader : Input does not appear to be an Ogg bitstream."; |
193 | + qDebug() << "oggvorbis::ParseHeader : Input does not appear to be an Ogg bitstream."; |
194 | return ERR; |
195 | } |
196 | #else |
197 | @@ -247,47 +245,31 @@ |
198 | Track->setType("ogg"); |
199 | comment = ov_comment(&vf, -1); |
200 | if (comment == NULL) { |
201 | - qDebug() << "oggvorbis: fatal error reading file."; |
202 | - ov_clear(&vf); |
203 | - return ERR; |
204 | - } |
205 | - |
206 | - //precache |
207 | - const char* title_p = vorbis_comment_query(comment, (char*)"title", 0); //the char* cast is to shut up the compiler; libvorbis should take `const char*` here but I don't expect us to get them to change that -kousu 2009/02 |
208 | - const char* artist_p = vorbis_comment_query(comment, (char*)"artist", 0); |
209 | - const char* bpm_p = vorbis_comment_query(comment, (char*)"TBPM", 0); |
210 | - const char* album_p = vorbis_comment_query(comment, (char*)"album", 0); |
211 | - const char* year_p = vorbis_comment_query(comment, (char*)"date", 0); |
212 | - const char* genre_p = vorbis_comment_query(comment, (char*)"genre", 0); |
213 | - const char* track_p = vorbis_comment_query(comment, (char*)"tracknumber", 0); |
214 | - |
215 | - |
216 | - if(title_p) |
217 | - Track->setTitle(QString::fromUtf8(title_p)); |
218 | - if(artist_p) |
219 | - Track->setArtist(QString::fromUtf8(artist_p)); |
220 | - if(album_p) |
221 | - Track->setAlbum(QString::fromUtf8(album_p)); |
222 | - if(year_p) |
223 | - Track->setYear(QString::fromUtf8(year_p)); |
224 | - if(genre_p) |
225 | - Track->setGenre(QString::fromUtf8(genre_p)); |
226 | - if(track_p) |
227 | - Track->setTrackNumber(QString::fromUtf8(track_p)); |
228 | - if (bpm_p) { |
229 | - float bpm = str2bpm(bpm_p); |
230 | - if(bpm > 0.0f) { |
231 | - Track->setBpm(bpm); |
232 | - Track->setBpmConfirm(true); |
233 | - } |
234 | - } |
235 | - Track->setHeaderParsed(true); |
236 | + qDebug() << "oggvorbis::ParseHeader : no tags in file"; |
237 | + } else { |
238 | + //precache |
239 | + const char* title_p = vorbis_comment_query(comment, (char*)"title", 0); //the char* cast is to shut up the compiler; libvorbis should take `const char*` here but I don't expect us to get them to change that -kousu 2009/02 |
240 | + const char* artist_p = vorbis_comment_query(comment, (char*)"artist", 0); |
241 | + const char* bpm_p = vorbis_comment_query(comment, (char*)"TBPM", 0); |
242 | + |
243 | + if(title_p) |
244 | + Track->setTitle(title_p); |
245 | + if(artist_p) |
246 | + Track->setArtist(artist_p); |
247 | + if (bpm_p) { |
248 | + float bpm = str2bpm(bpm_p); |
249 | + if(bpm > 0.0f) { |
250 | + Track->setBpm(bpm); |
251 | + Track->setBpmConfirm(true); |
252 | + } |
253 | + } |
254 | + } |
255 | |
256 | int duration = (int)ov_time_total(&vf, -1); |
257 | if (duration == OV_EINVAL) { |
258 | ov_clear(&vf); //close on return ! |
259 | - return ERR; |
260 | - } |
261 | + return ERR; |
262 | + } |
263 | Track->setDuration(duration); |
264 | Track->setBitrate(ov_bitrate(&vf, -1)/1000); |
265 | |
266 | @@ -303,9 +285,9 @@ |
267 | ov_clear(&vf); |
268 | return ERR; |
269 | } |
270 | - |
271 | + |
272 | ov_clear(&vf); |
273 | - Track->setHeaderParsed(true); |
274 | + Track->setHeaderParsed(true); |
275 | return OK; |
276 | } |
277 | |
278 | |
279 | === modified file 'mixxx/src/soundsourceproxy.cpp' |
280 | --- mixxx/src/soundsourceproxy.cpp 2010-02-25 10:14:57 +0000 |
281 | +++ mixxx/src/soundsourceproxy.cpp 2010-02-25 22:20:33 +0000 |
282 | @@ -29,52 +29,74 @@ |
283 | #ifdef __FFMPEGFILE__ |
284 | #include "soundsourceffmpeg.h" |
285 | #endif |
286 | +#ifdef __WV__ |
287 | +#include "soundsourcewv.h" |
288 | +#endif |
289 | + |
290 | |
291 | //Added by qt3to4: |
292 | #include <Q3ValueList> |
293 | |
294 | |
295 | -SoundSourceProxy::SoundSourceProxy(QString qFilename) |
296 | - : SoundSource(qFilename), |
297 | - m_pSoundSource(NULL) { |
298 | - initialize(qFilename); |
299 | -} |
300 | - |
301 | -SoundSourceProxy::SoundSourceProxy(TrackInfoObject * pTrack) |
302 | - : SoundSource(pTrack->getLocation()), |
303 | - m_pSoundSource(NULL) { |
304 | - initialize(pTrack->getLocation()); |
305 | - |
306 | - // Set the track duration in seconds |
307 | - if(getSrate()) |
308 | - pTrack->setDuration(length()/(2*getSrate())); |
309 | - else |
310 | - pTrack->setDuration(0); |
311 | -} |
312 | - |
313 | -void SoundSourceProxy::initialize(QString qFilename) { |
314 | - |
315 | -#ifdef __FFMPEGFILE__ |
316 | - m_pSoundSource = new SoundSourceFFmpeg(qFilename); |
317 | - return; |
318 | -#endif |
319 | - QString filename = qFilename.toLower(); |
320 | - if (filename.endsWith(".mp3")) |
321 | - m_pSoundSource = new SoundSourceMp3(qFilename); |
322 | - else if (filename.endsWith(".ogg") || filename.endsWith(".oga")) |
323 | - m_pSoundSource = new SoundSourceOggVorbis(qFilename); |
324 | -#ifdef __M4A__ |
325 | - else if (filename.endsWith(".m4a") || |
326 | - filename.endsWith(".mp4")) |
327 | - m_pSoundSource = new SoundSourceM4A(qFilename); |
328 | -#endif |
329 | -#ifdef __SNDFILE__ |
330 | - else if (filename.endsWith(".wav") || |
331 | - filename.endsWith(".aif") || |
332 | - filename.endsWith(".aiff") || |
333 | - filename.endsWith(".flac")) |
334 | - m_pSoundSource = new SoundSourceSndFile(qFilename); |
335 | -#endif |
336 | +SoundSourceProxy::SoundSourceProxy(QString qFilename) : SoundSource(qFilename) |
337 | +{ |
338 | +#ifdef __FFMPEGFILE__ |
339 | + m_pSoundSource = new SoundSourceFFmpeg(qFilename); |
340 | + return; |
341 | +#endif |
342 | + QString filename = qFilename.toLower(); |
343 | + if (filename.endsWith(".mp3")) |
344 | + m_pSoundSource = new SoundSourceMp3(qFilename); |
345 | + else if (filename.endsWith(".ogg") || filename.endsWith(".oga")) |
346 | + m_pSoundSource = new SoundSourceOggVorbis(qFilename); |
347 | +#ifdef __M4A__ |
348 | + else if (filename.endsWith(".m4a")) |
349 | + m_pSoundSource = new SoundSourceM4A(qFilename); |
350 | +#endif |
351 | +#ifdef __WV__ |
352 | + else if (qFilename.toLower().endsWith(".wv")) |
353 | + m_pSoundSource = new SoundSourceWV(qFilename); |
354 | +#endif |
355 | +#ifdef __SNDFILE__ |
356 | + else if (filename.endsWith(".wav") || |
357 | + filename.endsWith(".aif") || |
358 | + filename.endsWith(".aiff") || |
359 | + filename.endsWith(".flac")) |
360 | + m_pSoundSource = new SoundSourceSndFile(qFilename); |
361 | +#endif |
362 | +} |
363 | + |
364 | +SoundSourceProxy::SoundSourceProxy(TrackInfoObject * pTrack) : SoundSource(pTrack->getLocation()) |
365 | +{ |
366 | + QString qFilename = pTrack->getLocation(); |
367 | + |
368 | +#ifdef __FFMPEGFILE__ |
369 | + m_pSoundSource = new SoundSourceFFmpeg(qFilename); |
370 | + return; |
371 | +#endif |
372 | + QString filename = qFilename.toLower(); |
373 | + if (filename.endsWith(".mp3")) |
374 | + m_pSoundSource = new SoundSourceMp3(qFilename); |
375 | + else if (filename.endsWith(".ogg") || filename.endsWith(".oga")) |
376 | + m_pSoundSource = new SoundSourceOggVorbis(qFilename); |
377 | +#ifdef __M4A__ |
378 | + else if (filename.endsWith(".m4a")) |
379 | + m_pSoundSource = new SoundSourceM4A(qFilename); |
380 | +#endif |
381 | +#ifdef __WV__ |
382 | + else if (qFilename.toLower().endsWith(".wv")) |
383 | + m_pSoundSource = new SoundSourceWV(qFilename); |
384 | +#endif |
385 | +#ifdef __SNDFILE__ |
386 | + else if (filename.endsWith(".wav") || |
387 | + filename.endsWith(".aif") || |
388 | + filename.endsWith(".aiff") || |
389 | + filename.endsWith(".flac")) |
390 | + m_pSoundSource = new SoundSourceSndFile(qFilename); |
391 | +#endif |
392 | + |
393 | + if(getSrate()) pTrack->setDuration(length()/(2*getSrate())); |
394 | + else pTrack->setDuration(0); |
395 | } |
396 | |
397 | SoundSourceProxy::~SoundSourceProxy() |
398 | @@ -118,9 +140,12 @@ |
399 | else if (filename.endsWith(".ogg") || filename.endsWith(".oga")) |
400 | return SoundSourceOggVorbis::ParseHeader(p); |
401 | #ifdef __M4A__ |
402 | - else if (filename.endsWith(".m4a") || |
403 | - filename.endsWith(".mp4")) |
404 | - return SoundSourceM4A::ParseHeader(p); |
405 | + else if (filename.endsWith(".m4a")) |
406 | + return SoundSourceM4A::ParseHeader(p); |
407 | +#endif |
408 | +#ifdef __WV__ |
409 | + else if (qFilename.toLower().endsWith(".wv")) |
410 | + return SoundSourceWV::ParseHeader(p); |
411 | #endif |
412 | #ifdef __SNDFILE__ |
413 | else if (filename.endsWith(".wav") || |
414 | |
415 | === added file 'mixxx/src/soundsourcewv.cpp' |
416 | --- mixxx/src/soundsourcewv.cpp 1970-01-01 00:00:00 +0000 |
417 | +++ mixxx/src/soundsourcewv.cpp 2010-02-25 22:20:33 +0000 |
418 | @@ -0,0 +1,194 @@ |
419 | +//soundsourcewv.cpp : sound source proxy for .wv (WavPack files) |
420 | +//created by fenugrec |
421 | +//great help from rryan & others on #mixxx |
422 | +//format_samples adapted from cmus (Peter Lemenkov) |
423 | + |
424 | +#include "trackinfoobject.h" |
425 | +#include "soundsourcewv.h" |
426 | + |
427 | +#include <QtDebug> |
428 | + |
429 | +void format_samples(int bps, char *dst, int32_t *src, uint32_t count); |
430 | + |
431 | +SoundSourceWV::SoundSourceWV(QString qFilename) : SoundSource(qFilename) |
432 | +{ |
433 | + // Initialize variables to invalid values in case loading fails. |
434 | + filewvc=NULL; |
435 | + QByteArray qBAFilename = qFilename.toUtf8(); |
436 | + char msg[80]; //hold posible error message |
437 | + |
438 | + filewvc = WavpackOpenFileInput(qBAFilename.data(),msg,OPEN_2CH_MAX,0); |
439 | + if (!filewvc) { |
440 | + qDebug() << "SSWV::constructor: failed to open file : "<<msg; |
441 | + return; |
442 | + } |
443 | + if (WavpackGetMode(filewvc) & MODE_FLOAT) { |
444 | + qDebug() << "SSWV::constructor: cannot load 32bit float files"; |
445 | + WavpackCloseFile(filewvc); |
446 | + filewvc=NULL; |
447 | + return; |
448 | + } |
449 | + // wavpack_open succeeded -> populate variables |
450 | + filelength = WavpackGetNumSamples(filewvc); |
451 | + SRATE=WavpackGetSampleRate(filewvc); |
452 | + channels=WavpackGetReducedChannels(filewvc); |
453 | + Bps=WavpackGetBytesPerSample(filewvc); |
454 | + qDebug () << "SSWV::constructor: opened filewvc with filelength: "<<filelength<<" SRATE: " << SRATE |
455 | + << " channels: " << channels << " bytes per samp: "<<Bps; |
456 | + if (Bps>2) { |
457 | + qDebug() << "SSWV::constructor: warning: input file has > 2 bytes per sample, will be truncated to 16bits"; |
458 | + } |
459 | +} |
460 | + |
461 | + |
462 | +SoundSourceWV::~SoundSourceWV(){ |
463 | + if (filewvc) { |
464 | + WavpackCloseFile(filewvc); |
465 | + filewvc=NULL; |
466 | + } |
467 | +} |
468 | + |
469 | + |
470 | +long SoundSourceWV::seek(long filepos){ |
471 | + if (WavpackSeekSample(filewvc,filepos>>1) != true) { |
472 | + qDebug() << "SSWV::seek : could not seek to sample #" << (filepos>>1); |
473 | + return 0; |
474 | + } |
475 | + return filepos; |
476 | +} |
477 | + |
478 | + |
479 | +unsigned SoundSourceWV::read(volatile unsigned long size, const SAMPLE* destination){ |
480 | + //SAMPLE is "short int" => 16bits. [size] is timesamps*2 (because L+R) |
481 | + SAMPLE * dest = (SAMPLE*) destination; |
482 | + unsigned long sampsread=0; |
483 | + unsigned long timesamps, tsdone; |
484 | + |
485 | + //tempbuffer is fixed size : WV_BUF_LENGTH of uint32 |
486 | + while (sampsread != size) { |
487 | + timesamps=(size-sampsread)>>1; //timesamps still remaining |
488 | + if (timesamps > (WV_BUF_LENGTH/channels)) { //if requested size requires more than one buffer filling |
489 | + timesamps=(WV_BUF_LENGTH/channels); //tempbuffer must hold (timesamps * channels) samples |
490 | + qDebug() << "SSWV::read : performance warning, size requested > buffer size !"; |
491 | + } |
492 | + |
493 | + tsdone=WavpackUnpackSamples(filewvc, tempbuffer, timesamps); //fill temp buffer with timesamps*4bytes*channels |
494 | + //data is right justified, format_samples() fixes that. |
495 | + |
496 | + format_samples(Bps, (char *) (dest + (sampsread>>1)*channels), tempbuffer, tsdone*channels); //this will unpack the 4byte/sample |
497 | + //output of wUnpackSamples(), sign-extending or truncating to output 16bit / sample. |
498 | + //specifying dest+sampsread should resume the conversion where it was left if size requested |
499 | + //required multiple reads (size req. > fixed buffer size) |
500 | + |
501 | + sampsread = sampsread + (tsdone<<1); |
502 | + if (tsdone!=timesamps) { |
503 | + qDebug () << "SSWV::read : WavpackUnpackSamples read "<<sampsread<<" asamps out of "<<size<<" requested"; |
504 | + break; //exit the while loop : subsequent reads are sure to read less than required. |
505 | + } |
506 | + |
507 | + } |
508 | + |
509 | + if (channels==1) { //if MONO : expand array to double it's size; see ssov.cpp |
510 | + for(int i=(sampsread/2-1); i>=0; i--) { //algo courtesy of rryan ! |
511 | + dest[i*2] = dest[i]; //go through array backwards, expanding and copying L -> R |
512 | + dest[(i*2)+1] = dest[i]; |
513 | + } |
514 | + } |
515 | + |
516 | + return sampsread; |
517 | +} |
518 | + |
519 | + |
520 | +inline long unsigned SoundSourceWV::length(){ |
521 | + //filelength is # of timesamps. |
522 | + return filelength<<1; |
523 | +} |
524 | + |
525 | + |
526 | +int SoundSourceWV::ParseHeader( TrackInfoObject * Track){ |
527 | + QString filename = Track->getLocation(); |
528 | + QByteArray qBAFilename = filename.toUtf8(); |
529 | + char msg[80]; |
530 | + *msg='\0'; |
531 | + |
532 | + WavpackContext *twvc = WavpackOpenFileInput(filename, msg, OPEN_TAGS,0); |
533 | + if (!twvc) { |
534 | + qDebug() << "SSWV::ParseHeader : WavpackOpenFileInput: " << msg; |
535 | + return ERR; |
536 | + } |
537 | + |
538 | + int wavpackmode = WavpackGetMode(twvc); |
539 | + if (MODE_FLOAT & wavpackmode) { |
540 | + qDebug() << "SSWV::ParseHeader: 32 bit float file format will not be loaded"; |
541 | + WavpackCloseFile(twvc); |
542 | + return ERR; |
543 | + } |
544 | + |
545 | + Track->setType("wv"); |
546 | + if (!(MODE_VALID_TAG & wavpackmode)) { |
547 | + qDebug() << "SSWV::ParseHeader: no valid tags"; |
548 | + } else { |
549 | + char wvtag[80]; |
550 | + if (WavpackGetTagItem(twvc, "TITLE", wvtag, 80)) //should be case-insensitive. |
551 | + Track->setTitle(QString(wvtag)); |
552 | + if (WavpackGetTagItem(twvc, "ARTIST", wvtag, 80)) |
553 | + Track->setArtist(QString(wvtag)); |
554 | + if (WavpackGetTagItem(twvc, "TBPM", wvtag, 80)) { |
555 | + float bpm=str2bpm(QString(wvtag)); |
556 | + if (bpm>0.0f) { |
557 | + Track->setBpm(bpm); |
558 | + Track->setBpmConfirm(true); |
559 | + } |
560 | + } |
561 | + } |
562 | + |
563 | + Track->setDuration(WavpackGetNumSamples(twvc) / WavpackGetSampleRate(twvc)); |
564 | + Track->setBitrate(WavpackGetAverageBitrate(twvc, 0)/1000); |
565 | + Track->setSampleRate(WavpackGetSampleRate(twvc)); |
566 | + Track->setChannels(WavpackGetReducedChannels(twvc)); |
567 | + |
568 | + WavpackCloseFile(twvc); |
569 | + Track->setHeaderParsed(true); |
570 | + return OK; |
571 | +} |
572 | + |
573 | + |
574 | + |
575 | +void format_samples(int Bps, char *dst, int32_t *src, uint32_t count) |
576 | +{ |
577 | + //this handles converting the fixed 32bit per sample produced by UnpackSamples |
578 | + //to 16 bps, by truncating (24/32) or sign-extending (8) |
579 | + //could eventually be asm-optimized.. |
580 | + int32_t temp; |
581 | + |
582 | + switch (Bps) { |
583 | + case 1: |
584 | + while (count--) { |
585 | + *dst++ = (char) 0; //left shift the 8 bit sample |
586 | + *dst++ = (char) *src++ ;//+ 128; //only works with u8int ? |
587 | + } |
588 | + break; |
589 | + case 2: |
590 | + while (count--) { |
591 | + *dst++ = (char) (temp = *src++); //low byte |
592 | + *dst++ = (char) (temp >> 8); //high byte |
593 | + } |
594 | + break; |
595 | + case 3: //modified to truncate to 16bits |
596 | + while (count--) { |
597 | + *dst++ = (char) (temp = (*src++) >> 8); |
598 | + *dst++ = (char) (temp >> 8); |
599 | + } |
600 | + break; |
601 | + case 4: //also truncates |
602 | + while (count--) { |
603 | + *dst++ = (char) (temp = (*src++) >> 16); |
604 | + *dst++ = (char) (temp >> 8); |
605 | + //*dst++ = (char) (temp >> 16); |
606 | + //*dst++ = (char) (temp >> 24); |
607 | + } |
608 | + break; |
609 | + } |
610 | + |
611 | + return; |
612 | +} |
613 | |
614 | === added file 'mixxx/src/soundsourcewv.h' |
615 | --- mixxx/src/soundsourcewv.h 1970-01-01 00:00:00 +0000 |
616 | +++ mixxx/src/soundsourcewv.h 2010-02-25 22:20:33 +0000 |
617 | @@ -0,0 +1,33 @@ |
618 | +//soundsourcewv.h |
619 | +// wavpack sound proxy for mixxx. |
620 | +// fenugrec 12/2009 |
621 | + |
622 | + |
623 | +#ifndef SOUNDSOURCEWV_H |
624 | +#define SOUNDSOURCEWV_H |
625 | + |
626 | +#include "qstring.h" |
627 | +#include "soundsource.h" |
628 | + |
629 | +#include "wavpack/wavpack.h" |
630 | + |
631 | +#define WV_BUF_LENGTH 65536 |
632 | +class TrackInfoObject; |
633 | + |
634 | +class SoundSourceWV : public SoundSource { |
635 | + public: |
636 | + SoundSourceWV(QString qFilename); |
637 | + ~SoundSourceWV(); |
638 | + long seek(long); |
639 | + unsigned read(unsigned long size, const SAMPLE*); |
640 | + inline long unsigned length(); |
641 | + static int ParseHeader( TrackInfoObject * ); |
642 | + private: |
643 | + int channels; |
644 | + int Bps; |
645 | + unsigned long filelength; |
646 | + WavpackContext * filewvc; //works as a file handle to access the wv file. |
647 | + int32_t tempbuffer[WV_BUF_LENGTH]; //hax ! legacy from cmus. this is 64k*4bytes. |
648 | + |
649 | +}; |
650 | +#endif |
651 | \ No newline at end of file |
652 | |
653 | === modified file 'mixxx/src/test/soundFileFormats/README.txt' |
654 | --- mixxx/src/test/soundFileFormats/README.txt 2009-07-21 18:07:08 +0000 |
655 | +++ mixxx/src/test/soundFileFormats/README.txt 2010-02-25 22:20:33 +0000 |
656 | @@ -1,8 +1,11 @@ |
657 | The scripts in this directory allow you to generate all the bit depths and sample rates of the file types that Mixxx supports. |
658 | |
659 | -You must have sox, lame and bzip2 installed on your system for them to work. |
660 | +You must have ffmpeg, wavpack and bzip2 installed on your system for them to work. |
661 | +If you do not specify "ff" when calling the script, you will also need vorbis-tools and NeroAAC |
662 | +(freely available at http://www.nero.com/enu/technologies-aac-codec.html ) |
663 | |
664 | -Then run ./generateFiles on OSX and Linux, or generateFiles.cmd on Windows. Then load each of the resulting files into Mixxx and verify for each: |
665 | +Then run ./generateFiles.sh on OSX and Linux. Then load each of the resulting files into Mixxx and verify for each: |
666 | +Running " ./generateFiles ff " will use only ffmpeg (some limitations apply, see script for details) |
667 | |
668 | 1) it loads without problems |
669 | 2) the large and summary waveforms eventually display something (just looks like a green bar) |
670 | @@ -13,4 +16,4 @@ |
671 | |
672 | If any of these fails, please report a bug using the instructions on this page: http://mixxx.org/wiki/doku.php/reporting_bugs |
673 | |
674 | -When you're done, you can run ./generateFiles clean to delete all the generated files |
675 | \ No newline at end of file |
676 | +When you're done, you can run "./generateFiles clean" to delete all the generated files - the whole testsuite takes about 200MB ! |
677 | |
678 | === modified file 'mixxx/src/test/soundFileFormats/generateFiles.sh' |
679 | --- mixxx/src/test/soundFileFormats/generateFiles.sh 2009-07-21 20:19:02 +0000 |
680 | +++ mixxx/src/test/soundFileFormats/generateFiles.sh 2010-02-25 22:20:33 +0000 |
681 | @@ -2,6 +2,7 @@ |
682 | # |
683 | # Test sound file generator for Mixxx |
684 | # created by Sean M. Pappalardo on 7/20/2009 |
685 | +# modified by fenugrec 01-2009 |
686 | # |
687 | # This script generates sound files in all of the formats |
688 | # Mixxx supports with all of the various sample sizes, |
689 | @@ -9,41 +10,34 @@ |
690 | # |
691 | # Pass it the "clean" argument to delete all the files it makes |
692 | # Pass it the "table" argument to generate a table of all the file formats & types in Wiki syntax |
693 | - |
694 | -formats=("wav" "mp3" "ogg" "flac") # mp3 must come after wav because it converts the wavs |
695 | +# Pass it the "ff" argument to use only ffmpeg (this will disable: |
696 | +# 22kHz m4a & ogg files |
697 | +# 96kHz ogg files |
698 | + |
699 | +formats=("wav" "mp3" "m4a" "ogg" "flac" "wv") #wav must be generated first |
700 | + |
701 | channels=(1 2) |
702 | -samplesizes=(16 24 32) |
703 | +channelnames=("Mono" "Stereo") #used as channelnames[channels-1] to get friendlyname. |
704 | +samplesizes=(s16 s24 s32) #sample_fmt for ffmpeg |
705 | samplerates=(22050 32000 44100 48000 96000) |
706 | |
707 | + |
708 | if [ "$1" == "clean" ] |
709 | then |
710 | for format in ${formats[*]} |
711 | do |
712 | + if [ -e invalidfile.${format} ] |
713 | + then |
714 | + echo "Removing invalidfile.${format}" |
715 | + rm invalidfile.${format} |
716 | + fi |
717 | + |
718 | for rate in ${samplerates[*]} |
719 | do |
720 | friendlyrate=`expr $rate / 1000` |
721 | for channel in ${channels[*]} |
722 | do |
723 | - if [ $channel -eq 1 ] |
724 | - then |
725 | - friendlychannel="Mono" |
726 | - fi |
727 | - if [ $channel -eq 2 ] |
728 | - then |
729 | - friendlychannel="Stereo" |
730 | - fi |
731 | - |
732 | - # Remove files from formats for which sample size is irrelevant |
733 | - if ( [ $format == "ogg" ] || [ $format == "mp3" ] ) && [ -e test${friendlyrate}k${friendlychannel}.${format} ] |
734 | - then |
735 | - echo "Removing test${friendlyrate}k${friendlychannel}.${format}" |
736 | - rm test${friendlyrate}k${friendlychannel}.${format} |
737 | - if [ $? -gt 0 ] |
738 | - then |
739 | - echo "Error #$?" |
740 | - exit 1 |
741 | - fi |
742 | - fi |
743 | + friendlychannel=${channelnames[${channel}-1]} |
744 | |
745 | for ssize in ${samplesizes[*]} |
746 | do |
747 | @@ -51,10 +45,6 @@ |
748 | then |
749 | echo "Removing test${ssize}bit${friendlyrate}k${friendlychannel}.${format}" |
750 | rm test${ssize}bit${friendlyrate}k${friendlychannel}.${format} |
751 | - if [ $? -gt 0 ] |
752 | - then |
753 | - echo "Error #$?" |
754 | - exit 1 |
755 | fi |
756 | fi |
757 | done |
758 | @@ -86,111 +76,98 @@ |
759 | do |
760 | if [ "$1" == "table" ] |
761 | then |
762 | - if [ $format == "ogg" ] || [ $format == "mp3" ] |
763 | - then |
764 | - friendlyformat="OGG Vorbis" |
765 | - fi |
766 | - if [ $format == "wav" ] |
767 | - then |
768 | - friendlyformat="WAVE/AIFF" |
769 | - fi |
770 | - if [ $format == "mp3" ] |
771 | - then |
772 | - friendlyformat="MP3" |
773 | - fi |
774 | - if [ $format == "flac" ] |
775 | - then |
776 | - friendlyformat="FLAC" |
777 | - fi |
778 | + case $format in |
779 | + "ogg") friendlyformat="OGG Vorbis";; |
780 | + "m4a") friendlyformat="M4A";; |
781 | + "wav") friendlyformat="WAVE/AIFF";; |
782 | + "mp3") friendlyformat="MP3";; |
783 | + "flac") friendlyformat="FLAC";; |
784 | + "wv") friendlyformat="WavPack";; |
785 | + esac |
786 | echo |
787 | echo "==== $friendlyformat ====" |
788 | echo "^ Channels ^ Bit depth ^ Sample Rate ^ Does it work? ^" |
789 | + else #if not generating a table |
790 | + vorbisflag="" |
791 | + enccmd="ffmpeg -i test\${ssize}bit\${friendlyrate}k\${friendlychannel}.wav -ab 128k \$vorbisflag test\${ssize}bit\${friendlyrate}k\${friendlychannel}.\${format}" |
792 | + #loaded default encoding command, which may be replaced by format-specific commands: |
793 | + |
794 | + if [ $format == "wav" ] |
795 | + then |
796 | + enccmd="ffmpeg -i 1kHzR440HzLReference_32i96kStereo.wav -sample_fmt \${ssize} -ar \${rate} -ac \${channel} test\${ssize}bit\${friendlyrate}k\${friendlychannel}.wav" |
797 | + elif [ $format == "ogg" ] |
798 | + then |
799 | + vorbisflag="-acodec libvorbis" #because .ogg is ambiguous to ffmpeg |
800 | + elif [ $format == "wv" ] |
801 | + then #use wavpack regardless (ffmpeg can't encode wv) |
802 | + enccmd="wavpack -b200 test\${ssize}bit\${friendlyrate}k\${friendlychannel}.wav" |
803 | + fi |
804 | + |
805 | + if [ "$1" != "ff" ] |
806 | + then #use specific encoders as well as ffmpeg |
807 | + if [ $format == "ogg" ] |
808 | + then |
809 | + enccmd="oggenc test\${ssize}bit\${friendlyrate}k\${friendlychannel}.wav" |
810 | + elif [ $format == "m4a" ] |
811 | + then |
812 | + enccmd="neroAacEnc -if test\${ssize}bit\${friendlyrate}k\${friendlychannel}.wav -of test\${ssize}bit\${friendlyrate}k\${friendlychannel}.\${format}" |
813 | + fi |
814 | + fi |
815 | + |
816 | + echo "Generating invalid file : invalidfile.${format}" |
817 | + echo "Invalid ${format} file" > invalidfile.${format} |
818 | fi |
819 | + |
820 | for channel in ${channels[*]} |
821 | do |
822 | - if [ $channel -eq 1 ] |
823 | - then |
824 | - friendlychannel="Mono" |
825 | - lameopt="-m m" |
826 | - fi |
827 | - if [ $channel -eq 2 ] |
828 | - then |
829 | - friendlychannel="Stereo" |
830 | - lameopt="" |
831 | - fi |
832 | + friendlychannel=${channelnames[${channel}-1]} #not sure if this is faster than ifs... |
833 | + |
834 | for ssize in ${samplesizes[*]} |
835 | do |
836 | - # Hack because sox doesn't abort if the parameters are out of spec |
837 | - if [ $ssize -gt 24 ] && [ $format == "flac" ] |
838 | - then |
839 | - if [ "$1" != "table" ] |
840 | - then |
841 | - echo "FLAC doesn't support ${ssize}-bit, skipping" |
842 | - fi |
843 | - break |
844 | - fi |
845 | - |
846 | - # vorbis and MP3 don't use bit depth, so only run sox once per sample rate |
847 | - if [ $ssize != ${samplesizes[0]} ] && ( [ $format == "ogg" ] || [ $format == "mp3" ] ) |
848 | - then |
849 | - break |
850 | - fi |
851 | + #MP3 & m4a don't use bit depth (s16 only), so only run once per sample rate |
852 | + if [ $ssize != "s16" ] && ( [ $format == "mp3" ] || [ $format == "m4a" ] ) |
853 | + then |
854 | + continue |
855 | + fi |
856 | + |
857 | for rate in ${samplerates[*]} |
858 | do |
859 | friendlyrate=`expr $rate / 1000` |
860 | - problem="false" |
861 | - |
862 | - # Hack because lame doesn't abort if the parameters are out of spec |
863 | - if [ $rate -gt 48000 ] && [ $format == "mp3" ] |
864 | - then |
865 | - if [ "$1" != "table" ] |
866 | - then |
867 | - echo "MP3 doesn't support ${rate}Hz, skipping" |
868 | - fi |
869 | - problem="true" |
870 | - fi |
871 | - |
872 | - if [ "$1" == "table" ] && [ "$problem" != "true" ] |
873 | - then |
874 | - if [ $format == "ogg" ] || [ $format == "mp3" ] |
875 | - then # sample size is irrelevant for MP3 and Vorbis |
876 | + #disable impossible formats. |
877 | + if [ $rate -gt 48000 ] && [ $format == "mp3" ] #96kHz MP3 unsupported |
878 | + then |
879 | + continue #next for-iteration |
880 | + elif [ $rate -lt 32000 ] && ([ $format == "m4a" ] || [ $format == "ogg" ]) && [ "$1" == "ff" ] |
881 | + then #disable 22khz m4a & ogg files with ffmpeg(buggy) |
882 | + continue |
883 | + elif [ $rate -gt 48000 ] && [ $format == "ogg" ] && [ "$1" == "ff" ] |
884 | + then #disable 96kHz ogg with ffmpeg |
885 | + continue |
886 | + fi |
887 | + |
888 | + if [ "$1" == "table" ] |
889 | + then |
890 | + if [ $format == "mp3" ] || [ $format == "m4a" ] |
891 | + then # sample size is irrelevant for MP3 and m4a |
892 | if [ $channel -eq 1 ] |
893 | then |
894 | echo "^ Mono ^ ^ $rate Hz | |" |
895 | else |
896 | - echo "^ $friendlychannel ^ ^ $rate Hz | |" |
897 | + echo "^ Stereo ^ ^ $rate Hz | |" |
898 | fi |
899 | else |
900 | if [ $channel -eq 1 ] |
901 | then |
902 | - echo "^ Mono ^ $ssize-bit ^ $rate Hz | |" |
903 | + echo "^ Mono ^ ${ssize#s}-bit ^ $rate Hz | |" |
904 | else |
905 | - echo "^ $friendlychannel ^ $ssize-bit ^ $rate Hz | |" |
906 | - fi |
907 | - fi |
908 | - fi |
909 | - |
910 | - if [ "$problem" != "true" ] && ( [ $format == "ogg" ] || [ $format == "mp3" ] ) |
911 | - then |
912 | - if [ "$1" != "table" ] |
913 | - then |
914 | - echo "Generating ${rate}Hz ${channel}-channel ${format} file" |
915 | - if [ $format == "mp3" ] |
916 | - then # sox can't make MP3s by default, so we use LAME |
917 | - lame -S $lameopt --tt test${friendlyrate}k${friendlychannel} test32bit${friendlyrate}k${friendlychannel}.wav test${friendlyrate}k${friendlychannel}.mp3 |
918 | - else # For formats where sample size is irrelevant |
919 | - sox -V0 1kHzR440HzLReference_32i96kStereo.wav -c ${channel} -r ${rate} test${friendlyrate}k${friendlychannel}.${format} |
920 | - fi |
921 | - problem="true" |
922 | - fi |
923 | - fi |
924 | - |
925 | - # Use sox by default |
926 | - if [ "$1" != "table" ] && [ $problem != "true" ] |
927 | - then |
928 | - echo "Generating ${ssize}-bit ${rate}Hz ${channel}-channel ${format} file" |
929 | - sox -V0 1kHzR440HzLReference_32i96kStereo.wav -b ${ssize} -c ${channel} -r ${rate} test${ssize}bit${friendlyrate}k${friendlychannel}.${format} |
930 | - fi |
931 | + echo "^ Stereo ^ ${ssize#s}-bit ^ $rate Hz | |" |
932 | + fi |
933 | + fi |
934 | + else #not generating a table |
935 | + echo "Generating ${ssize}bit ${rate}Hz ${friendlychannel} ${format} file" |
936 | + exec `eval ${enccmd}` 2>/dev/null #hack : parse enccmd and run it |
937 | + fi |
938 | + |
939 | if [ $? -gt 1 ] |
940 | then |
941 | echo "Error #$?, aborting" |
[copied from 1.7 merge proposal]
Wavpack support is pretty much ready to be included. SConscript
options may need to be tweaked to merge properly. Affected files:
src/SConscript audiofiles. h proxy.cpp soundFileFormat s/generateFiles .sh wv.{cpp, h} (added)
src/defs_
src/soundsource
src/test/
src/soundsource
Hence, it should not affect the functionality of mixxx beyond the
ability to process *.wv files.
Compiling mixxx with wavpack requires libwavpack > 4.0
(not hardcoded, any recent version should work)