Mercurial > audio-send
comparison org/ear.org @ 19:22ac5a0367cd
finally, a first pass at ear.org
author | Robert McIntyre <rlm@mit.edu> |
---|---|
date | Thu, 03 Nov 2011 14:54:45 -0700 |
parents | 1e201037f666 |
children | e8ae40c9848c |
comparison
equal
deleted
inserted
replaced
18:1e201037f666 | 19:22ac5a0367cd |
---|---|
44 | 44 |
45 To make many AI creatures in jMonkeyEngine that can each hear the | 45 To make many AI creatures in jMonkeyEngine that can each hear the |
46 world from their own perspective, it is necessary to go all the way | 46 world from their own perspective, it is necessary to go all the way |
47 back to =OpenAL= and implement support for simulated hearing there. | 47 back to =OpenAL= and implement support for simulated hearing there. |
48 | 48 |
49 * Extending =OpenAL= | |
49 ** =OpenAL= Devices | 50 ** =OpenAL= Devices |
50 | 51 |
51 =OpenAL= goes to great lengths to support many different systems, all | 52 =OpenAL= goes to great lengths to support many different systems, all |
52 with different sound capabilities and interfaces. It acomplishes this | 53 with different sound capabilities and interfaces. It acomplishes this |
53 difficult task by providing code for many different sound backends in | 54 difficult task by providing code for many different sound backends in |
72 available. | 73 available. |
73 | 74 |
74 Therefore, in order to support multiple listeners, and get the sound | 75 Therefore, in order to support multiple listeners, and get the sound |
75 data in a form that the AIs can use, it is necessary to create a new | 76 data in a form that the AIs can use, it is necessary to create a new |
76 Device, which supports this features. | 77 Device, which supports this features. |
77 | |
78 | 78 |
79 ** The Send Device | 79 ** The Send Device |
80 Adding a device to OpenAL is rather tricky -- there are five separate | 80 Adding a device to OpenAL is rather tricky -- there are five separate |
81 files in the =OpenAL= source tree that must be modified to do so. I've | 81 files in the =OpenAL= source tree that must be modified to do so. I've |
82 documented this process [[./add-new-device.org][here]] for anyone who is interested. | 82 documented this process [[./add-new-device.org][here]] for anyone who is interested. |
329 Most of the hard work in Context Synchronization is done in | 329 Most of the hard work in Context Synchronization is done in |
330 =syncSources()=. The only thing that =syncContexts()= has to worry | 330 =syncSources()=. The only thing that =syncContexts()= has to worry |
331 about is automoatically creating new sources whenever a slave context | 331 about is automoatically creating new sources whenever a slave context |
332 does not have the same number of sources as the master context. | 332 does not have the same number of sources as the master context. |
333 | 333 |
334 * Context Creation | 334 ** Context Creation |
335 #+begin_src C | 335 #+begin_src C |
336 static void addContext(ALCdevice *Device, ALCcontext *context){ | 336 static void addContext(ALCdevice *Device, ALCcontext *context){ |
337 send_data *data = (send_data*)Device->ExtraData; | 337 send_data *data = (send_data*)Device->ExtraData; |
338 // expand array if necessary | 338 // expand array if necessary |
339 if (data->numContexts >= data->maxContexts){ | 339 if (data->numContexts >= data->maxContexts){ |
357 Here, the slave context is created, and it's data is stored in the | 357 Here, the slave context is created, and it's data is stored in the |
358 device-wide =ExtraData= structure. The =renderBuffer= that is created | 358 device-wide =ExtraData= structure. The =renderBuffer= that is created |
359 here is where the rendered sound samples for this slave context will | 359 here is where the rendered sound samples for this slave context will |
360 eventually go. | 360 eventually go. |
361 | 361 |
362 * Context Switching | 362 ** Context Switching |
363 #+begin_src C | 363 #+begin_src C |
364 //////////////////// Context Switching | 364 //////////////////// Context Switching |
365 | 365 |
366 /* A device brings along with it two pieces of state | 366 /* A device brings along with it two pieces of state |
367 * which have to be swapped in and out with each context. | 367 * which have to be swapped in and out with each context. |
449 | 449 |
450 The main loop synchronizes the master LWJGL context with all the slave | 450 The main loop synchronizes the master LWJGL context with all the slave |
451 contexts, then walks each context, rendering just that context to it's | 451 contexts, then walks each context, rendering just that context to it's |
452 audio-sample storage buffer. | 452 audio-sample storage buffer. |
453 | 453 |
454 * JNI Methods | 454 ** JNI Methods |
455 | |
456 At this point, we have the ability to create multiple listeners by | |
457 using the master/slave context trick, and the rendered audio data is | |
458 waiting patiently in internal buffers, one for each listener. We need | |
459 a way to transport this information to Java, and also a way to drive | |
460 this device from Java. The following JNI interface code is inspired | |
461 by the way LWJGL interfaces with =OpenAL=. | |
462 | |
463 *** step | |
464 | |
455 #+begin_src C | 465 #+begin_src C |
456 //////////////////// JNI Methods | 466 //////////////////// JNI Methods |
457 | 467 |
458 #include "com_aurellem_send_AudioSend.h" | 468 #include "com_aurellem_send_AudioSend.h" |
459 | 469 |
465 JNIEXPORT void JNICALL Java_com_aurellem_send_AudioSend_nstep | 475 JNIEXPORT void JNICALL Java_com_aurellem_send_AudioSend_nstep |
466 (JNIEnv *env, jclass clazz, jlong device, jint samples){ | 476 (JNIEnv *env, jclass clazz, jlong device, jint samples){ |
467 UNUSED(env);UNUSED(clazz);UNUSED(device); | 477 UNUSED(env);UNUSED(clazz);UNUSED(device); |
468 renderData((ALCdevice*)((intptr_t)device), samples); | 478 renderData((ALCdevice*)((intptr_t)device), samples); |
469 } | 479 } |
470 | 480 #+end_src |
481 This device, unlike most of the other devices in =OpenAL=, does not | |
482 render sound unless asked. This enables the system to slow down or | |
483 speed up depending on the needs of the AIs who are using it to | |
484 listen. If the device tried to render samples in real-time, a | |
485 complicated AI whose mind takes 100 seconds of computer time to | |
486 simulate 1 second of AI-time would miss almost all of the sound in | |
487 its environment. | |
488 | |
489 | |
490 *** getSamples | |
491 #+begin_src C | |
471 /* | 492 /* |
472 * Class: com_aurellem_send_AudioSend | 493 * Class: com_aurellem_send_AudioSend |
473 * Method: ngetSamples | 494 * Method: ngetSamples |
474 * Signature: (JLjava/nio/ByteBuffer;III)V | 495 * Signature: (JLjava/nio/ByteBuffer;III)V |
475 */ | 496 */ |
484 send_data *data = (send_data*)recorder->ExtraData; | 505 send_data *data = (send_data*)recorder->ExtraData; |
485 if ((ALuint)n > data->numContexts){return;} | 506 if ((ALuint)n > data->numContexts){return;} |
486 memcpy(buffer_address, data->contexts[n]->renderBuffer, | 507 memcpy(buffer_address, data->contexts[n]->renderBuffer, |
487 BytesFromDevFmt(recorder->FmtType) * recorder->NumChan * samples); | 508 BytesFromDevFmt(recorder->FmtType) * recorder->NumChan * samples); |
488 } | 509 } |
489 | 510 #+end_src |
511 | |
512 This is the transport layer between C and Java that will eventually | |
513 allow us to access rendered sound data from clojure. | |
514 | |
515 *** Listener Management | |
516 | |
517 =addListener=, =setNthListenerf=, and =setNthListener3f= are | |
518 necessary to change the properties of any listener other than the | |
519 master one, since only the listener of the current active context is | |
520 affected by the normal =OpenAL= listener calls. | |
521 | |
522 #+begin_src C | |
490 /* | 523 /* |
491 * Class: com_aurellem_send_AudioSend | 524 * Class: com_aurellem_send_AudioSend |
492 * Method: naddListener | 525 * Method: naddListener |
493 * Signature: (J)V | 526 * Signature: (J)V |
494 */ | 527 */ |
539 if ((ALuint)contextNum > data->numContexts){return;} | 572 if ((ALuint)contextNum > data->numContexts){return;} |
540 alcMakeContextCurrent(data->contexts[contextNum]->ctx); | 573 alcMakeContextCurrent(data->contexts[contextNum]->ctx); |
541 alListenerf(param, v1); | 574 alListenerf(param, v1); |
542 alcMakeContextCurrent(current); | 575 alcMakeContextCurrent(current); |
543 } | 576 } |
544 | 577 #+end_src |
578 | |
579 *** Initilazation | |
580 =initDevice= is called from the Java side after LWJGL has created its | |
581 context, and before any calls to =addListener=. It establishes the | |
582 LWJGL context as the master context. | |
583 | |
584 =getAudioFormat= is a convienence function that uses JNI to build up a | |
585 =javax.sound.sampled.AudioFormat= object from data in the Device. This | |
586 way, there is no ambiguity about what the bits created by =step= and | |
587 returned by =getSamples= mean. | |
588 | |
589 #+begin_src C | |
545 /* | 590 /* |
546 * Class: com_aurellem_send_AudioSend | 591 * Class: com_aurellem_send_AudioSend |
547 * Method: ninitDevice | 592 * Method: ninitDevice |
548 * Signature: (J)V | 593 * Signature: (J)V |
549 */ | 594 */ |
550 JNIEXPORT void JNICALL Java_com_aurellem_send_AudioSend_ninitDevice | 595 JNIEXPORT void JNICALL Java_com_aurellem_send_AudioSend_ninitDevice |
551 (JNIEnv *env, jclass clazz, jlong device){ | 596 (JNIEnv *env, jclass clazz, jlong device){ |
552 UNUSED(env);UNUSED(clazz); | 597 UNUSED(env);UNUSED(clazz); |
553 | |
554 ALCdevice *Device = (ALCdevice*) ((intptr_t)device); | 598 ALCdevice *Device = (ALCdevice*) ((intptr_t)device); |
555 init(Device); | 599 init(Device); |
556 | 600 } |
557 } | |
558 | |
559 | 601 |
560 /* | 602 /* |
561 * Class: com_aurellem_send_AudioSend | 603 * Class: com_aurellem_send_AudioSend |
562 * Method: ngetAudioFormat | 604 * Method: ngetAudioFormat |
563 * Signature: (J)Ljavax/sound/sampled/AudioFormat; | 605 * Signature: (J)Ljavax/sound/sampled/AudioFormat; |
569 (*env)->FindClass(env, "javax/sound/sampled/AudioFormat"); | 611 (*env)->FindClass(env, "javax/sound/sampled/AudioFormat"); |
570 jmethodID AudioFormatConstructor = | 612 jmethodID AudioFormatConstructor = |
571 (*env)->GetMethodID(env, AudioFormatClass, "<init>", "(FIIZZ)V"); | 613 (*env)->GetMethodID(env, AudioFormatClass, "<init>", "(FIIZZ)V"); |
572 | 614 |
573 ALCdevice *Device = (ALCdevice*) ((intptr_t)device); | 615 ALCdevice *Device = (ALCdevice*) ((intptr_t)device); |
574 | |
575 //float frequency | |
576 | |
577 int isSigned; | 616 int isSigned; |
578 switch (Device->FmtType) | 617 switch (Device->FmtType) |
579 { | 618 { |
580 case DevFmtUByte: | 619 case DevFmtUByte: |
581 case DevFmtUShort: isSigned = 0; break; | 620 case DevFmtUShort: isSigned = 0; break; |
582 default : isSigned = 1; | 621 default : isSigned = 1; |
583 } | 622 } |
584 float frequency = Device->Frequency; | 623 float frequency = Device->Frequency; |
585 int bitsPerFrame = (8 * BytesFromDevFmt(Device->FmtType)); | 624 int bitsPerFrame = (8 * BytesFromDevFmt(Device->FmtType)); |
586 int channels = Device->NumChan; | 625 int channels = Device->NumChan; |
587 | |
588 | |
589 //printf("freq = %f, bpf = %d, channels = %d, signed? = %d\n", | |
590 // frequency, bitsPerFrame, channels, isSigned); | |
591 | |
592 jobject format = (*env)-> | 626 jobject format = (*env)-> |
593 NewObject( | 627 NewObject( |
594 env,AudioFormatClass,AudioFormatConstructor, | 628 env,AudioFormatClass,AudioFormatConstructor, |
595 frequency, | 629 frequency, |
596 bitsPerFrame, | 630 bitsPerFrame, |
597 channels, | 631 channels, |
598 isSigned, | 632 isSigned, |
599 0); | 633 0); |
600 return format; | 634 return format; |
601 } | 635 } |
602 | 636 #+end_src |
637 | |
638 *** Boring Device management stuff | |
639 This code is more-or-less copied verbatim from the other =OpenAL= | |
640 backends. It's the basis for =OpenAL='s primitive object system. | |
641 | |
642 #+begin_src C | |
603 //////////////////// Device Initilization / Management | 643 //////////////////// Device Initilization / Management |
604 | 644 |
605 static const ALCchar sendDevice[] = "Multiple Audio Send"; | 645 static const ALCchar sendDevice[] = "Multiple Audio Send"; |
606 | 646 |
607 static ALCboolean send_open_playback(ALCdevice *device, | 647 static ALCboolean send_open_playback(ALCdevice *device, |
683 break; | 723 break; |
684 } | 724 } |
685 } | 725 } |
686 #+end_src | 726 #+end_src |
687 | 727 |
688 | 728 * The Java interface, =AudioSend= |
689 | 729 |
690 | 730 The Java interface to the Send Device follows naturally from the JNI |
691 | 731 definitions. It is included here for completeness. The only thing here |
692 | 732 of note is the =deviceID=. This is available from LWJGL, but to only |
693 | 733 way to get it is reflection. Unfornatuently, there is no other way to |
734 control the Send device than to obtain a pointer to it. | |
735 | |
736 #+include: "../java/src/com/aurellem/send/AudioSend.java" src java :exports code | |
737 | |
738 * Finally, Ears in clojure! | |
739 | |
740 Now that the infastructure is complete (modulo a few patches to | |
741 jMonkeyEngine3 to support accessing this modified version of =OpenAL= | |
742 that are not worth discussing), the clojure ear abstraction is rather | |
743 simple. Just as there were =SceneProcessors= for vision, there are | |
744 now =SoundProcessors= for hearing. | |
745 | |
746 #+include "../../jmeCapture/src/com/aurellem/capture/audio/SoundProcessor.java" src java | |
694 | 747 |
695 #+srcname: ears | 748 #+srcname: ears |
696 #+begin_src clojure | 749 #+begin_src clojure |
697 (ns cortex.hearing) | 750 (ns cortex.hearing |
698 (use 'cortex.world) | 751 "Simulate the sense of hearing in jMonkeyEngine3. Enables multiple |
699 (use 'cortex.import) | 752 listeners at different positions in the same world. Passes vectors |
700 (use 'clojure.contrib.def) | 753 of floats in the range [-1.0 -- 1.0] in PCM format to any arbitray |
701 (cortex.import/mega-import-jme3) | 754 function." |
702 (rlm.rlm-commands/help) | 755 {:author "Robert McIntyre"} |
703 (import java.nio.ByteBuffer) | 756 (:use (cortex world util)) |
704 (import java.awt.image.BufferedImage) | 757 (:import java.nio.ByteBuffer) |
705 (import java.awt.Color) | 758 (:import org.tritonus.share.sampled.FloatSampleTools) |
706 (import java.awt.Dimension) | 759 (:import com.aurellem.capture.audio.SoundProcessor) |
707 (import java.awt.Graphics) | 760 (:import javax.sound.sampled.AudioFormat)) |
708 (import java.awt.Graphics2D) | 761 |
709 (import java.awt.event.WindowAdapter) | |
710 (import java.awt.event.WindowEvent) | |
711 (import java.awt.image.BufferedImage) | |
712 (import java.nio.ByteBuffer) | |
713 (import javax.swing.JFrame) | |
714 (import javax.swing.JPanel) | |
715 (import javax.swing.SwingUtilities) | |
716 (import javax.swing.ImageIcon) | |
717 (import javax.swing.JOptionPane) | |
718 (import java.awt.image.ImageObserver) | |
719 | |
720 (import 'com.jme3.capture.SoundProcessor) | |
721 | |
722 | |
723 (defn sound-processor | 762 (defn sound-processor |
724 "deals with converting ByteBuffers into Arrays of bytes so that the | 763 "Deals with converting ByteBuffers into Vectors of floats so that |
725 continuation functions can be defined in terms of immutable stuff." | 764 the continuation functions can be defined in terms of immutable |
765 stuff." | |
726 [continuation] | 766 [continuation] |
727 (proxy [SoundProcessor] [] | 767 (proxy [SoundProcessor] [] |
728 (cleanup []) | 768 (cleanup []) |
729 (process | 769 (process |
730 [#^ByteBuffer audioSamples numSamples] | 770 [#^ByteBuffer audioSamples numSamples #^AudioFormat audioFormat] |
731 (no-exceptions | 771 (let [bytes (byte-array numSamples) |
732 (let [byte-array (byte-array numSamples)] | 772 floats (float-array numSamples)] |
733 (.get audioSamples byte-array 0 numSamples) | 773 (.get audioSamples bytes 0 numSamples) |
734 (continuation | 774 (FloatSampleTools/byte2floatInterleaved |
735 (vec byte-array))))))) | 775 bytes 0 floats 0 |
776 (/ numSamples (.getFrameSize audioFormat)) audioFormat) | |
777 (continuation | |
778 (vec floats)))))) | |
736 | 779 |
737 (defn add-ear | 780 (defn add-ear |
738 "add an ear to the world. The continuation function will be called | 781 "Add an ear to the world. The continuation function will be called |
739 on the FFT or the sounds which the ear hears in the given | 782 on the FFT or the sounds which the ear hears in the given |
740 timeframe. Sound is 3D." | 783 timeframe. Sound is 3D." |
741 [world listener continuation] | 784 [world listener continuation] |
742 (let [renderer (.getAudioRenderer world)] | 785 (let [renderer (.getAudioRenderer world)] |
743 (.addListener renderer listener) | 786 (.addListener renderer listener) |
744 (.registerSoundProcessor renderer listener | 787 (.registerSoundProcessor renderer listener |
745 (sound-processor continuation)) | 788 (sound-processor continuation)) |
746 listener)) | 789 listener)) |
747 | 790 #+end_src |
748 #+end_src | 791 |
749 | 792 * Example |
750 | |
751 | 793 |
752 #+srcname: test-hearing | 794 #+srcname: test-hearing |
753 #+begin_src clojure :results silent | 795 #+begin_src clojure :results silent |
754 (ns test.hearing) | 796 (ns test.hearing |
755 (use 'cortex.world) | 797 (:use (cortex world util hearing)) |
756 (use 'cortex.import) | 798 (:import (com.jme3.audio AudioNode Listener)) |
757 (use 'clojure.contrib.def) | 799 (:import com.jme3.scene.Node)) |
758 (use 'body.ear) | |
759 (cortex.import/mega-import-jme3) | |
760 (rlm.rlm-commands/help) | |
761 | 800 |
762 (defn setup-fn [world] | 801 (defn setup-fn [world] |
763 (let [listener (Listener.)] | 802 (let [listener (Listener.)] |
764 (add-ear world listener #(println (nth % 0))))) | 803 (add-ear world listener #(println-repl (nth % 0))))) |
765 | 804 |
766 (defn play-sound [node world value] | 805 (defn play-sound [node world value] |
767 (if (not value) | 806 (if (not value) |
768 (do | 807 (do |
769 (.playSource (.getAudioRenderer world) node)))) | 808 (.playSource (.getAudioRenderer world) node)))) |
770 | 809 |
771 (defn test-world [] | 810 (defn test-basic-hearing [] |
772 (let [node1 (AudioNode. (asset-manager) "Sounds/pure.wav" false false)] | 811 (.start |
773 (world | 812 (let [node1 (AudioNode. (asset-manager) "Sounds/pure.wav" false false)] |
774 (Node.) | 813 (world |
775 {"key-space" (partial play-sound node1)} | 814 (Node.) |
776 setup-fn | 815 {"key-space" (partial play-sound node1)} |
777 no-op | 816 setup-fn |
778 ))) | 817 no-op)))) |
779 | 818 #+end_src |
780 | 819 |
781 #+end_src | 820 This extremely basic program prints out the first sample it encounters |
782 | 821 at every time stamp. You can see the rendered sound begin printed at |
783 | 822 the REPL. |
784 | |
785 * Example | |
786 | 823 |
787 * COMMENT Code Generation | 824 * COMMENT Code Generation |
788 | 825 |
789 #+begin_src clojure :tangle ../../cortex/src/cortex/hearing.clj | 826 #+begin_src clojure :tangle ../../cortex/src/cortex/hearing.clj |
790 <<ears>> | 827 <<ears>> |
796 | 833 |
797 | 834 |
798 #+begin_src C :tangle ../Alc/backends/send.c | 835 #+begin_src C :tangle ../Alc/backends/send.c |
799 <<send>> | 836 <<send>> |
800 #+end_src | 837 #+end_src |
838 | |
839 |