Mercurial > cortex
comparison org/hearing.org @ 220:c5f6d880558b
making hearing.org up-to-date
author | Robert McIntyre <rlm@mit.edu> |
---|---|
date | Sat, 11 Feb 2012 07:08:38 -0700 |
parents | facc2ef3fe5c |
children | 7c374c6cfe17 |
comparison
equal
deleted
inserted
replaced
219:5f14fd7b1288 | 220:c5f6d880558b |
---|---|
7 #+INCLUDE: ../../aurellem/org/level-0.org | 7 #+INCLUDE: ../../aurellem/org/level-0.org |
8 #+BABEL: :exports both :noweb yes :cache no :mkdirp yes | 8 #+BABEL: :exports both :noweb yes :cache no :mkdirp yes |
9 | 9 |
10 * Hearing | 10 * Hearing |
11 | 11 |
12 I want to be able to place ears in a similar manner to how I place | 12 At the end of this post I will have simulated ears that work the same |
13 the eyes. I want to be able to place ears in a unique spatial | 13 way as the simulated eyes in the last post. I will be able to place |
14 position, and receive as output at every tick the F.F.T. of whatever | 14 any number of ear-nodes in a blender file, and they will bind to the |
15 signals are happening at that point. | 15 closest physical object and follow it as it moves around. Each ear |
16 will provide access to the sound data it picks up between every frame. | |
16 | 17 |
17 Hearing is one of the more difficult senses to simulate, because there | 18 Hearing is one of the more difficult senses to simulate, because there |
18 is less support for obtaining the actual sound data that is processed | 19 is less support for obtaining the actual sound data that is processed |
19 by jMonkeyEngine3. | 20 by jMonkeyEngine3. There is no "split-screen" support for rendering |
21 sound from different points of view, and there is no way to directly | |
22 access the rendered sound data. | |
23 | |
24 ** Brief Description of jMonkeyEngine's Sound System | |
20 | 25 |
21 jMonkeyEngine's sound system works as follows: | 26 jMonkeyEngine's sound system works as follows: |
22 | 27 |
23 - jMonkeyEngine uses the =AppSettings= for the particular application | 28 - jMonkeyEngine uses the =AppSettings= for the particular application |
24 to determine what sort of =AudioRenderer= should be used. | 29 to determine what sort of =AudioRenderer= should be used. |
25 - although some support is provided for multiple AudioRendering | 30 - Although some support is provided for multiple AudioRendering |
26 backends, jMonkeyEngine at the time of this writing will either | 31 backends, jMonkeyEngine at the time of this writing will either |
27 pick no AudioRenderer at all, or the =LwjglAudioRenderer= | 32 pick no =AudioRenderer= at all, or the =LwjglAudioRenderer=. |
28 - jMonkeyEngine tries to figure out what sort of system you're | 33 - jMonkeyEngine tries to figure out what sort of system you're |
29 running and extracts the appropriate native libraries. | 34 running and extracts the appropriate native libraries. |
30 - the =LwjglAudioRenderer= uses the [[http://lwjgl.org/][=LWJGL=]] (LightWeight Java Game | 35 - The =LwjglAudioRenderer= uses the [[http://lwjgl.org/][=LWJGL=]] (LightWeight Java Game |
31 Library) bindings to interface with a C library called [[http://kcat.strangesoft.net/openal.html][=OpenAL=]] | 36 Library) bindings to interface with a C library called [[http://kcat.strangesoft.net/openal.html][=OpenAL=]] |
32 - =OpenAL= calculates the 3D sound localization and feeds a stream of | 37 - =OpenAL= renders the 3D sound and feeds the rendered sound directly |
33 sound to any of various sound output devices with which it knows | 38 to any of various sound output devices with which it knows how to |
34 how to communicate. | 39 communicate. |
35 | 40 |
36 A consequence of this is that there's no way to access the actual | 41 A consequence of this is that there's no way to access the actual |
37 sound data produced by =OpenAL=. Even worse, =OpenAL= only supports | 42 sound data produced by =OpenAL=. Even worse, =OpenAL= only supports |
38 one /listener/, which normally isn't a problem for games, but becomes | 43 one /listener/ (it renders sound data from only one perspective), |
39 a problem when trying to make multiple AI creatures that can each hear | 44 which normally isn't a problem for games, but becomes a problem when |
40 the world from a different perspective. | 45 trying to make multiple AI creatures that can each hear the world from |
46 a different perspective. | |
41 | 47 |
42 To make many AI creatures in jMonkeyEngine that can each hear the | 48 To make many AI creatures in jMonkeyEngine that can each hear the |
43 world from their own perspective, it is necessary to go all the way | 49 world from their own perspective, or to make a single creature with |
44 back to =OpenAL= and implement support for simulated hearing there. | 50 many ears, it is necessary to go all the way back to =OpenAL= and |
51 implement support for simulated hearing there. | |
45 | 52 |
46 * Extending =OpenAL= | 53 * Extending =OpenAL= |
47 ** =OpenAL= Devices | 54 ** =OpenAL= Devices |
48 | 55 |
49 =OpenAL= goes to great lengths to support many different systems, all | 56 =OpenAL= goes to great lengths to support many different systems, all |
69 the only point in the sound rendering process where this data is | 76 the only point in the sound rendering process where this data is |
70 available. | 77 available. |
71 | 78 |
72 Therefore, in order to support multiple listeners, and get the sound | 79 Therefore, in order to support multiple listeners, and get the sound |
73 data in a form that the AIs can use, it is necessary to create a new | 80 data in a form that the AIs can use, it is necessary to create a new |
74 Device, which supports this features. | 81 Device which supports this features. |
75 | 82 |
76 ** The Send Device | 83 ** The Send Device |
77 Adding a device to OpenAL is rather tricky -- there are five separate | 84 Adding a device to OpenAL is rather tricky -- there are five separate |
78 files in the =OpenAL= source tree that must be modified to do so. I've | 85 files in the =OpenAL= source tree that must be modified to do so. I've |
79 documented this process [[./add-new-device.org][here]] for anyone who is interested. | 86 documented this process [[../../audio-send/html/add-new-device.html][here]] for anyone who is interested. |
80 | 87 |
81 | 88 Again, my objectives are: |
82 Onward to that actual Device! | |
83 | |
84 again, my objectives are: | |
85 | 89 |
86 - Support Multiple Listeners from jMonkeyEngine3 | 90 - Support Multiple Listeners from jMonkeyEngine3 |
87 - Get access to the rendered sound data for further processing from | 91 - Get access to the rendered sound data for further processing from |
88 clojure. | 92 clojure. |
93 | |
94 I named it the "Multiple Audio Send" Deives, or =Send= Device for | |
95 short, since it sends audio data back to the callig application like | |
96 an Aux-Send cable on a mixing board. | |
97 | |
98 Onward to the actual Device! | |
89 | 99 |
90 ** =send.c= | 100 ** =send.c= |
91 | 101 |
92 ** Header | 102 ** Header |
93 #+name: send-header | 103 #+name: send-header |
170 #+end_src | 180 #+end_src |
171 | 181 |
172 Switching between contexts is not the normal operation of a Device, | 182 Switching between contexts is not the normal operation of a Device, |
173 and one of the problems with doing so is that a Device normally keeps | 183 and one of the problems with doing so is that a Device normally keeps |
174 around a few pieces of state such as the =ClickRemoval= array above | 184 around a few pieces of state such as the =ClickRemoval= array above |
175 which will become corrupted if the contexts are not done in | 185 which will become corrupted if the contexts are not rendered in |
176 parallel. The solution is to create a copy of this normally global | 186 parallel. The solution is to create a copy of this normally global |
177 device state for each context, and copy it back and forth into and out | 187 device state for each context, and copy it back and forth into and out |
178 of the actual device state whenever a context is rendered. | 188 of the actual device state whenever a context is rendered. |
179 | 189 |
180 ** Synchronization Macros | 190 ** Synchronization Macros |
396 Device->Contexts = currentContext; | 406 Device->Contexts = currentContext; |
397 Device->NumContexts = currentNumContext; | 407 Device->NumContexts = currentNumContext; |
398 } | 408 } |
399 #+end_src | 409 #+end_src |
400 | 410 |
401 =OpenAL= normally renders all Contexts in parallel, outputting the | 411 =OpenAL= normally renders all contexts in parallel, outputting the |
402 whole result to the buffer. It does this by iterating over the | 412 whole result to the buffer. It does this by iterating over the |
403 Device->Contexts array and rendering each context to the buffer in | 413 Device->Contexts array and rendering each context to the buffer in |
404 turn. By temporally setting Device->NumContexts to 1 and adjusting | 414 turn. By temporally setting Device->NumContexts to 1 and adjusting |
405 the Device's context list to put the desired context-to-be-rendered | 415 the Device's context list to put the desired context-to-be-rendered |
406 into position 0, we can get trick =OpenAL= into rendering each slave | 416 into position 0, we can get trick =OpenAL= into rendering each context |
407 context separate from all the others. | 417 separate from all the others. |
408 | 418 |
409 ** Main Device Loop | 419 ** Main Device Loop |
410 #+name: main-loop | 420 #+name: main-loop |
411 #+begin_src C | 421 #+begin_src C |
412 //////////////////// Main Device Loop | 422 //////////////////// Main Device Loop |
416 */ | 426 */ |
417 static void init(ALCdevice *Device){ | 427 static void init(ALCdevice *Device){ |
418 ALCcontext *masterContext = alcGetCurrentContext(); | 428 ALCcontext *masterContext = alcGetCurrentContext(); |
419 addContext(Device, masterContext); | 429 addContext(Device, masterContext); |
420 } | 430 } |
421 | |
422 | 431 |
423 static void renderData(ALCdevice *Device, int samples){ | 432 static void renderData(ALCdevice *Device, int samples){ |
424 if(!Device->Connected){return;} | 433 if(!Device->Connected){return;} |
425 send_data *data = (send_data*)Device->ExtraData; | 434 send_data *data = (send_data*)Device->ExtraData; |
426 ALCcontext *current = alcGetCurrentContext(); | 435 ALCcontext *current = alcGetCurrentContext(); |
449 alcMakeContextCurrent(current); | 458 alcMakeContextCurrent(current); |
450 } | 459 } |
451 #+end_src | 460 #+end_src |
452 | 461 |
453 The main loop synchronizes the master LWJGL context with all the slave | 462 The main loop synchronizes the master LWJGL context with all the slave |
454 contexts, then walks each context, rendering just that context to it's | 463 contexts, then iterates through each context, rendering just that |
455 audio-sample storage buffer. | 464 context to it's audio-sample storage buffer. |
456 | 465 |
457 ** JNI Methods | 466 ** JNI Methods |
458 | 467 |
459 At this point, we have the ability to create multiple listeners by | 468 At this point, we have the ability to create multiple listeners by |
460 using the master/slave context trick, and the rendered audio data is | 469 using the master/slave context trick, and the rendered audio data is |
461 waiting patiently in internal buffers, one for each listener. We need | 470 waiting patiently in internal buffers, one for each listener. We need |
462 a way to transport this information to Java, and also a way to drive | 471 a way to transport this information to Java, and also a way to drive |
463 this device from Java. The following JNI interface code is inspired | 472 this device from Java. The following JNI interface code is inspired |
464 by the way LWJGL interfaces with =OpenAL=. | 473 by the LWJGL JNI interface to =OpenAL=. |
465 | 474 |
466 *** step | 475 *** Stepping the Device |
467 #+name: jni-step | 476 #+name: jni-step |
468 #+begin_src C | 477 #+begin_src C |
469 //////////////////// JNI Methods | 478 //////////////////// JNI Methods |
470 | 479 |
471 #include "com_aurellem_send_AudioSend.h" | 480 #include "com_aurellem_send_AudioSend.h" |
488 complicated AI whose mind takes 100 seconds of computer time to | 497 complicated AI whose mind takes 100 seconds of computer time to |
489 simulate 1 second of AI-time would miss almost all of the sound in | 498 simulate 1 second of AI-time would miss almost all of the sound in |
490 its environment. | 499 its environment. |
491 | 500 |
492 | 501 |
493 *** getSamples | 502 *** Device->Java Data Transport |
494 #+name: jni-get-samples | 503 #+name: jni-get-samples |
495 #+begin_src C | 504 #+begin_src C |
496 /* | 505 /* |
497 * Class: com_aurellem_send_AudioSend | 506 * Class: com_aurellem_send_AudioSend |
498 * Method: ngetSamples | 507 * Method: ngetSamples |
637 0); | 646 0); |
638 return format; | 647 return format; |
639 } | 648 } |
640 #+end_src | 649 #+end_src |
641 | 650 |
642 ** Boring Device management stuff | 651 ** Boring Device Management Stuff / Memory Cleanup |
643 This code is more-or-less copied verbatim from the other =OpenAL= | 652 This code is more-or-less copied verbatim from the other =OpenAL= |
644 backends. It's the basis for =OpenAL='s primitive object system. | 653 Devices. It's the basis for =OpenAL='s primitive object system. |
645 #+name: device-init | 654 #+name: device-init |
646 #+begin_src C | 655 #+begin_src C |
647 //////////////////// Device Initialization / Management | 656 //////////////////// Device Initialization / Management |
648 | 657 |
649 static const ALCchar sendDevice[] = "Multiple Audio Send"; | 658 static const ALCchar sendDevice[] = "Multiple Audio Send"; |
730 #+end_src | 739 #+end_src |
731 | 740 |
732 * The Java interface, =AudioSend= | 741 * The Java interface, =AudioSend= |
733 | 742 |
734 The Java interface to the Send Device follows naturally from the JNI | 743 The Java interface to the Send Device follows naturally from the JNI |
735 definitions. It is included here for completeness. The only thing here | 744 definitions. The only thing here of note is the =deviceID=. This is |
736 of note is the =deviceID=. This is available from LWJGL, but to only | 745 available from LWJGL, but to only way to get it is with reflection. |
737 way to get it is reflection. Unfortunately, there is no other way to | 746 Unfortunately, there is no other way to control the Send device than |
738 control the Send device than to obtain a pointer to it. | 747 to obtain a pointer to it. |
739 | 748 |
740 #+include: "../java/src/com/aurellem/send/AudioSend.java" src java :exports code | 749 #+include: "../../audio-send/java/src/com/aurellem/send/AudioSend.java" src java |
750 | |
751 * The Java Audio Renderer, =AudioSendRenderer= | |
752 | |
753 #+include: "../../jmeCapture/src/com/aurellem/capture/audio/AudioSendRenderer.java" src java | |
754 | |
755 The =AudioSendRenderer= is a modified version of the | |
756 =LwjglAudioRenderer= which implements the =MultiListener= interface to | |
757 provide access and creation of more than one =Listener= object. | |
758 | |
759 ** MultiListener.java | |
760 | |
761 #+include: "../../jmeCapture/src/com/aurellem/capture/audio/MultiListener.java" src java | |
762 | |
763 ** SoundProcessors are like SceneProcessors | |
764 | |
765 A =SoundProcessor= is analgous to a =SceneProcessor=. Every frame, the | |
766 =SoundProcessor= registered with a given =Listener= recieves the | |
767 rendered sound data and can do whatever processing it wants with it. | |
768 | |
769 #+include "../../jmeCapture/src/com/aurellem/capture/audio/SoundProcessor.java" src java | |
741 | 770 |
742 * Finally, Ears in clojure! | 771 * Finally, Ears in clojure! |
743 | 772 |
744 Now that the infrastructure is complete the clojure ear abstraction is | 773 Now that the =C= and =Java= infrastructure is complete, the clojure |
745 simple. Just as there were =SceneProcessors= for vision, there are | 774 hearing abstraction is simple and closely parallels the [[./vision.org][vision]] |
746 now =SoundProcessors= for hearing. | 775 abstraction. |
747 | 776 |
748 #+include "../../jmeCapture/src/com/aurellem/capture/audio/SoundProcessor.java" src java | 777 ** Hearing Pipeline |
749 | 778 |
750 | 779 All sound rendering is done in the CPU, so =(hearing-pipeline)= is |
780 much less complicated than =(vision-pipelie)= The bytes available in | |
781 the ByteBuffer obtained from the =send= Device have different meanings | |
782 dependant upon the particular hardware or your system. That is why | |
783 the =AudioFormat= object is necessary to provide the meaning that the | |
784 raw bytes lack. =(byteBuffer->pulse-vector)= uses the excellent | |
785 conversion facilities from [[http://www.tritonus.org/ ][tritonus]] ([[http://tritonus.sourceforge.net/apidoc/org/tritonus/share/sampled/FloatSampleTools.html#byte2floatInterleaved%2528byte%5B%5D,%2520int,%2520float%5B%5D,%2520int,%2520int,%2520javax.sound.sampled.AudioFormat%2529][javadoc]]) to generate a clojure vector of | |
786 floats which represent the linear PCM encoded waveform of the | |
787 sound. With linear PCM (pulse code modulation) -1.0 represents maximum | |
788 rarefaction of the air while 1.0 represents maximum compression of the | |
789 air at a given instant. | |
751 | 790 |
752 #+name: ears | 791 #+name: ears |
792 #+begin_src clojure | |
793 (in-ns 'cortex.hearing) | |
794 | |
795 (defn hearing-pipeline | |
796 "Creates a SoundProcessor which wraps a sound processing | |
797 continuation function. The continuation is a function that takes | |
798 [#^ByteBuffer b #^Integer int numSamples #^AudioFormat af ], each of which | |
799 has already been apprpiately sized." | |
800 [continuation] | |
801 (proxy [SoundProcessor] [] | |
802 (cleanup []) | |
803 (process | |
804 [#^ByteBuffer audioSamples numSamples #^AudioFormat audioFormat] | |
805 (continuation audioSamples numSamples audioFormat)))) | |
806 | |
807 (defn byteBuffer->pulse-vector | |
808 "Extract the sound samples from the byteBuffer as a PCM encoded | |
809 waveform with values ranging from -1.0 to 1.0 into a vector of | |
810 floats." | |
811 [#^ByteBuffer audioSamples numSamples #^AudioFormat audioFormat] | |
812 (let [num-floats (/ numSamples (.getFrameSize audioFormat)) | |
813 bytes (byte-array numSamples) | |
814 floats (float-array num-floats)] | |
815 (.get audioSamples bytes 0 numSamples) | |
816 (FloatSampleTools/byte2floatInterleaved | |
817 bytes 0 floats 0 num-floats audioFormat) | |
818 (vec floats))) | |
819 #+end_src | |
820 | |
821 ** Physical Ears | |
822 | |
823 Together, these three functions define how ears found in a specially | |
824 prepared blender file will be translated to =Listener= objects in a | |
825 simulation. =(ears)= extracts all the children of to top level node | |
826 named "ears". =(add-ear!)= and =(update-listener-velocity!)= use | |
827 =(bind-sense)= to bind a =Listener= object located at the initial | |
828 position of an "ear" node to the closest physical object in the | |
829 creature. That =Listener= will stay in the same orientation to the | |
830 object with which it is bound, just as the camera in the [[http://aurellem.localhost/cortex/html/sense.html#sec-4-1][sense binding | |
831 demonstration]]. =OpenAL= simulates the doppler effect for moving | |
832 listeners, =(update-listener-velocity!)= ensures that this velocity | |
833 information is always up-to-date. | |
834 | |
835 #+begin_src clojure | |
836 (defvar | |
837 ^{:arglists '([creature])} | |
838 ears | |
839 (sense-nodes "ears") | |
840 "Return the children of the creature's \"ears\" node.") | |
841 | |
842 (defn update-listener-velocity! | |
843 "Update the listener's velocity every update loop." | |
844 [#^Spatial obj #^Listener lis] | |
845 (let [old-position (atom (.getLocation lis))] | |
846 (.addControl | |
847 obj | |
848 (proxy [AbstractControl] [] | |
849 (controlUpdate [tpf] | |
850 (let [new-position (.getLocation lis)] | |
851 (.setVelocity | |
852 lis | |
853 (.mult (.subtract new-position @old-position) | |
854 (float (/ tpf)))) | |
855 (reset! old-position new-position))) | |
856 (controlRender [_ _]))))) | |
857 | |
858 (defn add-ear! | |
859 "Create a Listener centered on the current position of 'ear | |
860 which follows the closest physical node in 'creature and | |
861 sends sound data to 'continuation." | |
862 [#^Application world #^Node creature #^Spatial ear continuation] | |
863 (let [target (closest-node creature ear) | |
864 lis (Listener.) | |
865 audio-renderer (.getAudioRenderer world) | |
866 sp (hearing-pipeline continuation)] | |
867 (.setLocation lis (.getWorldTranslation ear)) | |
868 (.setRotation lis (.getWorldRotation ear)) | |
869 (bind-sense target lis) | |
870 (update-listener-velocity! target lis) | |
871 (.addListener audio-renderer lis) | |
872 (.registerSoundProcessor audio-renderer lis sp))) | |
873 #+end_src | |
874 | |
875 ** Ear Creation | |
876 | |
877 #+begin_src clojure | |
878 (defn hearing-kernel | |
879 "Returns a functon which returns auditory sensory data when called | |
880 inside a running simulation." | |
881 [#^Node creature #^Spatial ear] | |
882 (let [hearing-data (atom []) | |
883 register-listener! | |
884 (runonce | |
885 (fn [#^Application world] | |
886 (add-ear! | |
887 world creature ear | |
888 (comp #(reset! hearing-data %) | |
889 byteBuffer->pulse-vector))))] | |
890 (fn [#^Application world] | |
891 (register-listener! world) | |
892 (let [data @hearing-data | |
893 topology | |
894 (vec (map #(vector % 0) (range 0 (count data))))] | |
895 [topology data])))) | |
896 | |
897 (defn hearing! | |
898 "Endow the creature in a particular world with the sense of | |
899 hearing. Will return a sequence of functions, one for each ear, | |
900 which when called will return the auditory data from that ear." | |
901 [#^Node creature] | |
902 (for [ear (ears creature)] | |
903 (hearing-kernel creature ear))) | |
904 #+end_src | |
905 | |
906 Each function returned by =(hearing-kernel!)= will register a new | |
907 =Listener= with the simulation the first time it is called. Each time | |
908 it is called, the hearing-function will return a vector of linear PCM | |
909 encoded sound data that was heard since the last frame. The size of | |
910 this vector is of course determined by the overall framerate of the | |
911 game. With a constant framerate of 60 frames per second and a sampling | |
912 frequency of 44,100 samples per second, the vector will have exactly | |
913 735 elements. | |
914 | |
915 ** Visualizing Hearing | |
916 | |
917 This is a simple visualization function which displaye the waveform | |
918 reported by the simulated sense of hearing. It converts the values | |
919 reported in the vector returned by the hearing function from the range | |
920 [-1.0, 1.0] to the range [0 255], converts to integer, and displays | |
921 the number as a greyscale pixel. | |
922 | |
923 #+begin_src clojure | |
924 (defn view-hearing | |
925 "Creates a function which accepts a list of auditory data and | |
926 display each element of the list to the screen as an image." | |
927 [] | |
928 (view-sense | |
929 (fn [[coords sensor-data]] | |
930 (let [pixel-data | |
931 (vec | |
932 (map | |
933 #(rem (int (* 255 (/ (+ 1 %) 2))) 256) | |
934 sensor-data)) | |
935 height 50 | |
936 image (BufferedImage. (count coords) height | |
937 BufferedImage/TYPE_INT_RGB)] | |
938 (dorun | |
939 (for [x (range (count coords))] | |
940 (dorun | |
941 (for [y (range height)] | |
942 (let [raw-sensor (pixel-data x)] | |
943 (.setRGB image x y (gray raw-sensor))))))) | |
944 image)))) | |
945 #+end_src | |
946 | |
947 * Testing Hearing | |
948 | |
949 ** Advanced Java Example | |
950 | |
951 I wrote a test case in Java that demonstrates the use of the Java | |
952 components of this hearing system. It is part of a larger java library | |
953 to capture perfect Audio from jMonkeyEngine. Some of the clojure | |
954 constructs above are partially reiterated in the java source file. But | |
955 first, the video! As far as I know this is the first instance of | |
956 multiple simulated listeners in a virtual environment using OpenAL. | |
957 | |
958 #+begin_html | |
959 <div class="figure"> | |
960 <center> | |
961 <video controls="controls" width="500"> | |
962 <source src="../video/java-hearing-test.ogg" type="video/ogg" | |
963 preload="none" poster="../images/aurellem-1280x480.png" /> | |
964 </video> | |
965 </center> | |
966 <p>The blue ball is emitting a constant sound. Each blue box is | |
967 listening for sound, and will change color from blue to green if it | |
968 detects sound which is louder than a certain threshold. As the blue | |
969 sphere travels along the path, it excites each of the cubes in turn.</p> | |
970 </div> | |
971 | |
972 #+end_html | |
973 | |
974 #+include "../../jmeCapture/src/com/aurellem/capture/examples/Advanced.java" src java | |
975 | |
976 Here is a small clojure program to drive the java program and make it | |
977 available as part of my test suite. | |
978 | |
979 #+name: test-hearing | |
980 #+begin_src clojure | |
981 (in-ns 'cortex.test.hearing) | |
982 | |
983 (defn test-java-hearing | |
984 "Testing hearing: | |
985 You should see a blue sphere flying around several | |
986 cubes. As the sphere approaches each cube, it turns | |
987 green." | |
988 [] | |
989 (doto (com.aurellem.capture.examples.Advanced.) | |
990 (.setSettings | |
991 (doto (AppSettings. true) | |
992 (.setAudioRenderer "Send"))) | |
993 (.setShowSettings false) | |
994 (.setPauseOnLostFocus false))) | |
995 #+end_src | |
996 | |
997 ** Adding Hearing to the Worm | |
998 | |
999 | |
1000 | |
1001 * Headers | |
1002 | |
1003 #+name: hearing-header | |
753 #+begin_src clojure | 1004 #+begin_src clojure |
754 (ns cortex.hearing | 1005 (ns cortex.hearing |
755 "Simulate the sense of hearing in jMonkeyEngine3. Enables multiple | 1006 "Simulate the sense of hearing in jMonkeyEngine3. Enables multiple |
756 listeners at different positions in the same world. Automatically | 1007 listeners at different positions in the same world. Automatically |
757 reads ear-nodes from specially prepared blender files and | 1008 reads ear-nodes from specially prepared blender files and |
767 (:import javax.sound.sampled.AudioFormat) | 1018 (:import javax.sound.sampled.AudioFormat) |
768 (:import (com.jme3.scene Spatial Node)) | 1019 (:import (com.jme3.scene Spatial Node)) |
769 (:import com.jme3.audio.Listener) | 1020 (:import com.jme3.audio.Listener) |
770 (:import com.jme3.app.Application) | 1021 (:import com.jme3.app.Application) |
771 (:import com.jme3.scene.control.AbstractControl)) | 1022 (:import com.jme3.scene.control.AbstractControl)) |
772 | 1023 #+end_src |
773 (defn sound-processor | 1024 |
774 "Deals with converting ByteBuffers into Vectors of floats so that | 1025 #+begin_src clojure |
775 the continuation functions can be defined in terms of immutable | |
776 stuff." | |
777 [continuation] | |
778 (proxy [SoundProcessor] [] | |
779 (cleanup []) | |
780 (process | |
781 [#^ByteBuffer audioSamples numSamples #^AudioFormat audioFormat] | |
782 (let [bytes (byte-array numSamples) | |
783 num-floats (/ numSamples (.getFrameSize audioFormat)) | |
784 floats (float-array num-floats)] | |
785 (.get audioSamples bytes 0 numSamples) | |
786 (FloatSampleTools/byte2floatInterleaved | |
787 bytes 0 floats 0 num-floats audioFormat) | |
788 (continuation | |
789 (vec floats)))))) | |
790 | |
791 (defvar | |
792 ^{:arglists '([creature])} | |
793 ears | |
794 (sense-nodes "ears") | |
795 "Return the children of the creature's \"ears\" node.") | |
796 | |
797 (defn update-listener-velocity! | |
798 "Update the listener's velocity every update loop." | |
799 [#^Spatial obj #^Listener lis] | |
800 (let [old-position (atom (.getLocation lis))] | |
801 (.addControl | |
802 obj | |
803 (proxy [AbstractControl] [] | |
804 (controlUpdate [tpf] | |
805 (let [new-position (.getLocation lis)] | |
806 (.setVelocity | |
807 lis | |
808 (.mult (.subtract new-position @old-position) | |
809 (float (/ tpf)))) | |
810 (reset! old-position new-position))) | |
811 (controlRender [_ _]))))) | |
812 | |
813 (defn add-ear! | |
814 "Create a Listener centered on the current position of 'ear | |
815 which follows the closest physical node in 'creature and | |
816 sends sound data to 'continuation." | |
817 [#^Application world #^Node creature #^Spatial ear continuation] | |
818 (let [target (closest-node creature ear) | |
819 lis (Listener.) | |
820 audio-renderer (.getAudioRenderer world) | |
821 sp (sound-processor continuation)] | |
822 (.setLocation lis (.getWorldTranslation ear)) | |
823 (.setRotation lis (.getWorldRotation ear)) | |
824 (bind-sense target lis) | |
825 (update-listener-velocity! target lis) | |
826 (.addListener audio-renderer lis) | |
827 (.registerSoundProcessor audio-renderer lis sp))) | |
828 | |
829 (defn hearing-fn | |
830 "Returns a functon which returns auditory sensory data when called | |
831 inside a running simulation." | |
832 [#^Node creature #^Spatial ear] | |
833 (let [hearing-data (atom []) | |
834 register-listener! | |
835 (runonce | |
836 (fn [#^Application world] | |
837 (add-ear! | |
838 world creature ear | |
839 (fn [data] | |
840 (reset! hearing-data (vec data))))))] | |
841 (fn [#^Application world] | |
842 (register-listener! world) | |
843 (let [data @hearing-data | |
844 topology | |
845 (vec (map #(vector % 0) (range 0 (count data)))) | |
846 scaled-data | |
847 (vec | |
848 (map | |
849 #(rem (int (* 255 (/ (+ 1 %) 2))) 256) | |
850 data))] | |
851 [topology scaled-data])))) | |
852 | |
853 (defn hearing! | |
854 "Endow the creature in a particular world with the sense of | |
855 hearing. Will return a sequence of functions, one for each ear, | |
856 which when called will return the auditory data from that ear." | |
857 [#^Node creature] | |
858 (for [ear (ears creature)] | |
859 (hearing-fn creature ear))) | |
860 | |
861 (defn view-hearing | |
862 "Creates a function which accepts a list of auditory data and | |
863 display each element of the list to the screen as an image." | |
864 [] | |
865 (view-sense | |
866 (fn [[coords sensor-data]] | |
867 (let [height 50 | |
868 image (BufferedImage. (count coords) height | |
869 BufferedImage/TYPE_INT_RGB)] | |
870 (dorun | |
871 (for [x (range (count coords))] | |
872 (dorun | |
873 (for [y (range height)] | |
874 (let [raw-sensor (sensor-data x)] | |
875 (.setRGB image x y (gray raw-sensor))))))) | |
876 image)))) | |
877 | |
878 #+end_src | |
879 | |
880 #+results: ears | |
881 : #'cortex.hearing/hearing! | |
882 | |
883 * Example | |
884 | |
885 #+name: test-hearing | |
886 #+begin_src clojure :results silent | |
887 (ns cortex.test.hearing | 1026 (ns cortex.test.hearing |
888 (:use (cortex world util hearing)) | 1027 (:use (cortex world util hearing)) |
889 (:import (com.jme3.audio AudioNode Listener)) | 1028 (:import (com.jme3.audio AudioNode Listener)) |
890 (:import com.jme3.scene.Node | 1029 (:import com.jme3.scene.Node |
891 com.jme3.system.AppSettings)) | 1030 com.jme3.system.AppSettings)) |
892 | 1031 #+end_src |
893 (defn setup-fn [world] | 1032 |
894 (let [listener (Listener.)] | 1033 |
895 (add-ear world listener #(println-repl (nth % 0))))) | 1034 * Next |
896 | |
897 (defn play-sound [node world value] | |
898 (if (not value) | |
899 (do | |
900 (.playSource (.getAudioRenderer world) node)))) | |
901 | |
902 (defn test-basic-hearing [] | |
903 (let [node1 (AudioNode. (asset-manager) "Sounds/pure.wav" false false)] | |
904 (world | |
905 (Node.) | |
906 {"key-space" (partial play-sound node1)} | |
907 setup-fn | |
908 no-op))) | |
909 | |
910 (defn test-advanced-hearing | |
911 "Testing hearing: | |
912 You should see a blue sphere flying around several | |
913 cubes. As the sphere approaches each cube, it turns | |
914 green." | |
915 [] | |
916 (doto (com.aurellem.capture.examples.Advanced.) | |
917 (.setSettings | |
918 (doto (AppSettings. true) | |
919 (.setAudioRenderer "Send"))) | |
920 (.setShowSettings false) | |
921 (.setPauseOnLostFocus false))) | |
922 | |
923 #+end_src | |
924 | |
925 This extremely basic program prints out the first sample it encounters | |
926 at every time stamp. You can see the rendered sound being printed at | |
927 the REPL. | |
928 | |
929 - As a bonus, this method of capturing audio for AI can also be used | 1035 - As a bonus, this method of capturing audio for AI can also be used |
930 to capture perfect audio from a jMonkeyEngine application, for use | 1036 to capture perfect audio from a jMonkeyEngine application, for use |
931 in demos and the like. | 1037 in demos and the like. |
932 | 1038 |
933 | 1039 |
1040 | |
934 * COMMENT Code Generation | 1041 * COMMENT Code Generation |
935 | 1042 |
936 #+begin_src clojure :tangle ../src/cortex/hearing.clj | 1043 #+begin_src clojure :tangle ../src/cortex/hearing.clj |
1044 <<hearing-header>> | |
937 <<ears>> | 1045 <<ears>> |
938 #+end_src | 1046 #+end_src |
939 | 1047 |
940 #+begin_src clojure :tangle ../src/cortex/test/hearing.clj | 1048 #+begin_src clojure :tangle ../src/cortex/test/hearing.clj |
941 <<test-hearing>> | 1049 <<test-hearing>> |