mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 05:33:00 +00:00 
			
		
		
		
	Merge branch 'mc-1.17.x' into mc-1.18.x
This commit is contained in:
		
							
								
								
									
										14
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								build.gradle
									
									
									
									
									
								
							| @@ -6,6 +6,7 @@ buildscript { | |||||||
|     } |     } | ||||||
|     dependencies { |     dependencies { | ||||||
|         classpath 'net.minecraftforge.gradle:ForgeGradle:5.1.+' |         classpath 'net.minecraftforge.gradle:ForgeGradle:5.1.+' | ||||||
|  |         classpath "org.spongepowered:mixingradle:0.7.+" | ||||||
|         classpath 'org.parchmentmc:librarian:1.+' |         classpath 'org.parchmentmc:librarian:1.+' | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -22,6 +23,7 @@ plugins { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| apply plugin: 'net.minecraftforge.gradle' | apply plugin: 'net.minecraftforge.gradle' | ||||||
|  | apply plugin: "org.spongepowered.mixin" | ||||||
| apply plugin: 'org.parchmentmc.librarian.forgegradle' | apply plugin: 'org.parchmentmc.librarian.forgegradle' | ||||||
| 
 | 
 | ||||||
| version = mod_version | version = mod_version | ||||||
| @@ -72,6 +74,8 @@ minecraft { | |||||||
|                     source sourceSets.main |                     source sourceSets.main | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|  |             arg "-mixin.config=computercraft.mixins.json" | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         client { |         client { | ||||||
| @@ -129,6 +133,11 @@ minecraft { | |||||||
|     accessTransformer file('src/main/resources/META-INF/accesstransformer.cfg') |     accessTransformer file('src/main/resources/META-INF/accesstransformer.cfg') | ||||||
|     accessTransformer file('src/testMod/resources/META-INF/accesstransformer.cfg') |     accessTransformer file('src/testMod/resources/META-INF/accesstransformer.cfg') | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | mixin { | ||||||
|  |     add sourceSets.main, 'computercraft.mixins.refmap.json' | ||||||
|  | } | ||||||
|  | 
 | ||||||
| repositories { | repositories { | ||||||
|     mavenCentral() |     mavenCentral() | ||||||
|     maven { |     maven { | ||||||
| @@ -152,6 +161,7 @@ dependencies { | |||||||
|     checkstyle "com.puppycrawl.tools:checkstyle:8.45" |     checkstyle "com.puppycrawl.tools:checkstyle:8.45" | ||||||
| 
 | 
 | ||||||
|     minecraft "net.minecraftforge:forge:${mc_version}-${forge_version}" |     minecraft "net.minecraftforge:forge:${mc_version}-${forge_version}" | ||||||
|  |     annotationProcessor 'org.spongepowered:mixin:0.8.4:processor' | ||||||
| 
 | 
 | ||||||
|     // compileOnly fg.deobf("mezz.jei:jei-1.17.1:8.0.0.14:api") |     // compileOnly fg.deobf("mezz.jei:jei-1.17.1:8.0.0.14:api") | ||||||
|     // runtimeOnly fg.deobf("mezz.jei:jei-1.17.1:8.0.0.14") |     // runtimeOnly fg.deobf("mezz.jei:jei-1.17.1:8.0.0.14") | ||||||
| @@ -170,7 +180,7 @@ dependencies { | |||||||
|         exclude group: "org.jetbrains", module: "annotations" |         exclude group: "org.jetbrains", module: "annotations" | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     cctJavadoc 'cc.tweaked:cct-javadoc:1.4.2' |     cctJavadoc 'cc.tweaked:cct-javadoc:1.4.4' | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Compile tasks | // Compile tasks | ||||||
| @@ -210,6 +220,8 @@ jar { | |||||||
|             "Implementation-Version"  : "${mod_version}", |             "Implementation-Version"  : "${mod_version}", | ||||||
|             "Implementation-Vendor"   : "SquidDev", |             "Implementation-Vendor"   : "SquidDev", | ||||||
|             "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ") |             "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ") | ||||||
|  |         , | ||||||
|  |             "MixinConfigs"            : "computercraft.mixins.json", | ||||||
|         ]) |         ]) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								doc/events/speaker_audio_empty.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								doc/events/speaker_audio_empty.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | --- | ||||||
|  | module: [kind=event] speaker_audio_empty | ||||||
|  | see: speaker.playAudio To play audio using the speaker | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ## Return Values | ||||||
|  | 1. @{string}: The event name. | ||||||
|  | 2. @{string}: The name of the speaker which is available to play more audio. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Example | ||||||
|  | This uses @{io.lines} to read audio data in blocks of 16KiB from "example_song.dfpwm", and then attempts to play it | ||||||
|  | using @{speaker.playAudio}. If the speaker's buffer is full, it waits for an event and tries again. | ||||||
|  | 
 | ||||||
|  | ```lua | ||||||
|  | local dfpwm = require("cc.audio.dfpwm") | ||||||
|  | local speaker = peripheral.find("speaker") | ||||||
|  | 
 | ||||||
|  | local decoder = dfpwm.make_decoder() | ||||||
|  | for chunk in io.lines("data/example.dfpwm", 16 * 1024) do | ||||||
|  |     local buffer = decoder(chunk) | ||||||
|  | 
 | ||||||
|  |     while not speaker.playAudio(buffer) do | ||||||
|  |         os.pullEvent("speaker_audio_empty") | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | ``` | ||||||
							
								
								
									
										200
									
								
								doc/guides/speaker_audio.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								doc/guides/speaker_audio.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,200 @@ | |||||||
|  | --- | ||||||
|  | module: [kind=guide] speaker_audio | ||||||
|  | see: speaker.playAudio Play PCM audio using a speaker. | ||||||
|  | see: cc.audio.dfpwm Provides utilities for encoding and decoding DFPWM files. | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | # Playing audio with speakers | ||||||
|  | CC: Tweaked's speaker peripheral provides a powerful way to play any audio you like with the @{speaker.playAudio} | ||||||
|  | method. However, for people unfamiliar with digital audio, it's not the most intuitive thing to use. This guide provides | ||||||
|  | an introduction to digital audio, demonstrates how to play music with CC: Tweaked's speakers, and then briefly discusses | ||||||
|  | the more complex topic of audio processing. | ||||||
|  | 
 | ||||||
|  | ## A short introduction to digital audio | ||||||
|  | When sound is recorded it is captured as an analogue signal, effectively the electrical version of a sound | ||||||
|  | wave. However, this signal is continuous, and so can't be used directly by a computer. Instead, we measure (or *sample*) | ||||||
|  | the amplitude of the wave many times a second and then *quantise* that amplitude, rounding it to the nearest | ||||||
|  | representable value. | ||||||
|  | 
 | ||||||
|  | This representation of sound - a long, uniformally sampled list of amplitudes is referred to as [Pulse-code | ||||||
|  | Modulation][PCM] (PCM). PCM can be thought of as the "standard" audio format, as it's incredibly easy to work with. For | ||||||
|  | instance, to mix two pieces of audio together, you can just samples from the two tracks together and take the average. | ||||||
|  | 
 | ||||||
|  | CC: Tweaked's speakers also work with PCM audio. It plays back 48,000 samples a second, where each sample is an integer | ||||||
|  | between -128 and 127. This is more commonly referred to as 48kHz and an 8-bit resolution. | ||||||
|  | 
 | ||||||
|  | Let's now look at a quick example. We're going to generate a [Sine Wave] at 220Hz, which sounds like a low monotonous | ||||||
|  | hum. First we wrap our speaker peripheral, and then we fill a table (also referred to as a *buffer*) with 128×1024 | ||||||
|  | samples - this is the maximum number of samples a speaker can accept in one go. | ||||||
|  | 
 | ||||||
|  | In order to fill this buffer, we need to do a little maths. We want to play 220 sine waves each second, where each sine | ||||||
|  | wave completes a full oscillation in 2π "units". This means one seconds worth of audio is 2×π×220 "units" long. We then | ||||||
|  | need to split this into 48k samples, basically meaning for each sample we move 2×π×220/48k "along" the sine curve. | ||||||
|  | 
 | ||||||
|  | ```lua {data-peripheral=speaker} | ||||||
|  | local speaker = peripheral.find("speaker") | ||||||
|  | 
 | ||||||
|  | local buffer = {} | ||||||
|  | local t, dt = 0, 2 * math.pi * 220 / 48000 | ||||||
|  | for i = 1, 128 * 1024 do | ||||||
|  |     buffer[i] = math.floor(math.sin(t) * 127) | ||||||
|  |     t = (t + dt) % (math.pi * 2) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | speaker.playAudio(buffer) | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Streaming audio | ||||||
|  | You might notice that the above snippet only generates a short bit of audio - 2.7s seconds to be precise. While we could | ||||||
|  | try increasing the number of loop iterations, we'll get an error when we try to play it through the speaker: the sound | ||||||
|  | buffer is too large for it to handle. | ||||||
|  | 
 | ||||||
|  | Our 2.7 seconds of audio is stored in a table with over 130 _thousand_ elements. If we wanted to play a full minute of | ||||||
|  | sine waves (and why wouldn't you?), you'd need a table with almost 3 _million_. Suddenly you find these numbers adding | ||||||
|  | up very quickly, and these tables take up more and more memory. | ||||||
|  | 
 | ||||||
|  | Instead of building our entire song (well, sine wave) in one go, we can produce it in small batches, each of which get | ||||||
|  | passed off to @{speaker.playAudio} when the time is right. This allows us to build a _stream_ of audio, where we read | ||||||
|  | chunks of audio one at a time (either from a file or a tone generator like above), do some optional processing to each | ||||||
|  | one, and then play them. | ||||||
|  | 
 | ||||||
|  | Let's adapt our example from above to do that instead. | ||||||
|  | 
 | ||||||
|  | ```lua {data-peripheral=speaker} | ||||||
|  | local speaker = peripheral.find("speaker") | ||||||
|  | 
 | ||||||
|  | local t, dt = 0, 2 * math.pi * 220 / 48000 | ||||||
|  | while true do | ||||||
|  |     local buffer = {} | ||||||
|  |     for i = 1, 16 * 1024 * 8 do | ||||||
|  |         buffer[i] = math.floor(math.sin(t) * 127) | ||||||
|  |         t = (t + dt) % (math.pi * 2) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     while not speaker.playAudio(buffer) do | ||||||
|  |         os.pullEvent("speaker_audio_empty") | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | It looks pretty similar to before, aside from we've wrapped the generation and playing code in a while loop, and added a | ||||||
|  | rather odd loop with @{speaker.playAudio} and @{os.pullEvent}. | ||||||
|  | 
 | ||||||
|  | Let's talk about this loop, why do we need to keep calling @{speaker.playAudio}? Remember that what we're trying to do | ||||||
|  | here is avoid keeping too much audio in memory at once. However, if we're generating audio quicker than the speakers can | ||||||
|  | play it, we're not helping at all - all this audio is still hanging around waiting to be played! | ||||||
|  | 
 | ||||||
|  | In order to avoid this, the speaker rejects any new chunks of audio if its backlog is too large. When this happens, | ||||||
|  | @{speaker.playAudio} returns false. Once enough audio has played, and the backlog has been reduced, a | ||||||
|  | @{speaker_audio_empty} event is queued, and we can try to play our chunk once more. | ||||||
|  | 
 | ||||||
|  | ## Storing audio | ||||||
|  | PCM is a fantastic way of representing audio when we want to manipulate it, but it's not very efficient when we want to | ||||||
|  | store it to disk. Compare the size of a WAV file (which uses PCM) to an equivalent MP3, it's often 5 times the size. | ||||||
|  | Instead, we store audio in special formats (or *codecs*) and then convert them to PCM when we need to do processing on | ||||||
|  | them. | ||||||
|  | 
 | ||||||
|  | Modern audio codecs use some incredibly impressive techniques to compress the audio as much as possible while preserving | ||||||
|  | sound quality. However, due to CC: Tweaked's limited processing power, it's not really possible to use these from your | ||||||
|  | computer. Instead, we need something much simpler. | ||||||
|  | 
 | ||||||
|  | DFPWM (Dynamic Filter Pulse Width Modulation) is the de facto standard audio format of the ComputerCraft (and | ||||||
|  | OpenComputers) world. Originally popularised by the addon mod [Computronics], CC:T now has built-in support for it with | ||||||
|  | the @{cc.audio.dfpwm} module. This allows you to read DFPWM files from disk, decode them to PCM, and then play them | ||||||
|  | using the speaker. | ||||||
|  | 
 | ||||||
|  | Let's dive in with an example, and we'll explain things afterwards: | ||||||
|  | 
 | ||||||
|  | ```lua {data-peripheral=speaker} | ||||||
|  | local dfpwm = require("cc.audio.dfpwm") | ||||||
|  | local speaker = peripheral.find("speaker") | ||||||
|  | 
 | ||||||
|  | local decoder = dfpwm.make_decoder() | ||||||
|  | for chunk in io.lines("data/example.dfpwm", 16 * 1024) do | ||||||
|  |     local buffer = decoder(chunk) | ||||||
|  | 
 | ||||||
|  |     while not speaker.playAudio(buffer) do | ||||||
|  |         os.pullEvent("speaker_audio_empty") | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Once again, we see the @{speaker.playAudio}/@{speaker_audio_empty} loop. However, the rest of the program is a little | ||||||
|  | different. | ||||||
|  | 
 | ||||||
|  | First, we require the dfpwm module and call @{cc.audio.dfpwm.make_decoder} to construct a new decoder. This decoder | ||||||
|  | accepts blocks of DFPWM data and converts it to a list of 8-bit amplitudes, which we can then play with our speaker. | ||||||
|  | 
 | ||||||
|  | As mentioned to above, @{speaker.playAudio} accepts at most 128×1024 samples in one go. DFPMW uses a single bit for each | ||||||
|  | sample, which means we want to process our audio in chunks of 16×1024 bytes (16KiB). In order to do this, we use | ||||||
|  | @{io.lines}, which provides a nice way to loop over chunks of a file. You can of course just use @{fs.open} and | ||||||
|  | @{fs.BinaryReadHandle.read} if you prefer. | ||||||
|  | 
 | ||||||
|  | ## Processing audio | ||||||
|  | As mentioned near the beginning of this guide, PCM audio is pretty easy to work with as it's just a list of amplitudes. | ||||||
|  | You can mix together samples from different streams by adding their amplitudes, change the rate of playback by removing | ||||||
|  | samples, etc... | ||||||
|  | 
 | ||||||
|  | Let's put together a small demonstration here. We're going to add a small delay effect to the song above, so that you | ||||||
|  | hear a faint echo about a second later. | ||||||
|  | 
 | ||||||
|  | In order to do this, we'll follow a format similar to the previous example, decoding the audio and then playing it. | ||||||
|  | However, we'll also add some new logic between those two steps, which loops over every sample in our chunk of audio, and | ||||||
|  | adds the sample from one second ago to it. | ||||||
|  | 
 | ||||||
|  | For this, we'll need to keep track of the last 48k samples - exactly one seconds worth of audio. We can do this using a | ||||||
|  | [Ring Buffer], which helps makes things a little more efficient. | ||||||
|  | 
 | ||||||
|  | ```lua | ||||||
|  | local dfpwm = require("cc.audio.dfpwm") | ||||||
|  | local speaker = peripheral.find("speaker") | ||||||
|  | 
 | ||||||
|  | -- Speakers play at 48kHz, so one second is 48k samples. We first fill our buffer | ||||||
|  | -- with 0s, as there's nothing to echo at the start of the track! | ||||||
|  | local samples_i, samples_n = 1, 48000 | ||||||
|  | local samples = {} | ||||||
|  | for i = 1, samples_n do samples[i] = 0 end | ||||||
|  | 
 | ||||||
|  | local decoder = dfpwm.make_decoder() | ||||||
|  | for chunk in io.lines("data/example.dfpwm", 16 * 1024) do | ||||||
|  |     local buffer = decoder(input) | ||||||
|  | 
 | ||||||
|  |     for i = 1, #buffer do | ||||||
|  |         local original_value = buffer[i] | ||||||
|  | 
 | ||||||
|  |         -- Replace this sample with its current amplitude plus the amplitude from one second ago. | ||||||
|  |         -- We scale both to ensure the resulting value is still between -128 and 127. | ||||||
|  |         buffer[i] = original_value * 0.7 + samples[samples_i] * 0.3 | ||||||
|  | 
 | ||||||
|  |         -- Now store the current sample, and move the "head" of our ring buffer forward one place. | ||||||
|  |         samples[samples_i] = original_value | ||||||
|  |         samples_i = samples_i + 1 | ||||||
|  |         if samples_i > samples_n then samples_i = 1 end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     while not speaker.playAudio(buffer) do | ||||||
|  |         os.pullEvent("speaker_audio_empty") | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | :::note Confused? | ||||||
|  | Don't worry if you don't understand this example. It's quite advanced, and does use some ideas that this guide doesn't | ||||||
|  | cover. That said, don't be afraid to ask on [Discord] or [IRC] either! | ||||||
|  | ::: | ||||||
|  | 
 | ||||||
|  | It's worth noting that the examples of audio processing we've mentioned here are about manipulating the _amplitude_ of | ||||||
|  | the wave. If you wanted to modify the _frequency_ (for instance, shifting the pitch), things get rather more complex. | ||||||
|  | For this, you'd need to use the [Fast Fourier transform][FFT] to convert the stream of amplitudes to frequencies, | ||||||
|  | process those, and then convert them back to amplitudes. | ||||||
|  | 
 | ||||||
|  | This is, I'm afraid, left as an exercise to the reader. | ||||||
|  | 
 | ||||||
|  | [Computronics]: https://github.com/Vexatos/Computronics/ "Computronics on GitHub" | ||||||
|  | [FFT]: https://en.wikipedia.org/wiki/Fast_Fourier_transform "Fast Fourier transform - Wikipedia" | ||||||
|  | [PCM]: https://en.wikipedia.org/wiki/Pulse-code_modulation "Pulse-code Modulation - Wikipedia" | ||||||
|  | [Ring Buffer]: https://en.wikipedia.org/wiki/Circular_buffer "Circular buffer - Wikipedia" | ||||||
|  | [Sine Wave]: https://en.wikipedia.org/wiki/Sine_wave "Sine wave - Wikipedia" | ||||||
|  | 
 | ||||||
|  | [Discord]: https://discord.computercraft.cc "The Minecraft Computer Mods Discord" | ||||||
|  | [IRC]: http://webchat.esper.net/?channels=computercraft "IRC webchat on EsperNet" | ||||||
| @@ -1,8 +1,9 @@ | |||||||
| ; -*- mode: Lisp;-*- | ; -*- mode: Lisp;-*- | ||||||
|  |  | ||||||
| (sources | (sources | ||||||
|   /doc/stub/ |  | ||||||
|   /doc/events/ |   /doc/events/ | ||||||
|  |   /doc/guides/ | ||||||
|  |   /doc/stub/ | ||||||
|   /build/docs/luaJavadoc/ |   /build/docs/luaJavadoc/ | ||||||
|   /src/main/resources/*/computercraft/lua/bios.lua |   /src/main/resources/*/computercraft/lua/bios.lua | ||||||
|   /src/main/resources/*/computercraft/lua/rom/ |   /src/main/resources/*/computercraft/lua/rom/ | ||||||
| @@ -27,7 +28,8 @@ | |||||||
|   (module-kinds |   (module-kinds | ||||||
|     (peripheral Peripherals) |     (peripheral Peripherals) | ||||||
|     (generic_peripheral "Generic Peripherals") |     (generic_peripheral "Generic Peripherals") | ||||||
|     (event Events)) |     (event Events) | ||||||
|  |     (guide Guides)) | ||||||
|  |  | ||||||
|   (library-path |   (library-path | ||||||
|     /doc/stub/ |     /doc/stub/ | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ public interface GenericPeripheral extends GenericSource | |||||||
|      * Get the type of the exposed peripheral. |      * Get the type of the exposed peripheral. | ||||||
|      * |      * | ||||||
|      * Unlike normal {@link IPeripheral}s, {@link GenericPeripheral} do not have to have a type. By default, the |      * Unlike normal {@link IPeripheral}s, {@link GenericPeripheral} do not have to have a type. By default, the | ||||||
|      * resulting peripheral uses the resource name of the wrapped {@link BlockEntity} (for instance {@literal minecraft:chest}). |      * resulting peripheral uses the resource name of the wrapped {@link BlockEntity} (for instance {@code minecraft:chest}). | ||||||
|      * |      * | ||||||
|      * However, in some cases it may be more appropriate to specify a more readable name. Overriding this method allows |      * However, in some cases it may be more appropriate to specify a more readable name. Overriding this method allows | ||||||
|      * you to do so. |      * you to do so. | ||||||
|   | |||||||
| @@ -63,7 +63,7 @@ public final class PeripheralType | |||||||
|      * Create a new non-empty peripheral type with additional traits. |      * Create a new non-empty peripheral type with additional traits. | ||||||
|      * |      * | ||||||
|      * @param type            The name of the type. |      * @param type            The name of the type. | ||||||
|      * @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@literal "inventory"}. |      * @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@code "inventory"}. | ||||||
|      * @return The constructed peripheral type. |      * @return The constructed peripheral type. | ||||||
|      */ |      */ | ||||||
|     public static PeripheralType ofType( @Nonnull String type, Collection<String> additionalTypes ) |     public static PeripheralType ofType( @Nonnull String type, Collection<String> additionalTypes ) | ||||||
| @@ -76,7 +76,7 @@ public final class PeripheralType | |||||||
|      * Create a new non-empty peripheral type with additional traits. |      * Create a new non-empty peripheral type with additional traits. | ||||||
|      * |      * | ||||||
|      * @param type            The name of the type. |      * @param type            The name of the type. | ||||||
|      * @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@literal "inventory"}. |      * @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@code "inventory"}. | ||||||
|      * @return The constructed peripheral type. |      * @return The constructed peripheral type. | ||||||
|      */ |      */ | ||||||
|     public static PeripheralType ofType( @Nonnull String type, @Nonnull String... additionalTypes ) |     public static PeripheralType ofType( @Nonnull String type, @Nonnull String... additionalTypes ) | ||||||
| @@ -88,7 +88,7 @@ public final class PeripheralType | |||||||
|     /** |     /** | ||||||
|      * Create a new peripheral type with no primary type but additional traits. |      * Create a new peripheral type with no primary type but additional traits. | ||||||
|      * |      * | ||||||
|      * @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@literal "inventory"}. |      * @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@code "inventory"}. | ||||||
|      * @return The constructed peripheral type. |      * @return The constructed peripheral type. | ||||||
|      */ |      */ | ||||||
|     public static PeripheralType ofAdditional( Collection<String> additionalTypes ) |     public static PeripheralType ofAdditional( Collection<String> additionalTypes ) | ||||||
| @@ -99,7 +99,7 @@ public final class PeripheralType | |||||||
|     /** |     /** | ||||||
|      * Create a new peripheral type with no primary type but additional traits. |      * Create a new peripheral type with no primary type but additional traits. | ||||||
|      * |      * | ||||||
|      * @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@literal "inventory"}. |      * @param additionalTypes Additional types, or "traits" of this peripheral. For instance, {@code "inventory"}. | ||||||
|      * @return The constructed peripheral type. |      * @return The constructed peripheral type. | ||||||
|      */ |      */ | ||||||
|     public static PeripheralType ofAdditional( @Nonnull String... additionalTypes ) |     public static PeripheralType ofAdditional( @Nonnull String... additionalTypes ) | ||||||
| @@ -108,7 +108,7 @@ public final class PeripheralType | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Get the name of this peripheral type. This may be {@literal null}. |      * Get the name of this peripheral type. This may be {@code null}. | ||||||
|      * |      * | ||||||
|      * @return The type of this peripheral. |      * @return The type of this peripheral. | ||||||
|      */ |      */ | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ | |||||||
| package dan200.computercraft.client; | package dan200.computercraft.client; | ||||||
| 
 | 
 | ||||||
| import dan200.computercraft.ComputerCraft; | import dan200.computercraft.ComputerCraft; | ||||||
|  | import dan200.computercraft.client.sound.SpeakerManager; | ||||||
| import dan200.computercraft.shared.peripheral.monitor.ClientMonitor; | import dan200.computercraft.shared.peripheral.monitor.ClientMonitor; | ||||||
| import net.minecraftforge.api.distmarker.Dist; | import net.minecraftforge.api.distmarker.Dist; | ||||||
| import net.minecraftforge.client.event.ClientPlayerNetworkEvent; | import net.minecraftforge.client.event.ClientPlayerNetworkEvent; | ||||||
| @@ -22,7 +23,7 @@ public class ClientHooks | |||||||
|         if( event.getWorld().isClientSide() ) |         if( event.getWorld().isClientSide() ) | ||||||
|         { |         { | ||||||
|             ClientMonitor.destroyAll(); |             ClientMonitor.destroyAll(); | ||||||
|             SoundManager.reset(); |             SpeakerManager.reset(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|   | |||||||
| @@ -1,84 +0,0 @@ | |||||||
| /* |  | ||||||
|  * This file is part of ComputerCraft - http://www.computercraft.info |  | ||||||
|  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. |  | ||||||
|  * Send enquiries to dratcliffe@gmail.com |  | ||||||
|  */ |  | ||||||
| package dan200.computercraft.client; |  | ||||||
| 
 |  | ||||||
| import net.minecraft.client.Minecraft; |  | ||||||
| import net.minecraft.client.resources.sounds.AbstractSoundInstance; |  | ||||||
| import net.minecraft.client.resources.sounds.SoundInstance; |  | ||||||
| import net.minecraft.client.resources.sounds.TickableSoundInstance; |  | ||||||
| import net.minecraft.resources.ResourceLocation; |  | ||||||
| import net.minecraft.sounds.SoundSource; |  | ||||||
| import net.minecraft.world.phys.Vec3; |  | ||||||
| 
 |  | ||||||
| import java.util.HashMap; |  | ||||||
| import java.util.Map; |  | ||||||
| import java.util.UUID; |  | ||||||
| 
 |  | ||||||
| public class SoundManager |  | ||||||
| { |  | ||||||
|     private static final Map<UUID, MoveableSound> sounds = new HashMap<>(); |  | ||||||
| 
 |  | ||||||
|     public static void playSound( UUID source, Vec3 position, ResourceLocation event, float volume, float pitch ) |  | ||||||
|     { |  | ||||||
|         var soundManager = Minecraft.getInstance().getSoundManager(); |  | ||||||
| 
 |  | ||||||
|         MoveableSound oldSound = sounds.get( source ); |  | ||||||
|         if( oldSound != null ) soundManager.stop( oldSound ); |  | ||||||
| 
 |  | ||||||
|         MoveableSound newSound = new MoveableSound( event, position, volume, pitch ); |  | ||||||
|         sounds.put( source, newSound ); |  | ||||||
|         soundManager.play( newSound ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static void stopSound( UUID source ) |  | ||||||
|     { |  | ||||||
|         SoundInstance sound = sounds.remove( source ); |  | ||||||
|         if( sound == null ) return; |  | ||||||
| 
 |  | ||||||
|         Minecraft.getInstance().getSoundManager().stop( sound ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static void moveSound( UUID source, Vec3 position ) |  | ||||||
|     { |  | ||||||
|         MoveableSound sound = sounds.get( source ); |  | ||||||
|         if( sound != null ) sound.setPosition( position ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static void reset() |  | ||||||
|     { |  | ||||||
|         sounds.clear(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static class MoveableSound extends AbstractSoundInstance implements TickableSoundInstance |  | ||||||
|     { |  | ||||||
|         protected MoveableSound( ResourceLocation sound, Vec3 position, float volume, float pitch ) |  | ||||||
|         { |  | ||||||
|             super( sound, SoundSource.RECORDS ); |  | ||||||
|             setPosition( position ); |  | ||||||
|             this.volume = volume; |  | ||||||
|             this.pitch = pitch; |  | ||||||
|             attenuation = Attenuation.LINEAR; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         void setPosition( Vec3 position ) |  | ||||||
|         { |  | ||||||
|             x = (float) position.x(); |  | ||||||
|             y = (float) position.y(); |  | ||||||
|             z = (float) position.z(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         public boolean isStopped() |  | ||||||
|         { |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         public void tick() |  | ||||||
|         { |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										125
									
								
								src/main/java/dan200/computercraft/client/sound/DfpwmStream.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/main/java/dan200/computercraft/client/sound/DfpwmStream.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | |||||||
|  | /* | ||||||
|  |  * This file is part of ComputerCraft - http://www.computercraft.info | ||||||
|  |  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||||
|  |  * Send enquiries to dratcliffe@gmail.com | ||||||
|  |  */ | ||||||
|  | package dan200.computercraft.client.sound; | ||||||
|  | 
 | ||||||
|  | import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral; | ||||||
|  | import io.netty.buffer.ByteBuf; | ||||||
|  | import net.minecraft.client.sounds.AudioStream; | ||||||
|  | import org.lwjgl.BufferUtils; | ||||||
|  | 
 | ||||||
|  | import javax.annotation.Nonnull; | ||||||
|  | import javax.sound.sampled.AudioFormat; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.nio.ByteBuffer; | ||||||
|  | import java.nio.ByteOrder; | ||||||
|  | import java.util.ArrayDeque; | ||||||
|  | import java.util.Queue; | ||||||
|  | 
 | ||||||
|  | class DfpwmStream implements AudioStream | ||||||
|  | { | ||||||
|  |     public static final int SAMPLE_RATE = SpeakerPeripheral.SAMPLE_RATE; | ||||||
|  | 
 | ||||||
|  |     private static final int PREC = 10; | ||||||
|  |     private static final int LPF_STRENGTH = 140; | ||||||
|  | 
 | ||||||
|  |     private static final AudioFormat MONO_16 = new AudioFormat( SAMPLE_RATE, 16, 1, true, false ); | ||||||
|  | 
 | ||||||
|  |     private final Queue<ByteBuffer> buffers = new ArrayDeque<>( 2 ); | ||||||
|  | 
 | ||||||
|  |     private int charge = 0; // q | ||||||
|  |     private int strength = 0; // s | ||||||
|  |     private int lowPassCharge; | ||||||
|  |     private boolean previousBit = false; | ||||||
|  | 
 | ||||||
|  |     DfpwmStream() | ||||||
|  |     { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     void push( @Nonnull ByteBuf input ) | ||||||
|  |     { | ||||||
|  |         int readable = input.readableBytes(); | ||||||
|  |         ByteBuffer output = ByteBuffer.allocate( readable * 16 ).order( ByteOrder.nativeOrder() ); | ||||||
|  | 
 | ||||||
|  |         for( int i = 0; i < readable; i++ ) | ||||||
|  |         { | ||||||
|  |             byte inputByte = input.readByte(); | ||||||
|  |             for( int j = 0; j < 8; j++ ) | ||||||
|  |             { | ||||||
|  |                 boolean currentBit = (inputByte & 1) != 0; | ||||||
|  |                 int target = currentBit ? 127 : -128; | ||||||
|  | 
 | ||||||
|  |                 // q' <- q + (s * (t - q) + 128)/256 | ||||||
|  |                 int nextCharge = charge + ((strength * (target - charge) + (1 << (PREC - 1))) >> PREC); | ||||||
|  |                 if( nextCharge == charge && nextCharge != target ) nextCharge += currentBit ? 1 : -1; | ||||||
|  | 
 | ||||||
|  |                 int z = currentBit == previousBit ? (1 << PREC) - 1 : 0; | ||||||
|  | 
 | ||||||
|  |                 int nextStrength = strength; | ||||||
|  |                 if( strength != z ) nextStrength += currentBit == previousBit ? 1 : -1; | ||||||
|  |                 if( nextStrength < 2 << (PREC - 8) ) nextStrength = 2 << (PREC - 8); | ||||||
|  | 
 | ||||||
|  |                 // Apply antijerk | ||||||
|  |                 int chargeWithAntijerk = currentBit == previousBit | ||||||
|  |                     ? nextCharge | ||||||
|  |                     : nextCharge + charge + 1 >> 1; | ||||||
|  | 
 | ||||||
|  |                 // And low pass filter: outQ <- outQ + ((expectedOutput - outQ) x 140 / 256) | ||||||
|  |                 lowPassCharge += ((chargeWithAntijerk - lowPassCharge) * LPF_STRENGTH + 0x80) >> 8; | ||||||
|  | 
 | ||||||
|  |                 charge = nextCharge; | ||||||
|  |                 strength = nextStrength; | ||||||
|  |                 previousBit = currentBit; | ||||||
|  | 
 | ||||||
|  |                 // Ideally we'd generate an 8-bit audio buffer. However, as we're piggybacking on top of another | ||||||
|  |                 // audio stream (which uses 16 bit audio), we need to keep in the same format. | ||||||
|  |                 output.putShort( (short) ((byte) (lowPassCharge & 0xFF) << 8) ); | ||||||
|  | 
 | ||||||
|  |                 inputByte >>= 1; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         output.flip(); | ||||||
|  |         synchronized( this ) | ||||||
|  |         { | ||||||
|  |             buffers.add( output ); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nonnull | ||||||
|  |     @Override | ||||||
|  |     public AudioFormat getFormat() | ||||||
|  |     { | ||||||
|  |         return MONO_16; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nonnull | ||||||
|  |     @Override | ||||||
|  |     public synchronized ByteBuffer read( int capacity ) | ||||||
|  |     { | ||||||
|  |         ByteBuffer result = BufferUtils.createByteBuffer( capacity ); | ||||||
|  |         while( result.hasRemaining() ) | ||||||
|  |         { | ||||||
|  |             ByteBuffer head = buffers.peek(); | ||||||
|  |             if( head == null ) break; | ||||||
|  | 
 | ||||||
|  |             int toRead = Math.min( head.remaining(), result.remaining() ); | ||||||
|  |             result.put( head.array(), head.position(), toRead ); // TODO: In 1.17 convert this to a ByteBuffer override | ||||||
|  |             head.position( head.position() + toRead ); | ||||||
|  | 
 | ||||||
|  |             if( head.hasRemaining() ) break; | ||||||
|  |             buffers.remove(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         result.flip(); | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void close() throws IOException | ||||||
|  |     { | ||||||
|  |         buffers.clear(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,80 @@ | |||||||
|  | /* | ||||||
|  |  * This file is part of ComputerCraft - http://www.computercraft.info | ||||||
|  |  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||||
|  |  * Send enquiries to dratcliffe@gmail.com | ||||||
|  |  */ | ||||||
|  | package dan200.computercraft.client.sound; | ||||||
|  | 
 | ||||||
|  | import dan200.computercraft.ComputerCraft; | ||||||
|  | import io.netty.buffer.ByteBuf; | ||||||
|  | import net.minecraft.client.Minecraft; | ||||||
|  | import net.minecraft.resources.ResourceLocation; | ||||||
|  | import net.minecraft.world.phys.Vec3; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * An instance of a speaker, which is either playing a {@link DfpwmStream} stream or a normal sound. | ||||||
|  |  */ | ||||||
|  | public class SpeakerInstance | ||||||
|  | { | ||||||
|  |     public static final ResourceLocation DFPWM_STREAM = new ResourceLocation( ComputerCraft.MOD_ID, "speaker.dfpwm_fake_audio_should_not_be_played" ); | ||||||
|  | 
 | ||||||
|  |     private DfpwmStream currentStream; | ||||||
|  |     private SpeakerSound sound; | ||||||
|  | 
 | ||||||
|  |     SpeakerInstance() | ||||||
|  |     { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public synchronized void pushAudio( ByteBuf buffer ) | ||||||
|  |     { | ||||||
|  |         if( currentStream == null ) currentStream = new DfpwmStream(); | ||||||
|  |         currentStream.push( buffer ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void playAudio( Vec3 position, float volume ) | ||||||
|  |     { | ||||||
|  |         var soundManager = Minecraft.getInstance().getSoundManager(); | ||||||
|  | 
 | ||||||
|  |         if( sound != null && sound.stream != currentStream ) | ||||||
|  |         { | ||||||
|  |             soundManager.stop( sound ); | ||||||
|  |             sound = null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if( sound != null && !soundManager.isActive( sound ) ) sound = null; | ||||||
|  | 
 | ||||||
|  |         if( sound == null && currentStream != null ) | ||||||
|  |         { | ||||||
|  |             sound = new SpeakerSound( DFPWM_STREAM, currentStream, position, volume, 1.0f ); | ||||||
|  |             soundManager.play( sound ); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void playSound( Vec3 position, ResourceLocation location, float volume, float pitch ) | ||||||
|  |     { | ||||||
|  |         var soundManager = Minecraft.getInstance().getSoundManager(); | ||||||
|  |         currentStream = null; | ||||||
|  | 
 | ||||||
|  |         if( sound != null ) | ||||||
|  |         { | ||||||
|  |             soundManager.stop( sound ); | ||||||
|  |             sound = null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         sound = new SpeakerSound( location, null, position, volume, pitch ); | ||||||
|  |         soundManager.play( sound ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     void setPosition( Vec3 position ) | ||||||
|  |     { | ||||||
|  |         if( sound != null ) sound.setPosition( position ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     void stop() | ||||||
|  |     { | ||||||
|  |         if( sound != null ) Minecraft.getInstance().getSoundManager().stop( sound ); | ||||||
|  | 
 | ||||||
|  |         currentStream = null; | ||||||
|  |         sound = null; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,58 @@ | |||||||
|  | /* | ||||||
|  |  * This file is part of ComputerCraft - http://www.computercraft.info | ||||||
|  |  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||||
|  |  * Send enquiries to dratcliffe@gmail.com | ||||||
|  |  */ | ||||||
|  | package dan200.computercraft.client.sound; | ||||||
|  | 
 | ||||||
|  | import net.minecraft.world.phys.Vec3; | ||||||
|  | import net.minecraftforge.api.distmarker.Dist; | ||||||
|  | import net.minecraftforge.client.event.sound.PlayStreamingSourceEvent; | ||||||
|  | import net.minecraftforge.eventbus.api.SubscribeEvent; | ||||||
|  | import net.minecraftforge.fml.common.Mod; | ||||||
|  | 
 | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.UUID; | ||||||
|  | import java.util.concurrent.ConcurrentHashMap; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Maps speakers source IDs to a {@link SpeakerInstance}. | ||||||
|  |  */ | ||||||
|  | @Mod.EventBusSubscriber( Dist.CLIENT ) | ||||||
|  | public class SpeakerManager | ||||||
|  | { | ||||||
|  |     private static final Map<UUID, SpeakerInstance> sounds = new ConcurrentHashMap<>(); | ||||||
|  | 
 | ||||||
|  |     @SubscribeEvent | ||||||
|  |     public static void playStreaming( PlayStreamingSourceEvent event ) | ||||||
|  |     { | ||||||
|  |         if( !(event.getSound() instanceof SpeakerSound) ) return; | ||||||
|  |         SpeakerSound sound = (SpeakerSound) event.getSound(); | ||||||
|  |         if( sound.stream == null ) return; | ||||||
|  | 
 | ||||||
|  |         event.getChannel().attachBufferStream( sound.stream ); | ||||||
|  |         event.getChannel().play(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static SpeakerInstance getSound( UUID source ) | ||||||
|  |     { | ||||||
|  |         return sounds.computeIfAbsent( source, x -> new SpeakerInstance() ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void stopSound( UUID source ) | ||||||
|  |     { | ||||||
|  |         SpeakerInstance sound = sounds.remove( source ); | ||||||
|  |         if( sound != null ) sound.stop(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void moveSound( UUID source, Vec3 position ) | ||||||
|  |     { | ||||||
|  |         SpeakerInstance sound = sounds.get( source ); | ||||||
|  |         if( sound != null ) sound.setPosition( position ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void reset() | ||||||
|  |     { | ||||||
|  |         sounds.clear(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,54 @@ | |||||||
|  | /* | ||||||
|  |  * This file is part of ComputerCraft - http://www.computercraft.info | ||||||
|  |  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||||
|  |  * Send enquiries to dratcliffe@gmail.com | ||||||
|  |  */ | ||||||
|  | package dan200.computercraft.client.sound; | ||||||
|  | 
 | ||||||
|  | import net.minecraft.client.resources.sounds.AbstractSoundInstance; | ||||||
|  | import net.minecraft.client.resources.sounds.TickableSoundInstance; | ||||||
|  | import net.minecraft.client.sounds.AudioStream; | ||||||
|  | import net.minecraft.resources.ResourceLocation; | ||||||
|  | import net.minecraft.sounds.SoundSource; | ||||||
|  | import net.minecraft.world.phys.Vec3; | ||||||
|  | 
 | ||||||
|  | import javax.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | public class SpeakerSound extends AbstractSoundInstance implements TickableSoundInstance | ||||||
|  | { | ||||||
|  |     DfpwmStream stream; | ||||||
|  | 
 | ||||||
|  |     SpeakerSound( ResourceLocation sound, DfpwmStream stream, Vec3 position, float volume, float pitch ) | ||||||
|  |     { | ||||||
|  |         super( sound, SoundSource.RECORDS ); | ||||||
|  |         setPosition( position ); | ||||||
|  |         this.stream = stream; | ||||||
|  |         this.volume = volume; | ||||||
|  |         this.pitch = pitch; | ||||||
|  |         attenuation = Attenuation.LINEAR; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     void setPosition( Vec3 position ) | ||||||
|  |     { | ||||||
|  |         x = (float) position.x(); | ||||||
|  |         y = (float) position.y(); | ||||||
|  |         z = (float) position.z(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean isStopped() | ||||||
|  |     { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void tick() | ||||||
|  |     { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     public AudioStream getStream() | ||||||
|  |     { | ||||||
|  |         return stream; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -15,7 +15,7 @@ import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketCl | |||||||
| import static io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker.MAX_WINDOW_SIZE; | import static io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker.MAX_WINDOW_SIZE; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * An alternative to {@link WebSocketClientCompressionHandler} which supports the {@literal client_no_context_takeover} |  * An alternative to {@link WebSocketClientCompressionHandler} which supports the {@code client_no_context_takeover} | ||||||
|  * extension. Makes CC <em>slightly</em> more flexible. |  * extension. Makes CC <em>slightly</em> more flexible. | ||||||
|  */ |  */ | ||||||
| @ChannelHandler.Sharable | @ChannelHandler.Sharable | ||||||
|   | |||||||
| @@ -14,8 +14,8 @@ import dan200.computercraft.core.computer.TimeoutState; | |||||||
| import dan200.computercraft.core.tracking.Tracking; | import dan200.computercraft.core.tracking.Tracking; | ||||||
| import dan200.computercraft.core.tracking.TrackingField; | import dan200.computercraft.core.tracking.TrackingField; | ||||||
| import dan200.computercraft.shared.util.ThreadUtils; | import dan200.computercraft.shared.util.ThreadUtils; | ||||||
| import org.squiddev.cobalt.*; |  | ||||||
| import org.squiddev.cobalt.LuaTable; | import org.squiddev.cobalt.LuaTable; | ||||||
|  | import org.squiddev.cobalt.*; | ||||||
| import org.squiddev.cobalt.compiler.CompileException; | import org.squiddev.cobalt.compiler.CompileException; | ||||||
| import org.squiddev.cobalt.compiler.LoadState; | import org.squiddev.cobalt.compiler.LoadState; | ||||||
| import org.squiddev.cobalt.debug.DebugFrame; | import org.squiddev.cobalt.debug.DebugFrame; | ||||||
|   | |||||||
| @@ -0,0 +1,92 @@ | |||||||
|  | /* | ||||||
|  |  * This file is part of ComputerCraft - http://www.computercraft.info | ||||||
|  |  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||||
|  |  * Send enquiries to dratcliffe@gmail.com | ||||||
|  |  */ | ||||||
|  | package dan200.computercraft.mixin; | ||||||
|  | 
 | ||||||
|  | import com.mojang.blaze3d.vertex.PoseStack; | ||||||
|  | import com.mojang.blaze3d.vertex.VertexConsumer; | ||||||
|  | import dan200.computercraft.shared.Registry; | ||||||
|  | import dan200.computercraft.shared.peripheral.modem.wired.BlockCable; | ||||||
|  | import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant; | ||||||
|  | import dan200.computercraft.shared.peripheral.modem.wired.CableShapes; | ||||||
|  | import dan200.computercraft.shared.util.WorldUtil; | ||||||
|  | import net.minecraft.client.Minecraft; | ||||||
|  | import net.minecraft.client.renderer.block.BlockModelShaper; | ||||||
|  | import net.minecraft.client.renderer.block.BlockRenderDispatcher; | ||||||
|  | import net.minecraft.client.renderer.block.ModelBlockRenderer; | ||||||
|  | import net.minecraft.client.renderer.texture.OverlayTexture; | ||||||
|  | import net.minecraft.client.resources.model.BakedModel; | ||||||
|  | import net.minecraft.core.BlockPos; | ||||||
|  | import net.minecraft.world.level.BlockAndTintGetter; | ||||||
|  | import net.minecraft.world.level.block.state.BlockState; | ||||||
|  | import net.minecraft.world.phys.BlockHitResult; | ||||||
|  | import net.minecraft.world.phys.HitResult; | ||||||
|  | import net.minecraftforge.client.model.data.IModelData; | ||||||
|  | import org.spongepowered.asm.mixin.Mixin; | ||||||
|  | import org.spongepowered.asm.mixin.Shadow; | ||||||
|  | import org.spongepowered.asm.mixin.injection.At; | ||||||
|  | import org.spongepowered.asm.mixin.injection.Inject; | ||||||
|  | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; | ||||||
|  | 
 | ||||||
|  | import java.util.Random; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Provides custom block breaking progress for modems, so it only applies to the current part. | ||||||
|  |  * | ||||||
|  |  * @see BlockRenderDispatcher#renderBreakingTexture(BlockState, BlockPos, BlockAndTintGetter, PoseStack, VertexConsumer, IModelData) | ||||||
|  |  */ | ||||||
|  | @Mixin( BlockRenderDispatcher.class ) | ||||||
|  | public class BlockRenderDispatcherMixin | ||||||
|  | { | ||||||
|  |     @Shadow | ||||||
|  |     private final Random random; | ||||||
|  |     @Shadow | ||||||
|  |     private final BlockModelShaper blockModelShaper; | ||||||
|  |     @Shadow | ||||||
|  |     private final ModelBlockRenderer modelRenderer; | ||||||
|  | 
 | ||||||
|  |     public BlockRenderDispatcherMixin( Random random, BlockModelShaper blockModelShaper, ModelBlockRenderer modelRenderer ) | ||||||
|  |     { | ||||||
|  |         this.random = random; | ||||||
|  |         this.blockModelShaper = blockModelShaper; | ||||||
|  |         this.modelRenderer = modelRenderer; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Inject( | ||||||
|  |         method = "name=/^renderBreakingTexture/ desc=/IModelData;\\)V$/", | ||||||
|  |         at = @At( "HEAD" ), | ||||||
|  |         cancellable = true, | ||||||
|  |         require = 0 // This isn't critical functionality, so don't worry if we can't apply it. | ||||||
|  |     ) | ||||||
|  |     public void renderBlockDamage( | ||||||
|  |         BlockState state, BlockPos pos, BlockAndTintGetter world, PoseStack pose, VertexConsumer buffers, IModelData modelData, | ||||||
|  |         CallbackInfo info | ||||||
|  |     ) | ||||||
|  |     { | ||||||
|  |         // Only apply to cables which have both a cable and modem | ||||||
|  |         if( state.getBlock() != Registry.ModBlocks.CABLE.get() | ||||||
|  |             || !state.getValue( BlockCable.CABLE ) | ||||||
|  |             || state.getValue( BlockCable.MODEM ) == CableModemVariant.None | ||||||
|  |         ) | ||||||
|  |         { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         HitResult hit = Minecraft.getInstance().hitResult; | ||||||
|  |         if( hit == null || hit.getType() != HitResult.Type.BLOCK ) return; | ||||||
|  |         BlockPos hitPos = ((BlockHitResult) hit).getBlockPos(); | ||||||
|  | 
 | ||||||
|  |         if( !hitPos.equals( pos ) ) return; | ||||||
|  | 
 | ||||||
|  |         info.cancel(); | ||||||
|  |         BlockState newState = WorldUtil.isVecInside( CableShapes.getModemShape( state ), hit.getLocation().subtract( pos.getX(), pos.getY(), pos.getZ() ) ) | ||||||
|  |             ? state.getBlock().defaultBlockState().setValue( BlockCable.MODEM, state.getValue( BlockCable.MODEM ) ) | ||||||
|  |             : state.setValue( BlockCable.MODEM, CableModemVariant.None ); | ||||||
|  | 
 | ||||||
|  |         BakedModel model = blockModelShaper.getBlockModel( newState ); | ||||||
|  |         long seed = newState.getSeed( pos ); | ||||||
|  |         modelRenderer.tesselateBlock( world, model, newState, pos, pose, buffers, true, random, seed, OverlayTexture.NO_OVERLAY, modelData ); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -57,10 +57,11 @@ public final class NetworkHandler | |||||||
|         registerMainThread( 13, NetworkDirection.PLAY_TO_CLIENT, ComputerTerminalClientMessage.class, ComputerTerminalClientMessage::new ); |         registerMainThread( 13, NetworkDirection.PLAY_TO_CLIENT, ComputerTerminalClientMessage.class, ComputerTerminalClientMessage::new ); | ||||||
|         registerMainThread( 14, NetworkDirection.PLAY_TO_CLIENT, PlayRecordClientMessage.class, PlayRecordClientMessage::new ); |         registerMainThread( 14, NetworkDirection.PLAY_TO_CLIENT, PlayRecordClientMessage.class, PlayRecordClientMessage::new ); | ||||||
|         registerMainThread( 15, NetworkDirection.PLAY_TO_CLIENT, MonitorClientMessage.class, MonitorClientMessage::new ); |         registerMainThread( 15, NetworkDirection.PLAY_TO_CLIENT, MonitorClientMessage.class, MonitorClientMessage::new ); | ||||||
|         registerMainThread( 16, NetworkDirection.PLAY_TO_CLIENT, SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new ); |         registerMainThread( 16, NetworkDirection.PLAY_TO_CLIENT, SpeakerAudioClientMessage.class, SpeakerAudioClientMessage::new ); | ||||||
|         registerMainThread( 17, NetworkDirection.PLAY_TO_CLIENT, SpeakerStopClientMessage.class, SpeakerStopClientMessage::new ); |         registerMainThread( 17, NetworkDirection.PLAY_TO_CLIENT, SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new ); | ||||||
|         registerMainThread( 18, NetworkDirection.PLAY_TO_CLIENT, SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new ); |         registerMainThread( 18, NetworkDirection.PLAY_TO_CLIENT, SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new ); | ||||||
|         registerMainThread( 19, NetworkDirection.PLAY_TO_CLIENT, UploadResultMessage.class, UploadResultMessage::new ); |         registerMainThread( 19, NetworkDirection.PLAY_TO_CLIENT, SpeakerStopClientMessage.class, SpeakerStopClientMessage::new ); | ||||||
|  |         registerMainThread( 20, NetworkDirection.PLAY_TO_CLIENT, UploadResultMessage.class, UploadResultMessage::new ); | ||||||
|         registerMainThread( 20, NetworkDirection.PLAY_TO_CLIENT, UpgradesLoadedMessage.class, UpgradesLoadedMessage::new ); |         registerMainThread( 20, NetworkDirection.PLAY_TO_CLIENT, UpgradesLoadedMessage.class, UpgradesLoadedMessage::new ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|   | |||||||
| @@ -0,0 +1,69 @@ | |||||||
|  | /* | ||||||
|  |  * This file is part of ComputerCraft - http://www.computercraft.info | ||||||
|  |  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||||
|  |  * Send enquiries to dratcliffe@gmail.com | ||||||
|  |  */ | ||||||
|  | package dan200.computercraft.shared.network.client; | ||||||
|  | 
 | ||||||
|  | import dan200.computercraft.client.sound.SpeakerManager; | ||||||
|  | import dan200.computercraft.shared.network.NetworkMessage; | ||||||
|  | import net.minecraft.network.FriendlyByteBuf; | ||||||
|  | import net.minecraft.world.phys.Vec3; | ||||||
|  | import net.minecraftforge.api.distmarker.Dist; | ||||||
|  | import net.minecraftforge.api.distmarker.OnlyIn; | ||||||
|  | import net.minecraftforge.network.NetworkEvent; | ||||||
|  | 
 | ||||||
|  | import javax.annotation.Nonnull; | ||||||
|  | import java.nio.ByteBuffer; | ||||||
|  | import java.util.UUID; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Starts a sound on the client. | ||||||
|  |  * | ||||||
|  |  * Used by speakers to play sounds. | ||||||
|  |  * | ||||||
|  |  * @see dan200.computercraft.shared.peripheral.speaker.TileSpeaker | ||||||
|  |  */ | ||||||
|  | public class SpeakerAudioClientMessage implements NetworkMessage | ||||||
|  | { | ||||||
|  |     private final UUID source; | ||||||
|  |     private final Vec3 pos; | ||||||
|  |     private final ByteBuffer content; | ||||||
|  |     private final float volume; | ||||||
|  | 
 | ||||||
|  |     public SpeakerAudioClientMessage( UUID source, Vec3 pos, float volume, ByteBuffer content ) | ||||||
|  |     { | ||||||
|  |         this.source = source; | ||||||
|  |         this.pos = pos; | ||||||
|  |         this.content = content; | ||||||
|  |         this.volume = volume; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public SpeakerAudioClientMessage( FriendlyByteBuf buf ) | ||||||
|  |     { | ||||||
|  |         source = buf.readUUID(); | ||||||
|  |         pos = new Vec3( buf.readDouble(), buf.readDouble(), buf.readDouble() ); | ||||||
|  |         volume = buf.readFloat(); | ||||||
|  | 
 | ||||||
|  |         SpeakerManager.getSound( source ).pushAudio( buf ); | ||||||
|  |         content = null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void toBytes( @Nonnull FriendlyByteBuf buf ) | ||||||
|  |     { | ||||||
|  |         buf.writeUUID( source ); | ||||||
|  |         buf.writeDouble( pos.x() ); | ||||||
|  |         buf.writeDouble( pos.y() ); | ||||||
|  |         buf.writeDouble( pos.z() ); | ||||||
|  |         buf.writeFloat( volume ); | ||||||
|  |         buf.writeBytes( content.duplicate() ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     @OnlyIn( Dist.CLIENT ) | ||||||
|  |     public void handle( NetworkEvent.Context context ) | ||||||
|  |     { | ||||||
|  |         SpeakerManager.getSound( source ).playAudio( pos, volume ); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -5,7 +5,7 @@ | |||||||
|  */ |  */ | ||||||
| package dan200.computercraft.shared.network.client; | package dan200.computercraft.shared.network.client; | ||||||
| 
 | 
 | ||||||
| import dan200.computercraft.client.SoundManager; | import dan200.computercraft.client.sound.SpeakerManager; | ||||||
| import dan200.computercraft.shared.network.NetworkMessage; | import dan200.computercraft.shared.network.NetworkMessage; | ||||||
| import net.minecraft.network.FriendlyByteBuf; | import net.minecraft.network.FriendlyByteBuf; | ||||||
| import net.minecraft.world.phys.Vec3; | import net.minecraft.world.phys.Vec3; | ||||||
| @@ -53,6 +53,6 @@ public class SpeakerMoveClientMessage implements NetworkMessage | |||||||
|     @OnlyIn( Dist.CLIENT ) |     @OnlyIn( Dist.CLIENT ) | ||||||
|     public void handle( NetworkEvent.Context context ) |     public void handle( NetworkEvent.Context context ) | ||||||
|     { |     { | ||||||
|         SoundManager.moveSound( source, pos ); |         SpeakerManager.moveSound( source, pos ); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
|  */ |  */ | ||||||
| package dan200.computercraft.shared.network.client; | package dan200.computercraft.shared.network.client; | ||||||
| 
 | 
 | ||||||
| import dan200.computercraft.client.SoundManager; | import dan200.computercraft.client.sound.SpeakerManager; | ||||||
| import dan200.computercraft.shared.network.NetworkMessage; | import dan200.computercraft.shared.network.NetworkMessage; | ||||||
| import net.minecraft.network.FriendlyByteBuf; | import net.minecraft.network.FriendlyByteBuf; | ||||||
| import net.minecraft.resources.ResourceLocation; | import net.minecraft.resources.ResourceLocation; | ||||||
| @@ -66,6 +66,6 @@ public class SpeakerPlayClientMessage implements NetworkMessage | |||||||
|     @OnlyIn( Dist.CLIENT ) |     @OnlyIn( Dist.CLIENT ) | ||||||
|     public void handle( NetworkEvent.Context context ) |     public void handle( NetworkEvent.Context context ) | ||||||
|     { |     { | ||||||
|         SoundManager.playSound( source, pos, sound, volume, pitch ); |         SpeakerManager.getSound( source ).playSound( pos, sound, volume, pitch ); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
|  */ |  */ | ||||||
| package dan200.computercraft.shared.network.client; | package dan200.computercraft.shared.network.client; | ||||||
| 
 | 
 | ||||||
| import dan200.computercraft.client.SoundManager; | import dan200.computercraft.client.sound.SpeakerManager; | ||||||
| import dan200.computercraft.shared.network.NetworkMessage; | import dan200.computercraft.shared.network.NetworkMessage; | ||||||
| import net.minecraft.network.FriendlyByteBuf; | import net.minecraft.network.FriendlyByteBuf; | ||||||
| import net.minecraftforge.api.distmarker.Dist; | import net.minecraftforge.api.distmarker.Dist; | ||||||
| @@ -46,6 +46,6 @@ public class SpeakerStopClientMessage implements NetworkMessage | |||||||
|     @OnlyIn( Dist.CLIENT ) |     @OnlyIn( Dist.CLIENT ) | ||||||
|     public void handle( NetworkEvent.Context context ) |     public void handle( NetworkEvent.Context context ) | ||||||
|     { |     { | ||||||
|         SoundManager.stopSound( source ); |         SpeakerManager.stopSound( source ); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL; | |||||||
| /** | /** | ||||||
|  * This peripheral allows you to interact with command blocks. |  * This peripheral allows you to interact with command blocks. | ||||||
|  * |  * | ||||||
|  * Command blocks are only wrapped as peripherals if the {@literal enable_command_block} option is true within the |  * Command blocks are only wrapped as peripherals if the {@code enable_command_block} option is true within the | ||||||
|  * config. |  * config. | ||||||
|  * |  * | ||||||
|  * This API is <em>not</em> the same as the {@link CommandAPI} API, which is exposed on command computers. |  * This API is <em>not</em> the same as the {@link CommandAPI} API, which is exposed on command computers. | ||||||
|   | |||||||
| @@ -0,0 +1,117 @@ | |||||||
|  | /* | ||||||
|  |  * This file is part of ComputerCraft - http://www.computercraft.info | ||||||
|  |  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||||
|  |  * Send enquiries to dratcliffe@gmail.com | ||||||
|  |  */ | ||||||
|  | package dan200.computercraft.shared.peripheral.speaker; | ||||||
|  | 
 | ||||||
|  | import dan200.computercraft.api.lua.LuaException; | ||||||
|  | import dan200.computercraft.api.lua.LuaTable; | ||||||
|  | import net.minecraft.util.Mth; | ||||||
|  | 
 | ||||||
|  | import javax.annotation.Nonnull; | ||||||
|  | import java.nio.ByteBuffer; | ||||||
|  | import java.util.Optional; | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
|  | 
 | ||||||
|  | import static dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral.SAMPLE_RATE; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Internal state of the DFPWM decoder and the state of playback. | ||||||
|  |  */ | ||||||
|  | class DfpwmState | ||||||
|  | { | ||||||
|  |     private static final long SECOND = TimeUnit.SECONDS.toNanos( 1 ); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The minimum size of the client's audio buffer. Once we have less than this on the client, we should send another | ||||||
|  |      * batch of audio. | ||||||
|  |      */ | ||||||
|  |     private static final long CLIENT_BUFFER = (long) (SECOND * 1.5); | ||||||
|  | 
 | ||||||
|  |     private static final int PREC = 10; | ||||||
|  | 
 | ||||||
|  |     private int charge = 0; // q | ||||||
|  |     private int strength = 0; // s | ||||||
|  |     private boolean previousBit = false; | ||||||
|  | 
 | ||||||
|  |     private boolean unplayed = true; | ||||||
|  |     private long clientEndTime = System.nanoTime(); | ||||||
|  |     private float pendingVolume = 1.0f; | ||||||
|  |     private ByteBuffer pendingAudio; | ||||||
|  | 
 | ||||||
|  |     synchronized boolean pushBuffer( LuaTable<?, ?> table, int size, @Nonnull Optional<Double> volume ) throws LuaException | ||||||
|  |     { | ||||||
|  |         if( pendingAudio != null ) return false; | ||||||
|  | 
 | ||||||
|  |         int outSize = size / 8; | ||||||
|  |         ByteBuffer buffer = ByteBuffer.allocate( outSize ); | ||||||
|  | 
 | ||||||
|  |         for( int i = 0; i < outSize; i++ ) | ||||||
|  |         { | ||||||
|  |             int thisByte = 0; | ||||||
|  |             for( int j = 1; j <= 8; j++ ) | ||||||
|  |             { | ||||||
|  |                 int level = table.getInt( i * 8 + j ); | ||||||
|  |                 if( level < -128 || level > 127 ) | ||||||
|  |                 { | ||||||
|  |                     throw new LuaException( "table item #" + (i * 8 + j) + " must be between -128 and 127" ); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 boolean currentBit = level > charge || (level == charge && charge == 127); | ||||||
|  | 
 | ||||||
|  |                 // Identical to DfpwmStream. Not happy with this, but saves some inheritance. | ||||||
|  |                 int target = currentBit ? 127 : -128; | ||||||
|  | 
 | ||||||
|  |                 // q' <- q + (s * (t - q) + 128)/256 | ||||||
|  |                 int nextCharge = charge + ((strength * (target - charge) + (1 << (PREC - 1))) >> PREC); | ||||||
|  |                 if( nextCharge == charge && nextCharge != target ) nextCharge += currentBit ? 1 : -1; | ||||||
|  | 
 | ||||||
|  |                 int z = currentBit == previousBit ? (1 << PREC) - 1 : 0; | ||||||
|  | 
 | ||||||
|  |                 int nextStrength = strength; | ||||||
|  |                 if( strength != z ) nextStrength += currentBit == previousBit ? 1 : -1; | ||||||
|  |                 if( nextStrength < 2 << (PREC - 8) ) nextStrength = 2 << (PREC - 8); | ||||||
|  | 
 | ||||||
|  |                 charge = nextCharge; | ||||||
|  |                 strength = nextStrength; | ||||||
|  |                 previousBit = currentBit; | ||||||
|  | 
 | ||||||
|  |                 thisByte = (thisByte >> 1) + (currentBit ? 128 : 0); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             buffer.put( (byte) thisByte ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         buffer.flip(); | ||||||
|  | 
 | ||||||
|  |         pendingAudio = buffer; | ||||||
|  |         pendingVolume = Mth.clamp( volume.orElse( (double) pendingVolume ).floatValue(), 0.0f, 3.0f ); | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     boolean shouldSendPending( long now ) | ||||||
|  |     { | ||||||
|  |         return pendingAudio != null && now >= clientEndTime - CLIENT_BUFFER; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     ByteBuffer pullPending( long now ) | ||||||
|  |     { | ||||||
|  |         ByteBuffer audio = pendingAudio; | ||||||
|  |         pendingAudio = null; | ||||||
|  |         // Compute when we should consider sending the next packet. | ||||||
|  |         clientEndTime = Math.max( now, clientEndTime ) + (audio.remaining() * SECOND * 8 / SAMPLE_RATE); | ||||||
|  |         unplayed = false; | ||||||
|  |         return audio; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     boolean isPlaying() | ||||||
|  |     { | ||||||
|  |         return unplayed || clientEndTime >= System.nanoTime(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     float getVolume() | ||||||
|  |     { | ||||||
|  |         return pendingVolume; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -9,77 +9,176 @@ import dan200.computercraft.ComputerCraft; | |||||||
| import dan200.computercraft.api.lua.ILuaContext; | import dan200.computercraft.api.lua.ILuaContext; | ||||||
| import dan200.computercraft.api.lua.LuaException; | import dan200.computercraft.api.lua.LuaException; | ||||||
| import dan200.computercraft.api.lua.LuaFunction; | import dan200.computercraft.api.lua.LuaFunction; | ||||||
|  | import dan200.computercraft.api.lua.LuaTable; | ||||||
|  | import dan200.computercraft.api.peripheral.IComputerAccess; | ||||||
| import dan200.computercraft.api.peripheral.IPeripheral; | import dan200.computercraft.api.peripheral.IPeripheral; | ||||||
| import dan200.computercraft.shared.network.NetworkHandler; | import dan200.computercraft.shared.network.NetworkHandler; | ||||||
|  | import dan200.computercraft.shared.network.client.SpeakerAudioClientMessage; | ||||||
| import dan200.computercraft.shared.network.client.SpeakerMoveClientMessage; | import dan200.computercraft.shared.network.client.SpeakerMoveClientMessage; | ||||||
| import dan200.computercraft.shared.network.client.SpeakerPlayClientMessage; | import dan200.computercraft.shared.network.client.SpeakerPlayClientMessage; | ||||||
|  | import dan200.computercraft.shared.network.client.SpeakerStopClientMessage; | ||||||
| import net.minecraft.ResourceLocationException; | import net.minecraft.ResourceLocationException; | ||||||
| import net.minecraft.core.BlockPos; | import net.minecraft.core.BlockPos; | ||||||
| import net.minecraft.network.protocol.game.ClientboundCustomSoundPacket; | import net.minecraft.network.protocol.game.ClientboundCustomSoundPacket; | ||||||
| import net.minecraft.resources.ResourceLocation; | import net.minecraft.resources.ResourceLocation; | ||||||
| import net.minecraft.server.MinecraftServer; | import net.minecraft.server.MinecraftServer; | ||||||
| import net.minecraft.sounds.SoundSource; | import net.minecraft.sounds.SoundSource; | ||||||
| import net.minecraft.util.Mth; |  | ||||||
| import net.minecraft.world.level.Level; | import net.minecraft.world.level.Level; | ||||||
| import net.minecraft.world.level.block.state.properties.NoteBlockInstrument; | import net.minecraft.world.level.block.state.properties.NoteBlockInstrument; | ||||||
| import net.minecraft.world.phys.Vec3; | import net.minecraft.world.phys.Vec3; | ||||||
| 
 | 
 | ||||||
| import javax.annotation.Nonnull; | import javax.annotation.Nonnull; | ||||||
| import java.util.Optional; | import javax.annotation.Nullable; | ||||||
| import java.util.UUID; | import java.util.*; | ||||||
| import java.util.concurrent.atomic.AtomicInteger; |  | ||||||
| 
 | 
 | ||||||
| import static dan200.computercraft.api.lua.LuaValues.checkFinite; | import static dan200.computercraft.api.lua.LuaValues.checkFinite; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Speakers allow playing notes and other sounds. |  * The speaker peirpheral allow your computer to play notes and other sounds. | ||||||
|  |  * | ||||||
|  |  * The speaker can play three kinds of sound, in increasing orders of complexity: | ||||||
|  |  * - {@link #playNote} allows you to play noteblock note. | ||||||
|  |  * - {@link #playSound} plays any built-in Minecraft sound, such as block sounds or mob noises. | ||||||
|  |  * - {@link #playAudio} can play arbitrary audio. | ||||||
|  * |  * | ||||||
|  * @cc.module speaker |  * @cc.module speaker | ||||||
|  * @cc.since 1.80pr1 |  * @cc.since 1.80pr1 | ||||||
|  */ |  */ | ||||||
| public abstract class SpeakerPeripheral implements IPeripheral | public abstract class SpeakerPeripheral implements IPeripheral | ||||||
| { | { | ||||||
|     private static final int MIN_TICKS_BETWEEN_SOUNDS = 1; |     /** | ||||||
|  |      * Number of samples/s in a dfpwm1a audio track. | ||||||
|  |      */ | ||||||
|  |     public static final int SAMPLE_RATE = 48000; | ||||||
|  | 
 | ||||||
|  |     private final UUID source = UUID.randomUUID(); | ||||||
|  |     private final Set<IComputerAccess> computers = Collections.newSetFromMap( new HashMap<>() ); | ||||||
| 
 | 
 | ||||||
|     private long clock = 0; |     private long clock = 0; | ||||||
|     private long lastPlayTime = 0; |  | ||||||
|     private final AtomicInteger notesThisTick = new AtomicInteger(); |  | ||||||
| 
 |  | ||||||
|     private long lastPositionTime; |     private long lastPositionTime; | ||||||
|     private Vec3 lastPosition; |     private Vec3 lastPosition; | ||||||
| 
 | 
 | ||||||
|  |     private long lastPlayTime; | ||||||
|  | 
 | ||||||
|  |     private final List<PendingSound> pendingNotes = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  |     private final Object lock = new Object(); | ||||||
|  |     private boolean shouldStop; | ||||||
|  |     private PendingSound pendingSound = null; | ||||||
|  |     private DfpwmState dfpwmState; | ||||||
|  | 
 | ||||||
|     public void update() |     public void update() | ||||||
|     { |     { | ||||||
|         clock++; |         clock++; | ||||||
|         notesThisTick.set( 0 ); | 
 | ||||||
|  |         Vec3 pos = getPosition(); | ||||||
|  |         Level level = getLevel(); | ||||||
|  |         if( level == null ) return; | ||||||
|  |         MinecraftServer server = level.getServer(); | ||||||
|  | 
 | ||||||
|  |         synchronized( pendingNotes ) | ||||||
|  |         { | ||||||
|  |             for( PendingSound sound : pendingNotes ) | ||||||
|  |             { | ||||||
|  |                 lastPlayTime = clock; | ||||||
|  |                 server.getPlayerList().broadcast( | ||||||
|  |                     null, pos.x, pos.y, pos.z, sound.volume * 16, level.dimension(), | ||||||
|  |                     new ClientboundCustomSoundPacket( sound.location, SoundSource.RECORDS, pos, sound.volume, sound.pitch ) | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |             pendingNotes.clear(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // The audio dispatch logic here is pretty messy, which I'm not proud of. The general logic here is that we hold | ||||||
|  |         // the main "lock" when modifying the dfpwmState/pendingSound variables and no other time. | ||||||
|  |         // dfpwmState will only ever transition from having a buffer to not having a buffer on the main thread (so this | ||||||
|  |         // method), so we don't need to bother locking that. | ||||||
|  |         boolean shouldStop; | ||||||
|  |         PendingSound sound; | ||||||
|  |         DfpwmState dfpwmState; | ||||||
|  |         synchronized( lock ) | ||||||
|  |         { | ||||||
|  |             sound = pendingSound; | ||||||
|  |             dfpwmState = this.dfpwmState; | ||||||
|  |             pendingSound = null; | ||||||
|  | 
 | ||||||
|  |             shouldStop = this.shouldStop; | ||||||
|  |             if( shouldStop ) | ||||||
|  |             { | ||||||
|  |                 dfpwmState = this.dfpwmState = null; | ||||||
|  |                 sound = null; | ||||||
|  |                 this.shouldStop = false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Stop the speaker and nuke the position, so we don't update it again. | ||||||
|  |         if( shouldStop && lastPosition != null ) | ||||||
|  |         { | ||||||
|  |             lastPosition = null; | ||||||
|  |             NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( getSource() ) ); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         long now = System.nanoTime(); | ||||||
|  |         if( sound != null ) | ||||||
|  |         { | ||||||
|  |             lastPlayTime = clock; | ||||||
|  |             NetworkHandler.sendToAllAround( | ||||||
|  |                 new SpeakerPlayClientMessage( getSource(), pos, sound.location, sound.volume, sound.pitch ), | ||||||
|  |                 level, pos, sound.volume * 16 | ||||||
|  |             ); | ||||||
|  |             syncedPosition( pos ); | ||||||
|  |         } | ||||||
|  |         else if( dfpwmState != null && dfpwmState.shouldSendPending( now ) ) | ||||||
|  |         { | ||||||
|  |             // If clients need to receive another batch of audio, send it and then notify computers our internal buffer is | ||||||
|  |             // free again. | ||||||
|  |             NetworkHandler.sendToAllTracking( | ||||||
|  |                 new SpeakerAudioClientMessage( getSource(), pos, dfpwmState.getVolume(), dfpwmState.pullPending( now ) ), | ||||||
|  |                 getLevel().getChunkAt( new BlockPos( pos ) ) | ||||||
|  |             ); | ||||||
|  |             syncedPosition( pos ); | ||||||
|  | 
 | ||||||
|  |             // And notify computers that we have space for more audio. | ||||||
|  |             for( IComputerAccess computer : computers ) | ||||||
|  |             { | ||||||
|  |                 computer.queueEvent( "speaker_audio_empty", computer.getAttachmentName() ); | ||||||
|  |             } | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         // Push position updates to any speakers which have ever played a note, |         // Push position updates to any speakers which have ever played a note, | ||||||
|         // have moved by a non-trivial amount and haven't had a position update |         // have moved by a non-trivial amount and haven't had a position update | ||||||
|         // in the last second. |         // in the last second. | ||||||
|         if( lastPlayTime > 0 && (clock - lastPositionTime) >= 20 ) |         if( lastPosition != null && (clock - lastPositionTime) >= 20 ) | ||||||
|         { |         { | ||||||
|             Vec3 position = getPosition(); |             Vec3 position = getPosition(); | ||||||
|             if( lastPosition == null || lastPosition.distanceToSqr( position ) >= 0.1 ) |             if( lastPosition.distanceToSqr( position ) >= 0.1 ) | ||||||
|             { |             { | ||||||
|                 lastPosition = position; |  | ||||||
|                 lastPositionTime = clock; |  | ||||||
|                 NetworkHandler.sendToAllTracking( |                 NetworkHandler.sendToAllTracking( | ||||||
|                     new SpeakerMoveClientMessage( getSource(), position ), |                     new SpeakerMoveClientMessage( getSource(), position ), | ||||||
|                     getLevel().getChunkAt( new BlockPos( position ) ) |                     getLevel().getChunkAt( new BlockPos( position ) ) | ||||||
|                 ); |                 ); | ||||||
|  |                 syncedPosition( position ); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Nullable | ||||||
|     public abstract Level getLevel(); |     public abstract Level getLevel(); | ||||||
| 
 | 
 | ||||||
|  |     @Nonnull | ||||||
|     public abstract Vec3 getPosition(); |     public abstract Vec3 getPosition(); | ||||||
| 
 | 
 | ||||||
|     protected abstract UUID getSource(); |     @Nonnull | ||||||
| 
 |     public UUID getSource() | ||||||
|     public boolean madeSound( long ticks ) |  | ||||||
|     { |     { | ||||||
|         return clock - lastPlayTime <= ticks; |         return source; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean madeSound() | ||||||
|  |     { | ||||||
|  |         DfpwmState state = dfpwmState; | ||||||
|  |         return clock - lastPlayTime <= 20 || (state != null && state.isPlaying()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Nonnull |     @Nonnull | ||||||
| @@ -90,18 +189,81 @@ public abstract class SpeakerPeripheral implements IPeripheral | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Plays a sound through the speaker. |      * Plays a note block note through the speaker. | ||||||
|      * |      * | ||||||
|      * This plays sounds similar to the {@code /playsound} command in Minecraft. |      * This takes the name of a note to play, as well as optionally the volume | ||||||
|      * It takes the namespaced path of a sound (e.g. {@code minecraft:block.note_block.harp}) |      * and pitch to play the note at. | ||||||
|      * with an optional volume and speed multiplier, and plays it through the speaker. |      * | ||||||
|  |      * The pitch argument uses semitones as the unit. This directly maps to the | ||||||
|  |      * number of clicks on a note block. For reference, 0, 12, and 24 map to F#, | ||||||
|  |      * and 6 and 18 map to C. | ||||||
|  |      * | ||||||
|  |      * A maximum of 8 notes can be played in a single tick. If this limit is hit, this function will return | ||||||
|  |      * {@literal false}. | ||||||
|  |      * | ||||||
|  |      * ### Valid instruments | ||||||
|  |      * The speaker supports [all of Minecraft's noteblock instruments](https://minecraft.fandom.com/wiki/Note_Block#Instruments). | ||||||
|  |      * These are: | ||||||
|  |      * | ||||||
|  |      * {@code "harp"}, {@code "basedrum"}, {@code "snare"}, {@code "hat"}, {@code "bass"}, @code "flute"}, | ||||||
|  |      * {@code "bell"}, {@code "guitar"}, {@code "chime"}, {@code "xylophone"}, {@code "iron_xylophone"}, | ||||||
|  |      * {@code "cow_bell"}, {@code "didgeridoo"}, {@code "bit"}, {@code "banjo"} and {@code "pling"}. | ||||||
|  |      * | ||||||
|  |      * @param context     The Lua context | ||||||
|  |      * @param instrumentA The instrument to use to play this note. | ||||||
|  |      * @param volumeA     The volume to play the note at, from 0.0 to 3.0. Defaults to 1.0. | ||||||
|  |      * @param pitchA      The pitch to play the note at in semitones, from 0 to 24. Defaults to 12. | ||||||
|  |      * @return Whether the note could be played as the limit was reached. | ||||||
|  |      * @throws LuaException If the instrument doesn't exist. | ||||||
|  |      */ | ||||||
|  |     @LuaFunction | ||||||
|  |     public final boolean playNote( ILuaContext context, String instrumentA, Optional<Double> volumeA, Optional<Double> pitchA ) throws LuaException | ||||||
|  |     { | ||||||
|  |         float volume = (float) checkFinite( 1, volumeA.orElse( 1.0 ) ); | ||||||
|  |         float pitch = (float) checkFinite( 2, pitchA.orElse( 1.0 ) ); | ||||||
|  | 
 | ||||||
|  |         NoteBlockInstrument instrument = null; | ||||||
|  |         for( NoteBlockInstrument testInstrument : NoteBlockInstrument.values() ) | ||||||
|  |         { | ||||||
|  |             if( testInstrument.getSerializedName().equalsIgnoreCase( instrumentA ) ) | ||||||
|  |             { | ||||||
|  |                 instrument = testInstrument; | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Check if the note exists | ||||||
|  |         if( instrument == null ) throw new LuaException( "Invalid instrument, \"" + instrument + "\"!" ); | ||||||
|  | 
 | ||||||
|  |         synchronized( pendingNotes ) | ||||||
|  |         { | ||||||
|  |             if( pendingNotes.size() >= ComputerCraft.maxNotesPerTick ) return false; | ||||||
|  |             pendingNotes.add( new PendingSound( instrument.getSoundEvent().getRegistryName(), volume, (float) Math.pow( 2.0, (pitch - 12.0) / 12.0 ) ) ); | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Plays a Minecraft sound through the speaker. | ||||||
|  |      * | ||||||
|  |      * This takes the [name of a Minecraft sound](https://minecraft.fandom.com/wiki/Sounds.json), such as | ||||||
|  |      * {@code "minecraft:block.note_block.harp"}, as well as an optional volume and pitch. | ||||||
|  |      * | ||||||
|  |      * Only one sound can be played at once. This function will return {@literal false} if another sound was started | ||||||
|  |      * this tick, or if some {@link #playAudio audio} is still playing. | ||||||
|      * |      * | ||||||
|      * @param context The Lua context |      * @param context The Lua context | ||||||
|      * @param name    The name of the sound to play. |      * @param name    The name of the sound to play. | ||||||
|      * @param volumeA The volume to play the sound at, from 0.0 to 3.0. Defaults to 1.0. |      * @param volumeA The volume to play the sound at, from 0.0 to 3.0. Defaults to 1.0. | ||||||
|      * @param pitchA  The speed to play the sound at, from 0.5 to 2.0. Defaults to 1.0. |      * @param pitchA  The speed to play the sound at, from 0.5 to 2.0. Defaults to 1.0. | ||||||
|      * @return Whether the sound could be played. |      * @return Whether the sound could be played. | ||||||
|      * @throws LuaException If the sound name couldn't be decoded. |      * @throws LuaException If the sound name was invalid. | ||||||
|  |      * @cc.usage Play a creeper hiss with the speaker. | ||||||
|  |      * | ||||||
|  |      * <pre>{@code | ||||||
|  |      * local speaker = peripheral.find("speaker") | ||||||
|  |      * speaker.playSound("entity.creeper.primed") | ||||||
|  |      * }</pre> | ||||||
|      */ |      */ | ||||||
|     @LuaFunction |     @LuaFunction | ||||||
|     public final boolean playSound( ILuaContext context, String name, Optional<Double> volumeA, Optional<Double> pitchA ) throws LuaException |     public final boolean playSound( ILuaContext context, String name, Optional<Double> volumeA, Optional<Double> pitchA ) throws LuaException | ||||||
| @@ -119,89 +281,123 @@ public abstract class SpeakerPeripheral implements IPeripheral | |||||||
|             throw new LuaException( "Malformed sound name '" + name + "' " ); |             throw new LuaException( "Malformed sound name '" + name + "' " ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return playSound( context, identifier, volume, pitch, false ); |         synchronized( lock ) | ||||||
|  |         { | ||||||
|  |             if( dfpwmState != null && dfpwmState.isPlaying() ) return false; | ||||||
|  |             dfpwmState = null; | ||||||
|  |             pendingSound = new PendingSound( identifier, volume, pitch ); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Plays a note block note through the speaker. |      * Attempt to stream some audio data to the speaker. | ||||||
|      * |      * | ||||||
|      * This takes the name of a note to play, as well as optionally the volume |      * This accepts a list of audio samples as amplitudes between -128 and 127. These are stored in an internal buffer | ||||||
|      * and pitch to play the note at. |      * and played back at 48kHz. If this buffer is full, this function will return {@literal false}. You should wait for | ||||||
|  |      * a @{speaker_audio_empty} event before trying again. | ||||||
|      * |      * | ||||||
|      * The pitch argument uses semitones as the unit. This directly maps to the |      * :::note | ||||||
|      * number of clicks on a note block. For reference, 0, 12, and 24 map to F#, |      * The speaker only buffers a single call to {@link #playAudio} at once. This means if you try to play a small | ||||||
|      * and 6 and 18 map to C. |      * number of samples, you'll have a lot of stutter. You should try to play as many samples in one call as possible | ||||||
|  |      * (up to 128×1024), as this reduces the chances of audio stuttering or halting, especially when the server or | ||||||
|  |      * computer is lagging. | ||||||
|  |      * ::: | ||||||
|      * |      * | ||||||
|      * @param context The Lua context |      * {@literal @}{speaker_audio} provides a more complete guide in to using speakers | ||||||
|      * @param name    The name of the note to play. |      * | ||||||
|      * @param volumeA The volume to play the note at, from 0.0 to 3.0. Defaults to 1.0. |      * @param context The Lua context. | ||||||
|      * @param pitchA  The pitch to play the note at in semitones, from 0 to 24. Defaults to 12. |      * @param audio   The audio data to play. | ||||||
|      * @return Whether the note could be played. |      * @param volume  The volume to play this audio at. | ||||||
|      * @throws LuaException If the instrument doesn't exist. |      * @return If there was room to accept this audio data. | ||||||
|  |      * @throws LuaException If the audio data is malformed. | ||||||
|  |      * @cc.tparam {number...} audio A list of amplitudes. | ||||||
|  |      * @cc.tparam [opt] number volume The volume to play this audio at. If not given, defaults to the previous volume | ||||||
|  |      * given to {@link #playAudio}. | ||||||
|  |      * @cc.since 1.100 | ||||||
|  |      * @cc.usage Read an audio file, decode it using @{cc.audio.dfpwm}, and play it using the speaker. | ||||||
|  |      * | ||||||
|  |      * <pre>{@code | ||||||
|  |      * local dfpwm = require("cc.audio.dfpwm") | ||||||
|  |      * local speaker = peripheral.find("speaker") | ||||||
|  |      * | ||||||
|  |      * local decoder = dfpwm.make_decoder() | ||||||
|  |      * for chunk in io.lines("data/example.dfpwm", 16 * 1024) do | ||||||
|  |      *     local buffer = decoder(chunk) | ||||||
|  |      * | ||||||
|  |      *     while not speaker.playAudio(buffer) do | ||||||
|  |      *         os.pullEvent("speaker_audio_empty") | ||||||
|  |      *     end | ||||||
|  |      * end | ||||||
|  |      * }</pre> | ||||||
|  |      * @cc.see cc.audio.dfpwm Provides utilities for decoding DFPWM audio files into a format which can be played by | ||||||
|  |      * the speaker. | ||||||
|  |      * @cc.see speaker_audio For a more complete introduction to the {@link #playAudio} function. | ||||||
|  |      */ | ||||||
|  |     @LuaFunction( unsafe = true ) | ||||||
|  |     public final boolean playAudio( ILuaContext context, LuaTable<?, ?> audio, Optional<Double> volume ) throws LuaException | ||||||
|  |     { | ||||||
|  |         checkFinite( 1, volume.orElse( 0.0 ) ); | ||||||
|  | 
 | ||||||
|  |         // TODO: Use ArgumentHelpers instead? | ||||||
|  |         int length = audio.length(); | ||||||
|  |         if( length <= 0 ) throw new LuaException( "Cannot play empty audio" ); | ||||||
|  |         if( length > 1024 * 16 * 8 ) throw new LuaException( "Audio data is too large" ); | ||||||
|  | 
 | ||||||
|  |         DfpwmState state; | ||||||
|  |         synchronized( lock ) | ||||||
|  |         { | ||||||
|  |             if( dfpwmState == null || !dfpwmState.isPlaying() ) dfpwmState = new DfpwmState(); | ||||||
|  |             state = dfpwmState; | ||||||
|  | 
 | ||||||
|  |             pendingSound = null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return state.pushBuffer( audio, length, volume ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Stop all audio being played by this speaker. | ||||||
|  |      * | ||||||
|  |      * This clears any audio that {@link #playAudio} had queued and stops the latest sound played by {@link #playSound}. | ||||||
|  |      * | ||||||
|  |      * @cc.since 1.100 | ||||||
|      */ |      */ | ||||||
|     @LuaFunction |     @LuaFunction | ||||||
|     public final synchronized boolean playNote( ILuaContext context, String name, Optional<Double> volumeA, Optional<Double> pitchA ) throws LuaException |     public final void stop() | ||||||
|     { |     { | ||||||
|         float volume = (float) checkFinite( 1, volumeA.orElse( 1.0 ) ); |         shouldStop = true; | ||||||
|         float pitch = (float) checkFinite( 2, pitchA.orElse( 1.0 ) ); |  | ||||||
| 
 |  | ||||||
|         NoteBlockInstrument instrument = null; |  | ||||||
|         for( NoteBlockInstrument testInstrument : NoteBlockInstrument.values() ) |  | ||||||
|         { |  | ||||||
|             if( testInstrument.getSerializedName().equalsIgnoreCase( name ) ) |  | ||||||
|             { |  | ||||||
|                 instrument = testInstrument; |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|         // Check if the note exists |     private void syncedPosition( Vec3 position ) | ||||||
|         if( instrument == null ) throw new LuaException( "Invalid instrument, \"" + name + "\"!" ); |     { | ||||||
| 
 |         lastPosition = position; | ||||||
|         // If the resource location for note block notes changes, this method call will need to be updated |         lastPositionTime = clock; | ||||||
|         boolean success = playSound( context, instrument.getSoundEvent().getRegistryName(), volume, (float) Math.pow( 2.0, (pitch - 12.0) / 12.0 ), true ); |  | ||||||
|         if( success ) notesThisTick.incrementAndGet(); |  | ||||||
|         return success; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private synchronized boolean playSound( ILuaContext context, ResourceLocation name, float volume, float pitch, boolean isNote ) throws LuaException |     @Override | ||||||
|  |     public void attach( @Nonnull IComputerAccess computer ) | ||||||
|     { |     { | ||||||
|         if( clock - lastPlayTime < MIN_TICKS_BETWEEN_SOUNDS ) |         computers.add( computer ); | ||||||
|         { |  | ||||||
|             // Rate limiting occurs when we've already played a sound within the last tick. |  | ||||||
|             if( !isNote ) return false; |  | ||||||
|             // Or we've played more notes than allowable within the current tick. |  | ||||||
|             if( clock - lastPlayTime != 0 || notesThisTick.get() >= ComputerCraft.maxNotesPerTick ) return false; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|         Level world = getLevel(); |     @Override | ||||||
|         Vec3 pos = getPosition(); |     public void detach( @Nonnull IComputerAccess computer ) | ||||||
| 
 |  | ||||||
|         float actualVolume = Mth.clamp( volume, 0.0f, 3.0f ); |  | ||||||
|         float range = actualVolume * 16; |  | ||||||
| 
 |  | ||||||
|         context.issueMainThreadTask( () -> { |  | ||||||
|             MinecraftServer server = world.getServer(); |  | ||||||
|             if( server == null ) return null; |  | ||||||
| 
 |  | ||||||
|             if( isNote ) |  | ||||||
|     { |     { | ||||||
|                 server.getPlayerList().broadcast( |         computers.remove( computer ); | ||||||
|                     null, pos.x, pos.y, pos.z, range, world.dimension(), |  | ||||||
|                     new ClientboundCustomSoundPacket( name, SoundSource.RECORDS, pos, actualVolume, pitch ) |  | ||||||
|                 ); |  | ||||||
|     } |     } | ||||||
|             else |  | ||||||
|             { |  | ||||||
|                 NetworkHandler.sendToAllAround( |  | ||||||
|                     new SpeakerPlayClientMessage( getSource(), pos, name, actualVolume, pitch ), |  | ||||||
|                     world, pos, range |  | ||||||
|                 ); |  | ||||||
|             } |  | ||||||
|             return null; |  | ||||||
|         } ); |  | ||||||
| 
 | 
 | ||||||
|         lastPlayTime = clock; |     private static final class PendingSound | ||||||
|         return true; |     { | ||||||
|  |         final ResourceLocation location; | ||||||
|  |         final float volume; | ||||||
|  |         final float pitch; | ||||||
|  | 
 | ||||||
|  |         private PendingSound( ResourceLocation location, float volume, float pitch ) | ||||||
|  |         { | ||||||
|  |             this.location = location; | ||||||
|  |             this.volume = volume; | ||||||
|  |             this.pitch = pitch; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -21,7 +21,6 @@ import net.minecraftforge.common.util.LazyOptional; | |||||||
| 
 | 
 | ||||||
| import javax.annotation.Nonnull; | import javax.annotation.Nonnull; | ||||||
| import javax.annotation.Nullable; | import javax.annotation.Nullable; | ||||||
| import java.util.UUID; |  | ||||||
| 
 | 
 | ||||||
| import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL; | import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL; | ||||||
| 
 | 
 | ||||||
| @@ -29,7 +28,6 @@ public class TileSpeaker extends TileGeneric | |||||||
| { | { | ||||||
|     private final SpeakerPeripheral peripheral; |     private final SpeakerPeripheral peripheral; | ||||||
|     private LazyOptional<IPeripheral> peripheralCap; |     private LazyOptional<IPeripheral> peripheralCap; | ||||||
|     private final UUID source = UUID.randomUUID(); |  | ||||||
| 
 | 
 | ||||||
|     public TileSpeaker( BlockEntityType<TileSpeaker> type, BlockPos pos, BlockState state ) |     public TileSpeaker( BlockEntityType<TileSpeaker> type, BlockPos pos, BlockState state ) | ||||||
|     { |     { | ||||||
| @@ -48,7 +46,7 @@ public class TileSpeaker extends TileGeneric | |||||||
|         super.setRemoved(); |         super.setRemoved(); | ||||||
|         if( level != null && !level.isClientSide ) |         if( level != null && !level.isClientSide ) | ||||||
|         { |         { | ||||||
|             NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( source ) ); |             NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( peripheral.getSource() ) ); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -87,6 +85,7 @@ public class TileSpeaker extends TileGeneric | |||||||
|             return speaker.getLevel(); |             return speaker.getLevel(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         @Nonnull | ||||||
|         @Override |         @Override | ||||||
|         public Vec3 getPosition() |         public Vec3 getPosition() | ||||||
|         { |         { | ||||||
| @@ -94,12 +93,6 @@ public class TileSpeaker extends TileGeneric | |||||||
|             return new Vec3( pos.getX(), pos.getY(), pos.getZ() ); |             return new Vec3( pos.getX(), pos.getY(), pos.getZ() ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         @Override |  | ||||||
|         protected UUID getSource() |  | ||||||
|         { |  | ||||||
|             return speaker.source; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @Override |         @Override | ||||||
|         public boolean equals( @Nullable IPeripheral other ) |         public boolean equals( @Nullable IPeripheral other ) | ||||||
|         { |         { | ||||||
|   | |||||||
| @@ -12,7 +12,6 @@ import net.minecraft.server.MinecraftServer; | |||||||
| import net.minecraftforge.server.ServerLifecycleHooks; | import net.minecraftforge.server.ServerLifecycleHooks; | ||||||
| 
 | 
 | ||||||
| import javax.annotation.Nonnull; | import javax.annotation.Nonnull; | ||||||
| import java.util.UUID; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * A speaker peripheral which is used on an upgrade, and so is only attached to one computer. |  * A speaker peripheral which is used on an upgrade, and so is only attached to one computer. | ||||||
| @@ -21,14 +20,6 @@ public abstract class UpgradeSpeakerPeripheral extends SpeakerPeripheral | |||||||
| { | { | ||||||
|     public static final String ADJECTIVE = "upgrade.computercraft.speaker.adjective"; |     public static final String ADJECTIVE = "upgrade.computercraft.speaker.adjective"; | ||||||
| 
 | 
 | ||||||
|     private final UUID source = UUID.randomUUID(); |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     protected final UUID getSource() |  | ||||||
|     { |  | ||||||
|         return source; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |     @Override | ||||||
|     public void detach( @Nonnull IComputerAccess computer ) |     public void detach( @Nonnull IComputerAccess computer ) | ||||||
|     { |     { | ||||||
| @@ -36,6 +27,6 @@ public abstract class UpgradeSpeakerPeripheral extends SpeakerPeripheral | |||||||
|         MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); |         MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); | ||||||
|         if( server == null || server.isStopped() ) return; |         if( server == null || server.isStopped() ) return; | ||||||
| 
 | 
 | ||||||
|         NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( source ) ); |         NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( getSource() ) ); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -42,6 +42,6 @@ public class PocketSpeaker extends AbstractPocketUpgrade | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         speaker.update(); |         speaker.update(); | ||||||
|         access.setLight( speaker.madeSound( 20 ) ? 0x3320fc : -1 ); |         access.setLight( speaker.madeSound() ? 0x3320fc : -1 ); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -10,6 +10,8 @@ import dan200.computercraft.shared.peripheral.speaker.UpgradeSpeakerPeripheral; | |||||||
| import net.minecraft.world.level.Level; | import net.minecraft.world.level.Level; | ||||||
| import net.minecraft.world.phys.Vec3; | import net.minecraft.world.phys.Vec3; | ||||||
| 
 | 
 | ||||||
|  | import javax.annotation.Nonnull; | ||||||
|  | 
 | ||||||
| public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral | public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral | ||||||
| { | { | ||||||
|     private Level world = null; |     private Level world = null; | ||||||
| @@ -27,6 +29,7 @@ public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral | |||||||
|         return world; |         return world; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Nonnull | ||||||
|     @Override |     @Override | ||||||
|     public Vec3 getPosition() |     public Vec3 getPosition() | ||||||
|     { |     { | ||||||
|   | |||||||
| @@ -43,6 +43,7 @@ public class TurtleSpeaker extends AbstractTurtleUpgrade | |||||||
|             return turtle.getLevel(); |             return turtle.getLevel(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         @Nonnull | ||||||
|         @Override |         @Override | ||||||
|         public Vec3 getPosition() |         public Vec3 getPosition() | ||||||
|         { |         { | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								src/main/resources/assets/computercraft/sounds.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/main/resources/assets/computercraft/sounds.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | { | ||||||
|  |     "speaker.dfpwm_fake_audio_should_not_be_played": { | ||||||
|  |         "sounds": [ | ||||||
|  |             { | ||||||
|  |                 "name": "computercraft:empty", | ||||||
|  |                 "stream": true | ||||||
|  |             } | ||||||
|  |         ] | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/main/resources/assets/computercraft/sounds/empty.ogg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/main/resources/assets/computercraft/sounds/empty.ogg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										13
									
								
								src/main/resources/computercraft.mixins.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/main/resources/computercraft.mixins.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | { | ||||||
|  |     "minVersion": "0.8", | ||||||
|  |     "required": true, | ||||||
|  |     "compatibilityLevel": "JAVA_8", | ||||||
|  |     "refmap": "computercraft.mixins.refmap.json", | ||||||
|  |     "package": "dan200.computercraft.mixin", | ||||||
|  |     "client": [ | ||||||
|  |         "BlockRenderDispatcherMixin" | ||||||
|  |     ], | ||||||
|  |     "injectors": { | ||||||
|  |         "defaultRequire": 1 | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -29,9 +29,9 @@ CHANNEL_REPEAT = 65533 | |||||||
| -- greater or equal to this limit wrap around to 0. | -- greater or equal to this limit wrap around to 0. | ||||||
| MAX_ID_CHANNELS = 65500 | MAX_ID_CHANNELS = 65500 | ||||||
|  |  | ||||||
| local tReceivedMessages = {} | local received_messages = {} | ||||||
| local tHostnames = {} | local hostnames = {} | ||||||
| local nClearTimer | local prune_received_timer | ||||||
|  |  | ||||||
| local function id_as_channel(id) | local function id_as_channel(id) | ||||||
|     return (id or os.getComputerID()) % MAX_ID_CHANNELS |     return (id or os.getComputerID()) % MAX_ID_CHANNELS | ||||||
| @@ -115,10 +115,10 @@ be @{rednet.open|opened} before sending is possible. | |||||||
| Assuming the target was in range and also had a correctly opened modem, it | Assuming the target was in range and also had a correctly opened modem, it | ||||||
| may then use @{rednet.receive} to collect the message. | may then use @{rednet.receive} to collect the message. | ||||||
|  |  | ||||||
| @tparam number nRecipient The ID of the receiving computer. | @tparam number recipient The ID of the receiving computer. | ||||||
| @param message The message to send. This should not contain coroutines or | @param message The message to send. This should not contain coroutines or | ||||||
| functions, as they will be converted to @{nil}. | functions, as they will be converted to @{nil}. | ||||||
| @tparam[opt] string sProtocol The "protocol" to send this message under. When | @tparam[opt] string protocol The "protocol" to send this message under. When | ||||||
| using @{rednet.receive} one can filter to only receive messages sent under a | using @{rednet.receive} one can filter to only receive messages sent under a | ||||||
| particular protocol. | particular protocol. | ||||||
| @treturn boolean If this message was successfully sent (i.e. if rednet is | @treturn boolean If this message was successfully sent (i.e. if rednet is | ||||||
| @@ -131,41 +131,41 @@ actually _received_. | |||||||
|  |  | ||||||
|     rednet.send(2, "Hello from rednet!") |     rednet.send(2, "Hello from rednet!") | ||||||
| ]] | ]] | ||||||
| function send(nRecipient, message, sProtocol) | function send(recipient, message, protocol) | ||||||
|     expect(1, nRecipient, "number") |     expect(1, recipient, "number") | ||||||
|     expect(3, sProtocol, "string", "nil") |     expect(3, protocol, "string", "nil") | ||||||
|     -- Generate a (probably) unique message ID |     -- Generate a (probably) unique message ID | ||||||
|     -- We could do other things to guarantee uniqueness, but we really don't need to |     -- We could do other things to guarantee uniqueness, but we really don't need to | ||||||
|     -- Store it to ensure we don't get our own messages back |     -- Store it to ensure we don't get our own messages back | ||||||
|     local nMessageID = math.random(1, 2147483647) |     local message_id = math.random(1, 2147483647) | ||||||
|     tReceivedMessages[nMessageID] = os.clock() + 9.5 |     received_messages[message_id] = os.clock() + 9.5 | ||||||
|     if not nClearTimer then nClearTimer = os.startTimer(10) end |     if not prune_received_timer then prune_received_timer = os.startTimer(10) end | ||||||
|  |  | ||||||
|     -- Create the message |     -- Create the message | ||||||
|     local nReplyChannel = id_as_channel() |     local reply_channel = id_as_channel() | ||||||
|     local tMessage = { |     local message_wrapper = { | ||||||
|         nMessageID = nMessageID, |         nMessageID = message_id, | ||||||
|         nRecipient = nRecipient, |         nRecipient = recipient, | ||||||
|         nSender = os.getComputerID(), |         nSender = os.getComputerID(), | ||||||
|         message = message, |         message = message, | ||||||
|         sProtocol = sProtocol, |         sProtocol = protocol, | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     local sent = false |     local sent = false | ||||||
|     if nRecipient == os.getComputerID() then |     if recipient == os.getComputerID() then | ||||||
|         -- Loopback to ourselves |         -- Loopback to ourselves | ||||||
|         os.queueEvent("rednet_message", os.getComputerID(), message, sProtocol) |         os.queueEvent("rednet_message", os.getComputerID(), message_wrapper, protocol) | ||||||
|         sent = true |         sent = true | ||||||
|     else |     else | ||||||
|         -- Send on all open modems, to the target and to repeaters |         -- Send on all open modems, to the target and to repeaters | ||||||
|         if nRecipient ~= CHANNEL_BROADCAST then |         if recipient ~= CHANNEL_BROADCAST then | ||||||
|             nRecipient = id_as_channel(nRecipient) |             recipient = id_as_channel(recipient) | ||||||
|         end |         end | ||||||
|  |  | ||||||
|         for _, sModem in ipairs(peripheral.getNames()) do |         for _, modem in ipairs(peripheral.getNames()) do | ||||||
|             if isOpen(sModem) then |             if isOpen(modem) then | ||||||
|                 peripheral.call(sModem, "transmit", nRecipient, nReplyChannel, tMessage) |                 peripheral.call(modem, "transmit", recipient, reply_channel, message_wrapper) | ||||||
|                 peripheral.call(sModem, "transmit", CHANNEL_REPEAT, nReplyChannel, tMessage) |                 peripheral.call(modem, "transmit", CHANNEL_REPEAT, reply_channel, message_wrapper) | ||||||
|                 sent = true |                 sent = true | ||||||
|             end |             end | ||||||
|         end |         end | ||||||
| @@ -179,23 +179,23 @@ end | |||||||
| -- | -- | ||||||
| -- @param message The message to send. This should not contain coroutines or | -- @param message The message to send. This should not contain coroutines or | ||||||
| -- functions, as they will be converted to @{nil}. | -- functions, as they will be converted to @{nil}. | ||||||
| -- @tparam[opt] string sProtocol The "protocol" to send this message under. When | -- @tparam[opt] string protocol The "protocol" to send this message under. When | ||||||
| -- using @{rednet.receive} one can filter to only receive messages sent under a | -- using @{rednet.receive} one can filter to only receive messages sent under a | ||||||
| -- particular protocol. | -- particular protocol. | ||||||
| -- @see rednet.receive | -- @see rednet.receive | ||||||
| -- @changed 1.6 Added protocol parameter. | -- @changed 1.6 Added protocol parameter. | ||||||
| function broadcast(message, sProtocol) | function broadcast(message, protocol) | ||||||
|     expect(2, sProtocol, "string", "nil") |     expect(2, protocol, "string", "nil") | ||||||
|     send(CHANNEL_BROADCAST, message, sProtocol) |     send(CHANNEL_BROADCAST, message, protocol) | ||||||
| end | end | ||||||
|  |  | ||||||
| --[[- Wait for a rednet message to be received, or until `nTimeout` seconds have | --[[- Wait for a rednet message to be received, or until `nTimeout` seconds have | ||||||
| elapsed. | elapsed. | ||||||
|  |  | ||||||
| @tparam[opt] string sProtocolFilter The protocol the received message must be | @tparam[opt] string protocol_filter The protocol the received message must be | ||||||
| sent with. If specified, any messages not sent under this protocol will be | sent with. If specified, any messages not sent under this protocol will be | ||||||
| discarded. | discarded. | ||||||
| @tparam[opt] number nTimeout The number of seconds to wait if no message is | @tparam[opt] number timeout The number of seconds to wait if no message is | ||||||
| received. | received. | ||||||
| @treturn[1] number The computer which sent this message | @treturn[1] number The computer which sent this message | ||||||
| @return[1] The received message | @return[1] The received message | ||||||
| @@ -227,34 +227,34 @@ received. | |||||||
|  |  | ||||||
|     print(message) |     print(message) | ||||||
| ]] | ]] | ||||||
| function receive(sProtocolFilter, nTimeout) | function receive(protocol_filter, timeout) | ||||||
|     -- The parameters used to be ( nTimeout ), detect this case for backwards compatibility |     -- The parameters used to be ( nTimeout ), detect this case for backwards compatibility | ||||||
|     if type(sProtocolFilter) == "number" and nTimeout == nil then |     if type(protocol_filter) == "number" and timeout == nil then | ||||||
|         sProtocolFilter, nTimeout = nil, sProtocolFilter |         protocol_filter, timeout = nil, protocol_filter | ||||||
|     end |     end | ||||||
|     expect(1, sProtocolFilter, "string", "nil") |     expect(1, protocol_filter, "string", "nil") | ||||||
|     expect(2, nTimeout, "number", "nil") |     expect(2, timeout, "number", "nil") | ||||||
|  |  | ||||||
|     -- Start the timer |     -- Start the timer | ||||||
|     local timer = nil |     local timer = nil | ||||||
|     local sFilter = nil |     local event_filter = nil | ||||||
|     if nTimeout then |     if timeout then | ||||||
|         timer = os.startTimer(nTimeout) |         timer = os.startTimer(timeout) | ||||||
|         sFilter = nil |         event_filter = nil | ||||||
|     else |     else | ||||||
|         sFilter = "rednet_message" |         event_filter = "rednet_message" | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     -- Wait for events |     -- Wait for events | ||||||
|     while true do |     while true do | ||||||
|         local sEvent, p1, p2, p3 = os.pullEvent(sFilter) |         local event, p1, p2, p3 = os.pullEvent(event_filter) | ||||||
|         if sEvent == "rednet_message" then |         if event == "rednet_message" then | ||||||
|             -- Return the first matching rednet_message |             -- Return the first matching rednet_message | ||||||
|             local nSenderID, message, sProtocol = p1, p2, p3 |             local sender_id, message, protocol = p1, p2, p3 | ||||||
|             if sProtocolFilter == nil or sProtocol == sProtocolFilter then |             if protocol_filter == nil or protocol == protocol_filter then | ||||||
|                 return nSenderID, message, sProtocol |                 return sender_id, message, protocol | ||||||
|             end |             end | ||||||
|         elseif sEvent == "timer" then |         elseif event == "timer" then | ||||||
|             -- Return nil if we timeout |             -- Return nil if we timeout | ||||||
|             if p1 == timer then |             if p1 == timer then | ||||||
|                 return nil |                 return nil | ||||||
| @@ -276,34 +276,34 @@ end | |||||||
| -- "registering" themselves before doing so (eg while offline or part of a | -- "registering" themselves before doing so (eg while offline or part of a | ||||||
| -- different network). | -- different network). | ||||||
| -- | -- | ||||||
| -- @tparam string sProtocol The protocol this computer provides. | -- @tparam string protocol The protocol this computer provides. | ||||||
| -- @tparam string sHostname The name this protocol exposes for the given protocol. | -- @tparam string hostname The name this protocol exposes for the given protocol. | ||||||
| -- @throws If trying to register a hostname which is reserved, or currently in use. | -- @throws If trying to register a hostname which is reserved, or currently in use. | ||||||
| -- @see rednet.unhost | -- @see rednet.unhost | ||||||
| -- @see rednet.lookup | -- @see rednet.lookup | ||||||
| -- @since 1.6 | -- @since 1.6 | ||||||
| function host(sProtocol, sHostname) | function host(protocol, hostname) | ||||||
|     expect(1, sProtocol, "string") |     expect(1, protocol, "string") | ||||||
|     expect(2, sHostname, "string") |     expect(2, hostname, "string") | ||||||
|     if sHostname == "localhost" then |     if hostname == "localhost" then | ||||||
|         error("Reserved hostname", 2) |         error("Reserved hostname", 2) | ||||||
|     end |     end | ||||||
|     if tHostnames[sProtocol] ~= sHostname then |     if hostnames[protocol] ~= hostname then | ||||||
|         if lookup(sProtocol, sHostname) ~= nil then |         if lookup(protocol, hostname) ~= nil then | ||||||
|             error("Hostname in use", 2) |             error("Hostname in use", 2) | ||||||
|         end |         end | ||||||
|         tHostnames[sProtocol] = sHostname |         hostnames[protocol] = hostname | ||||||
|     end |     end | ||||||
| end | end | ||||||
|  |  | ||||||
| --- Stop @{rednet.host|hosting} a specific protocol, meaning it will no longer | --- Stop @{rednet.host|hosting} a specific protocol, meaning it will no longer | ||||||
| -- respond to @{rednet.lookup} requests. | -- respond to @{rednet.lookup} requests. | ||||||
| -- | -- | ||||||
| -- @tparam string sProtocol The protocol to unregister your self from. | -- @tparam string protocol The protocol to unregister your self from. | ||||||
| -- @since 1.6 | -- @since 1.6 | ||||||
| function unhost(sProtocol) | function unhost(protocol) | ||||||
|     expect(1, sProtocol, "string") |     expect(1, protocol, "string") | ||||||
|     tHostnames[sProtocol] = nil |     hostnames[protocol] = nil | ||||||
| end | end | ||||||
|  |  | ||||||
| --- Search the local rednet network for systems @{rednet.host|hosting} the | --- Search the local rednet network for systems @{rednet.host|hosting} the | ||||||
| @@ -313,36 +313,36 @@ end | |||||||
| -- If a hostname is specified, only one ID will be returned (assuming an exact | -- If a hostname is specified, only one ID will be returned (assuming an exact | ||||||
| -- match is found). | -- match is found). | ||||||
| -- | -- | ||||||
| -- @tparam string sProtocol The protocol to search for. | -- @tparam string protocol The protocol to search for. | ||||||
| -- @tparam[opt] string sHostname The hostname to search for. | -- @tparam[opt] string hostname The hostname to search for. | ||||||
| -- | -- | ||||||
| -- @treturn[1] { number }|nil A list of computer IDs hosting the given | -- @treturn[1] { number }|nil A list of computer IDs hosting the given | ||||||
| -- protocol, or @{nil} if none exist. | -- protocol, or @{nil} if none exist. | ||||||
| -- @treturn[2] number|nil The computer ID with the provided hostname and protocol, | -- @treturn[2] number|nil The computer ID with the provided hostname and protocol, | ||||||
| -- or @{nil} if none exists. | -- or @{nil} if none exists. | ||||||
| -- @since 1.6 | -- @since 1.6 | ||||||
| function lookup(sProtocol, sHostname) | function lookup(protocol, hostname) | ||||||
|     expect(1, sProtocol, "string") |     expect(1, protocol, "string") | ||||||
|     expect(2, sHostname, "string", "nil") |     expect(2, hostname, "string", "nil") | ||||||
|  |  | ||||||
|     -- Build list of host IDs |     -- Build list of host IDs | ||||||
|     local tResults = nil |     local results = nil | ||||||
|     if sHostname == nil then |     if hostname == nil then | ||||||
|         tResults = {} |         results = {} | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     -- Check localhost first |     -- Check localhost first | ||||||
|     if tHostnames[sProtocol] then |     if hostnames[protocol] then | ||||||
|         if sHostname == nil then |         if hostname == nil then | ||||||
|             table.insert(tResults, os.getComputerID()) |             table.insert(results, os.getComputerID()) | ||||||
|         elseif sHostname == "localhost" or sHostname == tHostnames[sProtocol] then |         elseif hostname == "localhost" or hostname == hostnames[protocol] then | ||||||
|             return os.getComputerID() |             return os.getComputerID() | ||||||
|         end |         end | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     if not isOpen() then |     if not isOpen() then | ||||||
|         if tResults then |         if results then | ||||||
|             return table.unpack(tResults) |             return table.unpack(results) | ||||||
|         end |         end | ||||||
|         return nil |         return nil | ||||||
|     end |     end | ||||||
| @@ -350,8 +350,8 @@ function lookup(sProtocol, sHostname) | |||||||
|     -- Broadcast a lookup packet |     -- Broadcast a lookup packet | ||||||
|     broadcast({ |     broadcast({ | ||||||
|         sType = "lookup", |         sType = "lookup", | ||||||
|         sProtocol = sProtocol, |         sProtocol = protocol, | ||||||
|         sHostname = sHostname, |         sHostname = hostname, | ||||||
|     }, "dns") |     }, "dns") | ||||||
|  |  | ||||||
|     -- Start a timer |     -- Start a timer | ||||||
| @@ -362,30 +362,28 @@ function lookup(sProtocol, sHostname) | |||||||
|         local event, p1, p2, p3 = os.pullEvent() |         local event, p1, p2, p3 = os.pullEvent() | ||||||
|         if event == "rednet_message" then |         if event == "rednet_message" then | ||||||
|             -- Got a rednet message, check if it's the response to our request |             -- Got a rednet message, check if it's the response to our request | ||||||
|             local nSenderID, tMessage, sMessageProtocol = p1, p2, p3 |             local sender_id, message, message_protocol = p1, p2, p3 | ||||||
|             if sMessageProtocol == "dns" and type(tMessage) == "table" and tMessage.sType == "lookup response" then |             if message_protocol == "dns" and type(message) == "table" and message.sType == "lookup response" then | ||||||
|                 if tMessage.sProtocol == sProtocol then |                 if message.sProtocol == protocol then | ||||||
|                     if sHostname == nil then |                     if hostname == nil then | ||||||
|                         table.insert(tResults, nSenderID) |                         table.insert(results, sender_id) | ||||||
|                     elseif tMessage.sHostname == sHostname then |                     elseif message.sHostname == hostname then | ||||||
|                         return nSenderID |                         return sender_id | ||||||
|                     end |                     end | ||||||
|                 end |                 end | ||||||
|             end |             end | ||||||
|         else |         elseif event == "timer" and p1 == timer then | ||||||
|             -- Got a timer event, check it's the end of our timeout |             -- Got a timer event, check it's the end of our timeout | ||||||
|             if p1 == timer then |  | ||||||
|             break |             break | ||||||
|         end |         end | ||||||
|     end |     end | ||||||
|     end |     if results then | ||||||
|     if tResults then |         return table.unpack(results) | ||||||
|         return table.unpack(tResults) |  | ||||||
|     end |     end | ||||||
|     return nil |     return nil | ||||||
| end | end | ||||||
|  |  | ||||||
| local bRunning = false | local started = false | ||||||
|  |  | ||||||
| --- Listen for modem messages and converts them into rednet messages, which may | --- Listen for modem messages and converts them into rednet messages, which may | ||||||
| -- then be @{receive|received}. | -- then be @{receive|received}. | ||||||
| @@ -393,51 +391,51 @@ local bRunning = false | |||||||
| -- This is automatically started in the background on computer startup, and | -- This is automatically started in the background on computer startup, and | ||||||
| -- should not be called manually. | -- should not be called manually. | ||||||
| function run() | function run() | ||||||
|     if bRunning then |     if started then | ||||||
|         error("rednet is already running", 2) |         error("rednet is already running", 2) | ||||||
|     end |     end | ||||||
|     bRunning = true |     started = true | ||||||
|  |  | ||||||
|     while bRunning do |     while true do | ||||||
|         local sEvent, p1, p2, p3, p4 = os.pullEventRaw() |         local event, p1, p2, p3, p4 = os.pullEventRaw() | ||||||
|         if sEvent == "modem_message" then |         if event == "modem_message" then | ||||||
|             -- Got a modem message, process it and add it to the rednet event queue |             -- Got a modem message, process it and add it to the rednet event queue | ||||||
|             local sModem, nChannel, nReplyChannel, tMessage = p1, p2, p3, p4 |             local modem, channel, reply_channel, message = p1, p2, p3, p4 | ||||||
|             if nChannel == id_as_channel() or nChannel == CHANNEL_BROADCAST then |             if channel == id_as_channel() or channel == CHANNEL_BROADCAST then | ||||||
|                 if type(tMessage) == "table" and type(tMessage.nMessageID) == "number" |                 if type(message) == "table" and type(message.nMessageID) == "number" | ||||||
|                     and tMessage.nMessageID == tMessage.nMessageID and not tReceivedMessages[tMessage.nMessageID] |                     and message.nMessageID == message.nMessageID and not received_messages[message.nMessageID] | ||||||
|                     and ((tMessage.nRecipient and tMessage.nRecipient == os.getComputerID()) or nChannel == CHANNEL_BROADCAST) |                     and ((message.nRecipient and message.nRecipient == os.getComputerID()) or channel == CHANNEL_BROADCAST) | ||||||
|                     and isOpen(sModem) |                     and isOpen(modem) | ||||||
|                 then |                 then | ||||||
|                     tReceivedMessages[tMessage.nMessageID] = os.clock() + 9.5 |                     received_messages[message.nMessageID] = os.clock() + 9.5 | ||||||
|                     if not nClearTimer then nClearTimer = os.startTimer(10) end |                     if not prune_received_timer then prune_received_timer = os.startTimer(10) end | ||||||
|                     os.queueEvent("rednet_message", tMessage.nSender or nReplyChannel, tMessage.message, tMessage.sProtocol) |                     os.queueEvent("rednet_message", message.nSender or reply_channel, message.message, message.sProtocol) | ||||||
|                 end |                 end | ||||||
|             end |             end | ||||||
|  |  | ||||||
|         elseif sEvent == "rednet_message" then |         elseif event == "rednet_message" then | ||||||
|             -- Got a rednet message (queued from above), respond to dns lookup |             -- Got a rednet message (queued from above), respond to dns lookup | ||||||
|             local nSenderID, tMessage, sProtocol = p1, p2, p3 |             local sender, message, protocol = p1, p2, p3 | ||||||
|             if sProtocol == "dns" and type(tMessage) == "table" and tMessage.sType == "lookup" then |             if protocol == "dns" and type(message) == "table" and message.sType == "lookup" then | ||||||
|                 local sHostname = tHostnames[tMessage.sProtocol] |                 local hostname = hostnames[message.sProtocol] | ||||||
|                 if sHostname ~= nil and (tMessage.sHostname == nil or tMessage.sHostname == sHostname) then |                 if hostname ~= nil and (message.sHostname == nil or message.sHostname == hostname) then | ||||||
|                     rednet.send(nSenderID, { |                     send(sender, { | ||||||
|                         sType = "lookup response", |                         sType = "lookup response", | ||||||
|                         sHostname = sHostname, |                         sHostname = hostname, | ||||||
|                         sProtocol = tMessage.sProtocol, |                         sProtocol = message.sProtocol, | ||||||
|                     }, "dns") |                     }, "dns") | ||||||
|                 end |                 end | ||||||
|             end |             end | ||||||
|  |  | ||||||
|         elseif sEvent == "timer" and p1 == nClearTimer then |         elseif event == "timer" and p1 == prune_received_timer then | ||||||
|             -- Got a timer event, use it to clear the event queue |             -- Got a timer event, use it to prune the set of received messages | ||||||
|             nClearTimer = nil |             prune_received_timer = nil | ||||||
|             local nNow, bHasMore = os.clock(), nil |             local now, has_more = os.clock(), nil | ||||||
|             for nMessageID, nDeadline in pairs(tReceivedMessages) do |             for message_id, deadline in pairs(received_messages) do | ||||||
|                 if nDeadline <= nNow then tReceivedMessages[nMessageID] = nil |                 if deadline <= now then received_messages[message_id] = nil | ||||||
|                 else bHasMore = true end |                 else has_more = true end | ||||||
|             end |             end | ||||||
|             nClearTimer = bHasMore and os.startTimer(10) |             prune_received_timer = has_more and os.startTimer(10) | ||||||
|         end |         end | ||||||
|     end |     end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -59,7 +59,8 @@ end | |||||||
| -- @treturn Redirect The current terminal redirect | -- @treturn Redirect The current terminal redirect | ||||||
| -- @since 1.6 | -- @since 1.6 | ||||||
| -- @usage | -- @usage | ||||||
| -- Create a new @{window} which draws to the current redirect target | -- Create a new @{window} which draws to the current redirect target. | ||||||
|  | -- | ||||||
| --     window.create(term.current(), 1, 1, 10, 10) | --     window.create(term.current(), 1, 1, 10, 10) | ||||||
| term.current = function() | term.current = function() | ||||||
|     return redirectTarget |     return redirectTarget | ||||||
|   | |||||||
| @@ -0,0 +1,5 @@ | |||||||
|  | The speaker program plays audio files using speakers attached to this computer. | ||||||
|  | 
 | ||||||
|  | ## Examples: | ||||||
|  | - `speaker play example.dfpwm left` plays the "example.dfpwm" audio file using the speaker on the left of the computer. | ||||||
|  | - `speaker stop` stops any currently playing audio. | ||||||
| @@ -0,0 +1,228 @@ | |||||||
|  | --[[- | ||||||
|  | Provides utilities for converting between streams of DFPWM audio data and a list of amplitudes. | ||||||
|  |  | ||||||
|  | DFPWM (Dynamic Filter Pulse Width Modulation) is an audio codec designed by GreaseMonkey. It's a relatively compact | ||||||
|  | format compared to raw PCM data, only using 1 bit per sample, but is simple enough to simple enough to encode and decode | ||||||
|  | in real time. | ||||||
|  |  | ||||||
|  | Typically DFPWM audio is read from @{fs.BinaryReadHandle|the filesystem} or a @{http.Response|a web request} as a | ||||||
|  | string, and converted a format suitable for @{speaker.playAudio}. | ||||||
|  |  | ||||||
|  | ## Encoding and decoding files | ||||||
|  | This modules exposes two key functions, @{make_decoder} and @{make_encoder}, which construct a new decoder or encoder. | ||||||
|  | The returned encoder/decoder is itself a function, which converts between the two kinds of data. | ||||||
|  |  | ||||||
|  | These encoders and decoders have lots of hidden state, so you should be careful to use the same encoder or decoder for | ||||||
|  | a specific audio stream. Typically you will want to create a decoder for each stream of audio you read, and an encoder | ||||||
|  | for each one you write. | ||||||
|  |  | ||||||
|  | ## Converting audio to DFPWM | ||||||
|  | DFPWM is not a popular file format and so standard audio processing tools will not have an option to export to it. | ||||||
|  | Instead, you can convert audio files online using [music.madefor.cc] or with the [LionRay Wav Converter][LionRay] Java | ||||||
|  | application. | ||||||
|  |  | ||||||
|  | [music.madefor.cc]: https://music.madefor.cc/ "DFPWM audio converter for Computronics and CC: Tweaked" | ||||||
|  | [LionRay]: https://github.com/gamax92/LionRay/ "LionRay Wav Converter " | ||||||
|  |  | ||||||
|  | @see guide!speaker_audio Gives a more general introduction to audio processing and the speaker. | ||||||
|  | @see speaker.playAudio To play the decoded audio data. | ||||||
|  | @usage Reads "data/example.dfpwm" in chunks, decodes them and then doubles the speed of the audio. The resulting audio | ||||||
|  | is then re-encoded and saved to "speedy.dfpwm". This processed audio can then be played with the `speaker` program. | ||||||
|  |  | ||||||
|  | ```lua | ||||||
|  | local dfpwm = require("cc.audio.dfpwm") | ||||||
|  |  | ||||||
|  | local encoder = dfpwm.make_encoder() | ||||||
|  | local decoder = dfpwm.make_decoder() | ||||||
|  |  | ||||||
|  | local out = fs.open("speedy.dfpwm", "wb") | ||||||
|  | for input in io.lines("my_audio_track.dfpwm", 16 * 1024 * 2) do | ||||||
|  |   local decoded = decoder(input) | ||||||
|  |   local output = {} | ||||||
|  |  | ||||||
|  |   -- Read two samples at once and take the average. | ||||||
|  |   for i = 1, #decoded, 2 do | ||||||
|  |     local value_1, value_2 = decoded[i], decoded[i + 1] | ||||||
|  |     output[(i + 1) / 2] = (value_1 + value_2) / 2 | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   out.write(encoder(output)) | ||||||
|  |  | ||||||
|  |   sleep(0) -- This program takes a while to run, so we need to make sure we yield. | ||||||
|  | end | ||||||
|  | out.close() | ||||||
|  | ``` | ||||||
|  | ]] | ||||||
|  |  | ||||||
|  | local expect = require "cc.expect".expect | ||||||
|  |  | ||||||
|  | local char, byte, floor, band, rshift = string.char, string.byte, math.floor, bit32.band, bit32.arshift | ||||||
|  |  | ||||||
|  | local PREC = 10 | ||||||
|  | local PREC_POW = 2 ^ PREC | ||||||
|  | local PREC_POW_HALF = 2 ^ (PREC - 1) | ||||||
|  | local STRENGTH_MIN = 2 ^ (PREC - 8 + 1) | ||||||
|  |  | ||||||
|  | local function make_predictor() | ||||||
|  |     local charge, strength, previous_bit = 0, 0, false | ||||||
|  |  | ||||||
|  |     return function(current_bit) | ||||||
|  |         local target = current_bit and 127 or -128 | ||||||
|  |  | ||||||
|  |         local next_charge = charge + floor((strength * (target - charge) + PREC_POW_HALF) / PREC_POW) | ||||||
|  |         if next_charge == charge and next_charge ~= target then | ||||||
|  |             next_charge = next_charge + (current_bit and 1 or -1) | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         local z = current_bit == previous_bit and PREC_POW - 1 or 0 | ||||||
|  |         local next_strength = strength | ||||||
|  |         if next_strength ~= z then next_strength = next_strength + (current_bit == previous_bit and 1 or -1) end | ||||||
|  |         if next_strength < STRENGTH_MIN then next_strength = STRENGTH_MIN end | ||||||
|  |  | ||||||
|  |         charge, strength, previous_bit = next_charge, next_strength, current_bit | ||||||
|  |         return charge | ||||||
|  |     end | ||||||
|  | end | ||||||
|  |  | ||||||
|  | --[[- Create a new encoder for converting PCM audio data into DFPWM. | ||||||
|  |  | ||||||
|  | The returned encoder is itself a function. This function accepts a table of amplitude data between -128 and 127 and | ||||||
|  | returns the encoded DFPWM data. | ||||||
|  |  | ||||||
|  | :::caution Reusing encoders | ||||||
|  | Encoders have lots of internal state which tracks the state of the current stream. If you reuse an encoder for multiple | ||||||
|  | streams, or use different encoders for the same stream, the resulting audio may not sound correct. | ||||||
|  | ::: | ||||||
|  |  | ||||||
|  | @treturn function(pcm: { number... }):string The encoder function | ||||||
|  | @see encode A helper function for encoding an entire file of audio at once. | ||||||
|  | ]] | ||||||
|  | local function make_encoder() | ||||||
|  |     local predictor = make_predictor() | ||||||
|  |     local previous_charge = 0 | ||||||
|  |  | ||||||
|  |     return function(input) | ||||||
|  |         expect(1, input, "table") | ||||||
|  |  | ||||||
|  |         local output, output_n = {}, 0 | ||||||
|  |         for i = 1, #input, 8 do | ||||||
|  |             local this_byte = 0 | ||||||
|  |             for j = 0, 7 do | ||||||
|  |                 local inp_charge = floor(input[i + j] or 0) | ||||||
|  |                 if inp_charge > 127 or inp_charge < -128 then | ||||||
|  |                     error(("Amplitude at position %d was %d, but should be between -128 and 127"):format(i + j, inp_charge), 2) | ||||||
|  |                 end | ||||||
|  |  | ||||||
|  |                 local current_bit = inp_charge > previous_charge or (inp_charge == previous_charge and inp_charge == 127) | ||||||
|  |                 this_byte = floor(this_byte / 2) + (current_bit and 128 or 0) | ||||||
|  |  | ||||||
|  |                 previous_charge = predictor(current_bit) | ||||||
|  |             end | ||||||
|  |  | ||||||
|  |             output_n = output_n + 1 | ||||||
|  |             output[output_n] = char(this_byte) | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         return table.concat(output, "", 1, output_n) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  |  | ||||||
|  | --[[- Create a new decoder for converting DFPWM into PCM audio data. | ||||||
|  |  | ||||||
|  | The returned decoder is itself a function. This function accepts a string and returns a table of amplitudes, each value | ||||||
|  | between -128 and 127. | ||||||
|  |  | ||||||
|  | :::caution Reusing decoders | ||||||
|  | Decoders have lots of internal state which tracks the state of the current stream. If you reuse an decoder for multiple | ||||||
|  | streams, or use different decoders for the same stream, the resulting audio may not sound correct. | ||||||
|  | ::: | ||||||
|  |  | ||||||
|  | @treturn function(dfpwm: string):{ number... } The encoder function | ||||||
|  | @see decode A helper function for decoding an entire file of audio at once. | ||||||
|  |  | ||||||
|  | @usage Reads "data/example.dfpwm" in blocks of 16KiB (the speaker can accept a maximum of 128×1024 samples), decodes | ||||||
|  | them and then plays them through the speaker. | ||||||
|  |  | ||||||
|  | ```lua | ||||||
|  | local dfpwm = require "cc.audio.dfpwm" | ||||||
|  | local speaker = peripheral.find("speaker") | ||||||
|  |  | ||||||
|  | local decoder = dfpwm.make_decoder() | ||||||
|  | for input in io.lines("data/example.dfpwm", 16 * 1024 * 2) do | ||||||
|  |   local decoded = decoder(input) | ||||||
|  |   while not speaker.playAudio(output) do | ||||||
|  |     os.pullEvent("speaker_audio_empty") | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | ``` | ||||||
|  | ]] | ||||||
|  | local function make_decoder() | ||||||
|  |     local predictor = make_predictor() | ||||||
|  |     local low_pass_charge = 0 | ||||||
|  |     local previous_charge, previous_bit = 0, false | ||||||
|  |  | ||||||
|  |     return function (input, output) | ||||||
|  |         expect(1, input, "string") | ||||||
|  |  | ||||||
|  |         local output, output_n = {}, 0 | ||||||
|  |         for i = 1, #input do | ||||||
|  |             local input_byte = byte(input, i) | ||||||
|  |             for _ = 1, 8 do | ||||||
|  |                 local current_bit = band(input_byte, 1) ~= 0 | ||||||
|  |                 local charge = predictor(current_bit) | ||||||
|  |  | ||||||
|  |                 local antijerk = charge | ||||||
|  |                 if current_bit ~= previous_bit then | ||||||
|  |                     antijerk = floor((charge + previous_charge + 1) / 2) | ||||||
|  |                 end | ||||||
|  |  | ||||||
|  |                 previous_charge, previous_bit = charge, current_bit | ||||||
|  |  | ||||||
|  |                 low_pass_charge = low_pass_charge + floor(((antijerk - low_pass_charge) * 140 + 0x80) / 256) | ||||||
|  |  | ||||||
|  |                 output_n = output_n + 1 | ||||||
|  |                 output[output_n] = low_pass_charge | ||||||
|  |  | ||||||
|  |                 input_byte = rshift(input_byte, 1) | ||||||
|  |             end | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         return output | ||||||
|  |     end | ||||||
|  | end | ||||||
|  |  | ||||||
|  | --[[- A convenience function for decoding a complete file of audio at once. | ||||||
|  |  | ||||||
|  | This should only be used for short files. For larger files, one should read the file in chunks and process it using | ||||||
|  | @{make_decoder}. | ||||||
|  |  | ||||||
|  | @tparam string input The DFPWM data to convert. | ||||||
|  | @treturn { number... } The produced amplitude data. | ||||||
|  | @see make_decoder | ||||||
|  | ]] | ||||||
|  | local function decode(input) | ||||||
|  |     expect(1, input, "string") | ||||||
|  |     return make_decoder()(input) | ||||||
|  | end | ||||||
|  |  | ||||||
|  | --[[- A convenience function for encoding a complete file of audio at once. | ||||||
|  |  | ||||||
|  | This should only be used for complete pieces of audio. If you are writing writing multiple chunks to the same place, | ||||||
|  | you should use an encoder returned by @{make_encoder} instead. | ||||||
|  |  | ||||||
|  | @tparam { number... } input The table of amplitude data. | ||||||
|  | @treturn string The encoded DFPWM data. | ||||||
|  | @see make_encoder | ||||||
|  | ]] | ||||||
|  | local function encode(input) | ||||||
|  |     expect(1, input, "table") | ||||||
|  |     return make_encoder()(input) | ||||||
|  | end | ||||||
|  |  | ||||||
|  | return { | ||||||
|  |     make_encoder = make_encoder, | ||||||
|  |     encode = encode, | ||||||
|  |  | ||||||
|  |     make_decoder = make_decoder, | ||||||
|  |     decode = decode, | ||||||
|  | } | ||||||
| @@ -0,0 +1,62 @@ | |||||||
|  | local function get_speakers(name) | ||||||
|  |     if name then | ||||||
|  |         local speaker = peripheral.wrap(name) | ||||||
|  |         if speaker == nil then | ||||||
|  |             error(("Speaker %q does not exist"):format(name), 0) | ||||||
|  |             return | ||||||
|  |         elseif not peripheral.hasType(name, "speaker") then | ||||||
|  |             error(("%q is not a speaker"):format(name), 0) | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         return { speaker } | ||||||
|  |     else | ||||||
|  |         local speakers = { peripheral.find("speaker") } | ||||||
|  |         if #speakers == 0 then | ||||||
|  |             error("No speakers attached", 0) | ||||||
|  |         end | ||||||
|  |         return speakers | ||||||
|  |     end | ||||||
|  | end | ||||||
|  |  | ||||||
|  |  | ||||||
|  | local cmd = ... | ||||||
|  | if cmd == "stop" then | ||||||
|  |     local _, name = ... | ||||||
|  |     for _, speaker in pairs(get_speakers(name)) do speaker.stop() end | ||||||
|  | elseif cmd == "play" then | ||||||
|  |     local _, file, name = ... | ||||||
|  |     local speaker = get_speakers(name)[1] | ||||||
|  |  | ||||||
|  |     local handle, err | ||||||
|  |     if http and file:match("^https?://") then | ||||||
|  |         print("Downloading...") | ||||||
|  |         handle, err = http.get{ url = file, binary = true } | ||||||
|  |     else | ||||||
|  |         handle, err = fs.open(file, "rb") | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     if not handle then | ||||||
|  |         printError("Could not play audio:") | ||||||
|  |         error(err, 0) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     print("Playing " .. file) | ||||||
|  |  | ||||||
|  |     local decoder = require "cc.audio.dfpwm".make_decoder() | ||||||
|  |     while true do | ||||||
|  |         local chunk = handle.read(16 * 1024) | ||||||
|  |         if not chunk then break end | ||||||
|  |  | ||||||
|  |         local buffer = decoder(chunk) | ||||||
|  |         while not speaker.playAudio(buffer) do | ||||||
|  |             os.pullEvent("speaker_audio_empty") | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     handle.close() | ||||||
|  | else | ||||||
|  |     local programName = arg[0] or fs.getName(shell.getRunningProgram()) | ||||||
|  |     print("Usage:") | ||||||
|  |     print(programName .. " play <file or url> [speaker]") | ||||||
|  |     print(programName .. " stop [speaker]") | ||||||
|  | end | ||||||
| @@ -14,10 +14,6 @@ else | |||||||
|     print(#tModems .. " modems found.") |     print(#tModems .. " modems found.") | ||||||
| end | end | ||||||
|  |  | ||||||
| local function idAsChannel(id) |  | ||||||
|     return (id or os.getComputerID()) % rednet.MAX_ID_CHANNELS |  | ||||||
| end |  | ||||||
|  |  | ||||||
| local function open(nChannel) | local function open(nChannel) | ||||||
|     for n = 1, #tModems do |     for n = 1, #tModems do | ||||||
|         local sModem = tModems[n] |         local sModem = tModems[n] | ||||||
| @@ -53,11 +49,16 @@ local ok, error = pcall(function() | |||||||
|                         tReceivedMessages[tMessage.nMessageID] = true |                         tReceivedMessages[tMessage.nMessageID] = true | ||||||
|                         tReceivedMessageTimeouts[os.startTimer(30)] = tMessage.nMessageID |                         tReceivedMessageTimeouts[os.startTimer(30)] = tMessage.nMessageID | ||||||
|  |  | ||||||
|  |                         local recipient_channel = tMessage.nRecipient | ||||||
|  |                         if tMessage.nRecipient ~= rednet.CHANNEL_BROADCAST then | ||||||
|  |                             recipient_channel = recipient_channel % rednet.MAX_ID_CHANNELS | ||||||
|  |                         end | ||||||
|  |  | ||||||
|                         -- Send on all other open modems, to the target and to other repeaters |                         -- Send on all other open modems, to the target and to other repeaters | ||||||
|                         for n = 1, #tModems do |                         for n = 1, #tModems do | ||||||
|                             local sOtherModem = tModems[n] |                             local sOtherModem = tModems[n] | ||||||
|                             peripheral.call(sOtherModem, "transmit", rednet.CHANNEL_REPEAT, nReplyChannel, tMessage) |                             peripheral.call(sOtherModem, "transmit", rednet.CHANNEL_REPEAT, nReplyChannel, tMessage) | ||||||
|                             peripheral.call(sOtherModem, "transmit", idAsChannel(tMessage.nRecipient), nReplyChannel, tMessage) |                             peripheral.call(sOtherModem, "transmit", recipient_channel, nReplyChannel, tMessage) | ||||||
|                         end |                         end | ||||||
|  |  | ||||||
|                         -- Log the event |                         -- Log the event | ||||||
|   | |||||||
| @@ -115,6 +115,18 @@ shell.setCompletionFunction("rom/programs/fun/dj.lua", completion.build( | |||||||
|     { completion.choice, { "play", "play ", "stop " } }, |     { completion.choice, { "play", "play ", "stop " } }, | ||||||
|     completion.peripheral |     completion.peripheral | ||||||
| )) | )) | ||||||
|  | shell.setCompletionFunction("rom/programs/fun/speaker.lua", completion.build( | ||||||
|  |     { completion.choice, { "play ", "stop " } }, | ||||||
|  |     function(shell, text, previous) | ||||||
|  |         if previous[2] == "play" then return completion.file(shell, text, previous, true) | ||||||
|  |         elseif previous[2] == "stop" then return completion.peripheral(shell, text, previous, false) | ||||||
|  |         end | ||||||
|  |     end, | ||||||
|  |     function(shell, text, previous) | ||||||
|  |         if previous[2] == "play" then return completion.peripheral(shell, text, previous, false) | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  | )) | ||||||
| shell.setCompletionFunction("rom/programs/fun/advanced/paint.lua", completion.build(completion.file)) | shell.setCompletionFunction("rom/programs/fun/advanced/paint.lua", completion.build(completion.file)) | ||||||
| shell.setCompletionFunction("rom/programs/http/pastebin.lua", completion.build( | shell.setCompletionFunction("rom/programs/http/pastebin.lua", completion.build( | ||||||
|     { completion.choice, { "put ", "get ", "run " } }, |     { completion.choice, { "put ", "get ", "run " } }, | ||||||
|   | |||||||
| @@ -0,0 +1,39 @@ | |||||||
|  | /* | ||||||
|  |  * This file is part of ComputerCraft - http://www.computercraft.info | ||||||
|  |  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||||
|  |  * Send enquiries to dratcliffe@gmail.com | ||||||
|  |  */ | ||||||
|  | package dan200.computercraft.shared.peripheral.speaker; | ||||||
|  | 
 | ||||||
|  | import dan200.computercraft.api.lua.LuaException; | ||||||
|  | import dan200.computercraft.api.lua.ObjectLuaTable; | ||||||
|  | import org.junit.jupiter.api.Test; | ||||||
|  | 
 | ||||||
|  | import java.nio.ByteBuffer; | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.Optional; | ||||||
|  | 
 | ||||||
|  | import static org.junit.jupiter.api.Assertions.assertArrayEquals; | ||||||
|  | 
 | ||||||
|  | class DfpwmStateTest | ||||||
|  | { | ||||||
|  |     @Test | ||||||
|  |     public void testEncoder() throws LuaException | ||||||
|  |     { | ||||||
|  |         int[] input = new int[] { 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -6, -6, -6, -7, -7, -7, -7, -7, -7, -7, -7, -7, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -7, -7, -7, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -5, -5, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3 }; | ||||||
|  |         Map<Object, Object> inputTbl = new HashMap<>(); | ||||||
|  |         for( int i = 0; i < input.length; i++ ) inputTbl.put( (double) (i + 1), input[i] ); | ||||||
|  | 
 | ||||||
|  |         DfpwmState state = new DfpwmState(); | ||||||
|  |         state.pushBuffer( new ObjectLuaTable( inputTbl ), input.length, Optional.empty() ); | ||||||
|  |         ByteBuffer result = state.pullPending( 0 ); | ||||||
|  |         byte[] contents = new byte[result.remaining()]; | ||||||
|  |         result.get( contents ); | ||||||
|  | 
 | ||||||
|  |         assertArrayEquals( | ||||||
|  |             new byte[] { 87, 74, 42, -91, -92, -108, 84, -87, -86, 86, -83, 90, -83, -43, 90, -85, -42, 106, -43, -86, 106, -107, 42, -107, 74, -87, 74, -91, 74, -91, -86, -86, 106, 85, 107, -83, 106, -83, -83, 86, -75, -86, 42, 85, -107, 82, 41, -91, 82, 74, 41, -107, -86, -44, -86, 86, -75, 106, -83, -75, -86, -75, 90, -83, -86, -86, -86, 82, -91, 74, -107, -86, 82, -87, 82, 85, 85, 85, -83, 86, -75, -86, -43, 90, -83, 90, 85, 85, -107, 42, -91, 82, -86, 82, 74, 41, 85, -87, -86, -86, 106, -75, 90, -83, 86, -85, 106, -43, 106, 85, 85, 85, 85, -107, 42, 85, -86, 42, -107, -86, -86, -86, -86, 106, -75, -86, 86, -85 }, | ||||||
|  |             contents | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -189,13 +189,18 @@ end | |||||||
| local expect_mt = {} | local expect_mt = {} | ||||||
| expect_mt.__index = expect_mt | expect_mt.__index = expect_mt | ||||||
|  |  | ||||||
|  | function expect_mt:_fail(message) | ||||||
|  |     if self._extra then message = self._extra .. "\n" .. message end | ||||||
|  |     fail(message) | ||||||
|  | end | ||||||
|  |  | ||||||
| --- Assert that this expectation has the provided value | --- Assert that this expectation has the provided value | ||||||
| -- | -- | ||||||
| -- @param value The value to require this expectation to be equal to | -- @param value The value to require this expectation to be equal to | ||||||
| -- @throws If the values are not equal | -- @throws If the values are not equal | ||||||
| function expect_mt:equals(value) | function expect_mt:equals(value) | ||||||
|     if value ~= self.value then |     if value ~= self.value then | ||||||
|         fail(("Expected %s\n but got %s"):format(format(value), format(self.value))) |         self:_fail(("Expected %s\n but got %s"):format(format(value), format(self.value))) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     return self |     return self | ||||||
| @@ -209,7 +214,7 @@ expect_mt.eq = expect_mt.equals | |||||||
| -- @throws If the values are equal | -- @throws If the values are equal | ||||||
| function expect_mt:not_equals(value) | function expect_mt:not_equals(value) | ||||||
|     if value == self.value then |     if value == self.value then | ||||||
|         fail(("Expected any value but %s"):format(format(value))) |         self:_fail(("Expected any value but %s"):format(format(value))) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     return self |     return self | ||||||
| @@ -224,7 +229,7 @@ expect_mt.ne = expect_mt.not_equals | |||||||
| function expect_mt:type(exp_type) | function expect_mt:type(exp_type) | ||||||
|     local actual_type = type(self.value) |     local actual_type = type(self.value) | ||||||
|     if exp_type ~= actual_type then |     if exp_type ~= actual_type then | ||||||
|         fail(("Expected value of type %s\nbut got %s"):format(exp_type, actual_type)) |         self:_fail(("Expected value of type %s\nbut got %s"):format(exp_type, actual_type)) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     return self |     return self | ||||||
| @@ -273,7 +278,7 @@ end | |||||||
| -- @throws If they are not equivalent | -- @throws If they are not equivalent | ||||||
| function expect_mt:same(value) | function expect_mt:same(value) | ||||||
|     if not matches({}, true, self.value, value) then |     if not matches({}, true, self.value, value) then | ||||||
|         fail(("Expected %s\nbut got %s"):format(format(value), format(self.value))) |         self:_fail(("Expected %s\nbut got %s"):format(format(value), format(self.value))) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     return self |     return self | ||||||
| @@ -286,7 +291,7 @@ end | |||||||
| -- @throws If this does not match the provided value | -- @throws If this does not match the provided value | ||||||
| function expect_mt:matches(value) | function expect_mt:matches(value) | ||||||
|     if not matches({}, false, value, self.value) then |     if not matches({}, false, value, self.value) then | ||||||
|         fail(("Expected %s\nto match %s"):format(format(self.value), format(value))) |         self:_fail(("Expected %s\nto match %s"):format(format(self.value), format(value))) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     return self |     return self | ||||||
| @@ -299,19 +304,19 @@ end | |||||||
| -- @throws If this function was not called the expected number of times. | -- @throws If this function was not called the expected number of times. | ||||||
| function expect_mt:called(times) | function expect_mt:called(times) | ||||||
|     if getmetatable(self.value) ~= stub_mt or self.value.arguments == nil then |     if getmetatable(self.value) ~= stub_mt or self.value.arguments == nil then | ||||||
|         fail(("Expected stubbed function, got %s"):format(type(self.value))) |         self:_fail(("Expected stubbed function, got %s"):format(type(self.value))) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     local called = #self.value.arguments |     local called = #self.value.arguments | ||||||
|  |  | ||||||
|     if times == nil then |     if times == nil then | ||||||
|         if called == 0 then |         if called == 0 then | ||||||
|             fail("Expected stub to be called\nbut it was not.") |             self:_fail("Expected stub to be called\nbut it was not.") | ||||||
|         end |         end | ||||||
|     else |     else | ||||||
|         check('stub', 1, 'number', times) |         check('stub', 1, 'number', times) | ||||||
|         if called ~= times then |         if called ~= times then | ||||||
|             fail(("Expected stub to be called %d times\nbut was called %d times."):format(times, called)) |             self:_fail(("Expected stub to be called %d times\nbut was called %d times."):format(times, called)) | ||||||
|         end |         end | ||||||
|     end |     end | ||||||
|  |  | ||||||
| @@ -320,7 +325,7 @@ end | |||||||
|  |  | ||||||
| local function called_with_check(eq, self, ...) | local function called_with_check(eq, self, ...) | ||||||
|     if getmetatable(self.value) ~= stub_mt or self.value.arguments == nil then |     if getmetatable(self.value) ~= stub_mt or self.value.arguments == nil then | ||||||
|         fail(("Expected stubbed function, got %s"):format(type(self.value))) |         self:_fail(("Expected stubbed function, got %s"):format(type(self.value))) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     local exp_args = table.pack(...) |     local exp_args = table.pack(...) | ||||||
| @@ -331,14 +336,14 @@ local function called_with_check(eq, self, ...) | |||||||
|  |  | ||||||
|     local head = ("Expected stub to be called with %s\nbut was"):format(format(exp_args)) |     local head = ("Expected stub to be called with %s\nbut was"):format(format(exp_args)) | ||||||
|     if #actual_args == 0 then |     if #actual_args == 0 then | ||||||
|         fail(head .. " not called at all") |         self:_fail(head .. " not called at all") | ||||||
|     elseif #actual_args == 1 then |     elseif #actual_args == 1 then | ||||||
|         fail(("%s called with %s."):format(head, format(actual_args[1]))) |         self:_fail(("%s called with %s."):format(head, format(actual_args[1]))) | ||||||
|     else |     else | ||||||
|         local lines = { head .. " called with:" } |         local lines = { head .. " called with:" } | ||||||
|         for i = 1, #actual_args do lines[i + 1] = " - " .. format(actual_args[i]) end |         for i = 1, #actual_args do lines[i + 1] = " - " .. format(actual_args[i]) end | ||||||
|  |  | ||||||
|         fail(table.concat(lines, "\n")) |         self:_fail(table.concat(lines, "\n")) | ||||||
|     end |     end | ||||||
| end | end | ||||||
|  |  | ||||||
| @@ -363,15 +368,24 @@ end | |||||||
| function expect_mt:str_match(pattern) | function expect_mt:str_match(pattern) | ||||||
|     local actual_type = type(self.value) |     local actual_type = type(self.value) | ||||||
|     if actual_type ~= "string" then |     if actual_type ~= "string" then | ||||||
|         fail(("Expected value of type string\nbut got %s"):format(actual_type)) |         self:_fail(("Expected value of type string\nbut got %s"):format(actual_type)) | ||||||
|     end |     end | ||||||
|     if not self.value:find(pattern) then |     if not self.value:find(pattern) then | ||||||
|         fail(("Expected %q\n to match pattern %q"):format(self.value, pattern)) |         self:_fail(("Expected %q\n to match pattern %q"):format(self.value, pattern)) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     return self |     return self | ||||||
| end | end | ||||||
|  |  | ||||||
|  | --- Add extra information to this error message. | ||||||
|  | -- | ||||||
|  | -- @tparam string message Additional message to prepend in the case of failures. | ||||||
|  | -- @return The current | ||||||
|  | function expect_mt:describe(message) | ||||||
|  |     self._extra = tostring(message) | ||||||
|  |     return self | ||||||
|  | end | ||||||
|  |  | ||||||
| local expect = {} | local expect = {} | ||||||
| setmetatable(expect, expect) | setmetatable(expect, expect) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -88,6 +88,7 @@ describe("The rednet library", function() | |||||||
|         local fake_computer = require "support.fake_computer" |         local fake_computer = require "support.fake_computer" | ||||||
|         local debugx = require "support.debug_ext" |         local debugx = require "support.debug_ext" | ||||||
|  |  | ||||||
|  |         local function dawdle() while true do coroutine.yield() end end | ||||||
|         local function computer_with_rednet(id, fn, options) |         local function computer_with_rednet(id, fn, options) | ||||||
|             local computer = fake_computer.make_computer(id, function(env) |             local computer = fake_computer.make_computer(id, function(env) | ||||||
|                 local fns = { env.rednet.run } |                 local fns = { env.rednet.run } | ||||||
| @@ -105,6 +106,10 @@ describe("The rednet library", function() | |||||||
|                     end |                     end | ||||||
|                 end |                 end | ||||||
|  |  | ||||||
|  |                 if options and options.host then | ||||||
|  |                     env.rednet.host("some_protocol", "host_" .. id) | ||||||
|  |                 end | ||||||
|  |  | ||||||
|                 return parallel.waitForAny(table.unpack(fns)) |                 return parallel.waitForAny(table.unpack(fns)) | ||||||
|             end) |             end) | ||||||
|             local modem = fake_computer.add_modem(computer, "back") |             local modem = fake_computer.add_modem(computer, "back") | ||||||
| @@ -203,8 +208,8 @@ describe("The rednet library", function() | |||||||
|                 env.sleep(10) |                 env.sleep(10) | ||||||
|  |  | ||||||
|                 -- Ensure our pending message store is empty. Bit ugly to prod internals, but there's no other way. |                 -- Ensure our pending message store is empty. Bit ugly to prod internals, but there's no other way. | ||||||
|                 expect(debugx.getupvalue(rednet.run, "tReceivedMessages")):same({}) |                 expect(debugx.getupvalue(rednet.run, "received_messages")):same({}) | ||||||
|                 expect(debugx.getupvalue(rednet.run, "nClearTimer")):eq(nil) |                 expect(debugx.getupvalue(rednet.run, "prune_received_timer")):eq(nil) | ||||||
|             end, { open = true }) |             end, { open = true }) | ||||||
|  |  | ||||||
|             local computer_3, modem_3 = computer_with_rednet(3, nil, { open = true, rep = true }) |             local computer_3, modem_3 = computer_with_rednet(3, nil, { open = true, rep = true }) | ||||||
| @@ -222,5 +227,22 @@ describe("The rednet library", function() | |||||||
|             fake_computer.advance_all(computers, 10) |             fake_computer.advance_all(computers, 10) | ||||||
|             fake_computer.run_all(computers, { computer_1, computer_2 }) |             fake_computer.run_all(computers, { computer_1, computer_2 }) | ||||||
|         end) |         end) | ||||||
|  |  | ||||||
|  |         it("handles lookups between computers with massive IDs", function() | ||||||
|  |             local id_1, id_3 = 24283947, 93428798 | ||||||
|  |             local computer_1, modem_1 = computer_with_rednet(id_1, function(rednet) | ||||||
|  |                 local ids = { rednet.lookup("some_protocol") } | ||||||
|  |                 expect(ids):same { id_3 } | ||||||
|  |             end, { open = true }) | ||||||
|  |             local computer_2, modem_2 = computer_with_rednet(2, nil, { open = true, rep = true }) | ||||||
|  |             local computer_3, modem_3 = computer_with_rednet(id_3, dawdle, { open = true, host = true }) | ||||||
|  |             fake_computer.add_modem_edge(modem_1, modem_2) | ||||||
|  |             fake_computer.add_modem_edge(modem_2, modem_3) | ||||||
|  |  | ||||||
|  |             local computers = { computer_1, computer_2, computer_3 } | ||||||
|  |             fake_computer.run_all(computers, false) | ||||||
|  |             fake_computer.advance_all(computers, 3) | ||||||
|  |             fake_computer.run_all(computers, { computer_1 }) | ||||||
|  |         end) | ||||||
|     end) |     end) | ||||||
| end) | end) | ||||||
|   | |||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | describe("cc.audio.dfpwm", function() | ||||||
|  |     local dfpwm = require "cc.audio.dfpwm" | ||||||
|  |  | ||||||
|  |     describe("decode", function() | ||||||
|  |         it("decodes some test data", function() | ||||||
|  |             -- Look, I'm not proud of this. | ||||||
|  |             local input = "\43\225\33\44\30\240\171\23\253\201\46\186\68\189\74\160\188\16\94\169\251\87\11\240\19\92\85\185\126\5\172\64\17\250\85\245\255\169\244\1\85\200\33\176\82\104\163\17\126\23\91\226\37\224\117\184\198\11\180\19\148\86\191\246\255\188\231\10\210\85\124\202\15\232\43\162\117\63\220\15\250\88\87\230\173\106\41\13\228\143\246\190\119\169\143\68\201\40\149\62\20\72\3\160\114\169\254\39\152\30\20\42\84\24\47\64\43\61\221\95\191\42\61\42\206\4\247\81" | ||||||
|  |             local output = { 1, 2, 2, 2, 2, 2, 2, 1, 1, 1, 0, -1, -2, -2, -1, 0, 1, 0, -1, -3, -5, -5, -5, -7, -9, -11, -11, -9, -9, -9, -9, -10, -12, -12, -10, -8, -6, -6, -8, -10, -12, -14, -16, -18, -17, -15, -12, -9, -6, -3, -2, -2, -2, -2, -2, -2, 0, 3, 6, 7, 7, 7, 4, 1, 1, 1, 1, 3, 5, 7, 9, 12, 15, 15, 12, 12, 12, 9, 9, 11, 12, 12, 14, 16, 17, 17, 17, 14, 11, 11, 11, 10, 12, 14, 14, 13, 13, 10, 9, 9, 7, 5, 4, 4, 4, 4, 4, 6, 8, 10, 10, 10, 10, 10, 10, 10, 9, 8, 8, 8, 7, 6, 4, 2, 0, 0, 0, 0, 0, -1, -1, 0, 1, 3, 3, 3, 3, 2, 0, -2, -2, -2, -3, -5, -7, -7, -5, -3, -1, -1, -1, -1, -1, -1, -2, -2, -1, -1, -1, -1, 0, 1, 1, 1, 2, 3, 4, 5, 6, 7, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 9, 8, 7, 6, 4, 2, 0, 0, 2, 4, 6, 8, 10, 10, 8, 7, 7, 5, 3, 1, -1, 0, 2, 4, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 4, 5, 5, 5, 5, 5, 6, 7, 8, 9, 10, 9, 9, 9, 9, 9, 8, 7, 6, 5, 3, 1, 1, 3, 3, 3, 3, 3, 3, 2, 1, 0, -1, -3, -3, -3, -3, -2, -3, -4, -4, -3, -4, -5, -6, -6, -5, -5, -4, -3, -2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 18, 20, 20, 17, 16, 16, 15, 15, 15, 15, 13, 13, 13, 13, 14, 15, 16, 18, 18, 16, 14, 12, 10, 8, 5, 5, 5, 4, 4, 4, 4, 4, 4, 2, 0, -2, -2, -2, -4, -4, -2, 0, 0, -2, -4, -6, -6, -6, -8, -10, -12, -14, -16, -15, -13, -12, -11, -11, -11, -11, -13, -13, -13, -13, -13, -14, -16, -18, -18, -18, -18, -16, -16, -16, -14, -13, -14, -15, -15, -14, -14, -12, -11, -12, -13, -13, -12, -13, -14, -15, -15, -13, -11, -9, -7, -5, -5, -5, -3, -1, -1, -1, -1, -3, -5, -5, -3, -3, -3, -1, -1, -1, -1, -3, -3, -3, -4, -6, -6, -4, -2, 0, 0, 0, 0, -2, -2, -2, -3, -5, -7, -9, -11, -13, -13, -11, -9, -7, -6, -6, -6, -6, -4, -2, -2, -4, -6, -8, -7, -5, -3, -2, -2, -2, -2, 0, 0, -2, -4, -4, -2, 0, 2, 2, 1, 1, -1, -3, -5, -7, -10, -10, -10, -10, -8, -7, -7, -5, -3, -2, -4, -4, -4, -6, -8, -10, -12, -12, -12, -12, -12, -14, -13, -13, -13, -11, -11, -11, -11, -11, -11, -11, -9, -7, -5, -3, -1, -1, -1, -1, -1, 1, 1, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 22, 19, 18, 20, 22, 24, 23, 22, 24, 26, 28, 27, 24, 23, 25, 28, 28, 28, 27, 26, 26, 23, 20, 17, 14, 14, 14, 11, 11, 11, 11, 13, 15, 16, 16, 16, 15, 15, 14, 14, 12, 10, 9, 11, 13, 15, 17, 17, 14, 13, 13, 12, 12, 10, 9, 11, 13, 15, 17, 19, 19, 16, 13, 10, 7, 4, 1, 1, 2, 2, 4, 7, 10, 13, 13, 13, 12, 12, 12, 9, 6, 6, 6, 3, 0, 0, 0, 0, 2, 3, 3, 3, 3, 5, 7, 7, 7, 9, 11, 13, 15, 18, 18, 15, 12, 9, 8, 10, 13, 13, 13, 15, 18, 21, 24, 27, 27, 23, 19, 15, 11, 10, 9, 9, 12, 16, 19, 22, 23, 19, 14, 13, 16, 16, 15, 15, 14, 17, 20, 20, 19, 19, 18, 17, 14, 13, 15, 15, 12, 11, 13, 16, 19, 19, 18, 20, 20, 19, 18, 18, 17, 17, 16, 16, 16, 15, 17, 17, 16, 16, 13, 12, 12, 11, 11, 9, 9, 9, 9, 11, 11, 9, 7, 5, 3, 1, 1, 1, -1, -1, 1, 3, 5, 7, 9, 11, 12, 9, 6, 6, 6, 6, 8, 8, 7, 9, 11, 13, 13, 12, 14, 16, 18, 20, 20, 20, 22, 24, 26, 25, 25, 27, 29, 28, 27, 26, 23, 22, 22, 21, 21, 20, 22, 24, 26, 28, 27, 24, 21, 21, 21, 18, 17, 17, 14, 11, 11, 11, 10, 10, 7, 6, 6, 4, 3, 5, 5, 3, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, 0, -1, -1, 0, 0, 1, 2, 3, 4, 3, 1, -1, -3, -3, -3, -3, -2, -3, -4, -6, -8, -10, -10, -10, -12, -12, -12, -12, -10, -10, -11, -12, -14, -16, -18, -20, -22, -24, -26, -28, -27, -27, -26, -26, -25, -25, -27, -26, -24, -22, -22, -22, -22, -24, -24, -24, -24, -23, -23, -22, -22, -21, -20, -19, -17, -15, -13, -11, -9, -7, -7, -9, -9, -9, -11, -13, -15, -17, -16, -14, -13, -15, -14, -14, -14, -12, -10, -8, -7, -9, -11, -13, -15, -14, -14, -13, -13, -15, -17, -19, -18, -18, -17, -17, -16, -16, -18, -20, -22, -21, -21, -21, -21, -21, -20, -21, -22, -24, -24, -22, -22, -24, -26, -25, -23, -21, -19, -18, -17, -17, -19, -21, -23, -25, -27, -29, -31, -30, -29, -28, -26, -25, -24, -24, -23, -23, -25, -24, -24, -24, -22, -20, -18, -18, -20, -20, -20, -20, -18, -16, -16, -16, -14, -12, -10, -8, -6, -4, -4, -4, -4, -4, -2, 0, 2, 4, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 3, 3, 3, 3, 4, 5, 6, 5, 3, 1, 1, 1, 1, 1, 1, 1, 0, -1, -1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, -1, -2, -3, -4, -4, -2, 0, 0, 0, 1, 3, 5, 7, 7, 5, 3, 3, 3, 3, 3 } | ||||||
|  |  | ||||||
|  |             local decoded = dfpwm.decode(input) | ||||||
|  |             expect(#decoded):describe("The lengths match"):eq(#output) | ||||||
|  |             for i = 1, #decoded do expect(decoded[i]):describe("Item at #" .. i):eq(output[i]) end | ||||||
|  |         end) | ||||||
|  |     end) | ||||||
|  |  | ||||||
|  |     describe("encode", function() | ||||||
|  |         it("encodes some data", function() | ||||||
|  |             local input = { 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -6, -6, -6, -7, -7, -7, -7, -7, -7, -7, -7, -7, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -7, -7, -7, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -5, -5, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -7, -7, -7, -7, -7, -7, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -4, -4, -4, -4, -4, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -5, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -6, -5, -5, -5, -5, -5, -5, -5, -4, -4, -4, -4, -4, -4, -4, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3 } | ||||||
|  |             local output = { 87, 74, 42, 165, 164, 148, 84, 169, 170, 86, 173, 90, 173, 213, 90, 171, 214, 106, 213, 170, 106, 149, 42, 149, 74, 169, 74, 165, 74, 165, 170, 170, 106, 85, 107, 173, 106, 173, 173, 86, 181, 170, 42, 85, 149, 82, 41, 165, 82, 74, 41, 149, 170, 212, 170, 86, 181, 106, 173, 181, 170, 181, 90, 173, 170, 170, 170, 82, 165, 74, 149, 170, 82, 169, 82, 85, 85, 85, 173, 86, 181, 170, 213, 90, 173, 90, 85, 85, 149, 42, 165, 82, 170, 82, 74, 41, 85, 169, 170, 170, 106, 181, 90, 173, 86, 171, 106, 213, 106, 85, 85, 85, 85, 149, 42, 85, 170, 42, 149, 170, 170, 170, 170, 106, 181, 170, 86, 171 } | ||||||
|  |  | ||||||
|  |             local encoded = dfpwm.encode(input) | ||||||
|  |             expect(#encoded):describe("The lengths match"):eq(#output) | ||||||
|  |             for i = 1, #encoded do expect(encoded:byte(i)):describe("Item at #" .. i):eq(output[i] % 256) end | ||||||
|  |         end) | ||||||
|  |     end) | ||||||
|  | end) | ||||||
		Reference in New Issue
	
	Block a user
	 Jonathan Coates
					Jonathan Coates