alsactl 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 867a15dbe3727200b48ae90e35ad96c010bb19395b94f70629e2564979b2c76a
4
- data.tar.gz: b19233f1ee8a13d44fef4daaf58eab05077c50418299ff590966b8b35ae0aa18
3
+ metadata.gz: 482516915f981f7caead792ba342ce6c3cb641b581f30fcc1abb1aa4a738b345
4
+ data.tar.gz: d9901d56b145b182f74856571508c3a9c0c56373914ccd37e894f9ad5a3f63cf
5
5
  SHA512:
6
- metadata.gz: 6a5945c2b798ddee40b49fcff6fd35de7cc00063210464743eee7195022c4ac7c56c250df6fe3bc921f8cac9e381f005d856d956f53345cf44f379ab89d964fd
7
- data.tar.gz: b5832781e76557308f42a34253ad9978f5c90812cce6e96cb286793592c99d392f44ef407a43a780d93cbb308fc2fab776718529301ef78a2fad7d3b69264e53
6
+ metadata.gz: ebd6800c0e93d7bed7d50f6bf57735ac13929409a5e624e3001c99c3acaaa481c6604d1468a4603f8990691c27159ff2a1974707e313d6cde6993de97b4df423
7
+ data.tar.gz: 34070edd4b4bc9c6596cbb6eeaf269b7e5fc7a011f41f3d3bd987644fb5098f826d6920b4f6aee942a59f945ef63a46bc899295d206ffaf82bae5bcc093d2dc1
data/CHANGELOG.md CHANGED
@@ -1,4 +1,5 @@
1
1
  ### CHANGELOG -- (Ruby AlsaCtl) ###
2
2
  1) 0.1.0, unreleased: 2021-May-04 -- Changed the API to use Ruby as the complex implementation interface, because faster to get it done, less code, and way simpler than doing all the higher-order calculations (as well as string and symbol comparisons) in the C wrapper. [t. Edelweiss]
3
3
  1) 0.1.0, unreleased: 2021-May-04 -- In so doing above, turned entire C wrapper into an abstract class and module, so it can only be implemented by inheritance. Added such an implementation class, ``Mixers::DefMixer``. [t. Edelweiss]
4
- 1) 0.1.0, immediate pre-release: 2021-May-09 -- Realized that this gem was named too generic and changed the name to AlsaCtl, as there is a reasonable chance I might need to also handle PulseAudio or other APIs, at some point. Please also note the difference in the GitHub repo URL.
4
+ 1) 0.1.0, immediate pre-release: 2021-May-09 -- Realized that this gem was named too generic and changed the name to AlsaCtl, as there is a reasonable chance I might need to also handle PulseAudio or other APIs, at some point. Please also note the difference in the GitHub repo URL.
5
+ 1) 0.1.1, [CRITICAL PATCH] immediate pre-release: 2021-Jul-19 -- **CRITICAL PATCH (but does not change interface) -- please read** ``changes/0.1.1-critical.md`` **for information.** [t. Edelweiss]
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "rake/extensiontask"
2
+
3
+ # Build the AlsaCore extension
4
+ Rake::ExtensionTask.new "alsacore" do |ext|
5
+ ext.lib_dir = "ext/alsacore"
6
+ end # End build on AlsaCore
@@ -0,0 +1,20 @@
1
+ # SUMMARY #
2
+
3
+ **Date**
4
+ 2021-Jul-19
5
+
6
+ **Version**
7
+ 0.1.1
8
+
9
+ **Severity**
10
+ [CRITICAL UPDATE]
11
+
12
+ **Description**
13
+ No one appears to have noticed, perhaps due to the still highly-prevalent Ruby 2.7 on many systems, but this gem did NOT compile on later versions (2.8 BETA or anything 3.0+). This was due to a poorly-configured extconf, Rakefile, and gemspec. I have only been doing Ruby-C extensions for a few months, on and off, so I was not aware that Rake does not get invoked on execution of ``gem install``.
14
+
15
+ As such, the extension object file was not being placed into the correct location, even after a successful compilation, so the wrapper kept attempting to link _ONLY_ to the 2.7 version of the ``.so``. The key takeaway from this and why it would _still install_ was that the shared object, the one generated by my own local dev machine and older dev environment, was actually being placed into the proper location as a static dep, whereas the local, native, newly-compiled extension library was staying in an unlinked location (still under ``/ext/alsacore/``).
16
+
17
+ Thanks to a suggestion by @sampersand, on reviewing the ``openssl`` gem, I found out the correct way to link it was to leave the compiled object inside said directory, and to ``require 'alsacore.so'``, instead of using ``require_relative`` and the local path.
18
+
19
+ **Final Notes**
20
+ Please make sure to update this gem, if you have it installed, as the next Ruby version you upgrade to will NOT allow this to work, otherwise.
@@ -0,0 +1,372 @@
1
+ /* IN-PROCESS FILE - 2021-May-09 */
2
+ // Include Ruby
3
+ #include <ruby.h>
4
+
5
+ // Include other libraries
6
+ #include <stdio.h>
7
+ #include <math.h>
8
+
9
+ // ALSA includes
10
+ #include <alsa/asoundlib.h>
11
+ #include <alsa/version.h>
12
+
13
+ // Expansion to create a mixer pointer and then get the
14
+ // data from it
15
+ #define MIXLOADER base_mixer_obj* mixer; TypedData_Get_Struct(self, base_mixer_obj, &base_mixer_type, mixer);
16
+
17
+ // Don't try to handle anything on NULL mixer
18
+ #define CHK_MIX if (mixer->handle == NULL || \
19
+ mixer->element == NULL) { return Qnil; }
20
+
21
+ // Shorthand to allocate the channel arrays
22
+ #define MAX_CHANNELS (SND_MIXER_SCHN_LAST + 1)
23
+
24
+ // Shorthand early return on error
25
+ #define CRIT_CHECK if (err != 0) { return INT2NUM(err); }
26
+
27
+ /*
28
+ * Document-module: AlsaCore
29
+ *
30
+ * This module provides the concrete methods from which
31
+ * to extend custom mixers for volume control.
32
+ *
33
+ * A simple example of a default/Master mixer object is
34
+ * available and usable as +AlsaCtl::DefMixer+.
35
+ *
36
+ * = Example
37
+ *
38
+ * require 'AlsaCore'
39
+ *
40
+ * class MyMixer < AlsaCore::BaseMixer
41
+ */
42
+
43
+ // Basic type initiators used in this file
44
+ VALUE AlsaCore = Qnil;
45
+ VALUE BaseMixer = Qnil;
46
+ // End type initiators
47
+
48
+ /*
49
+ * Document-class: AlsaCore::BaseMixer
50
+ *
51
+ * The BaseMixer class provides purely protected methods
52
+ * that can be used from an inheriting class. +AlsaCtl::DefMixer+
53
+ * inherits this class and provides a simple interface without
54
+ * complex mixer management operations.
55
+ *
56
+ * BaseMixer provides methods for
57
+ * 1. Resource management
58
+ * 2. Connection
59
+ * 3. Disconnection
60
+ * 4. Setting ALL the volumes of a mixer
61
+ * 5. Per-channel volume setting of the mixer
62
+ * 6. Per-channel retrieval of mixer volume information
63
+ * 7. Enumeration of channels within the mixer
64
+ * 8. Key error handling for common operations
65
+ *
66
+ * The API can be found on https://www.alsa-project.org/alsa-doc/alsa-lib/
67
+ *
68
+ *---
69
+ * Document-method: pro_initialize
70
+ *
71
+ * (base_mixer_m_initialize)
72
+ *
73
+ * Protected method initializes the BaseMixer class.
74
+ *
75
+ * Instantiates the wrapped struct. Does not take any
76
+ * arguments.
77
+ *
78
+ * self.new -> BaseMixer
79
+ * self.initialize -> BaseMixer
80
+ *
81
+ *---
82
+ * Document-method: pro_connect
83
+ *
84
+ * (method_base_mixer_connect)
85
+ *
86
+ * Connects to a requested ALSA mixer.
87
+ *
88
+ * Takes 2 strings, a card name and element name (identifier).
89
+ * +DefMixer+ automatically passes the values "default" and "Master"
90
+ * to generate the default mixer element for the system.
91
+ *
92
+ * self.connect(char*, char*) -> BaseMixer | error
93
+ *
94
+ * pro_connect("default", "Master") -> new BaseMixer
95
+ *
96
+ *---
97
+ * Document-method: pro_close
98
+ *
99
+ * (method_base_mixer_disconnect)
100
+ *
101
+ * Disconnects the mixer and sets its struct member pointers
102
+ * to NULL.
103
+ *
104
+ * Takes no arguments.
105
+ *
106
+ * Returns true, if no error.
107
+ *
108
+ *---
109
+ * Document-method: pro_enum
110
+ *
111
+ * (method_base_mixer_enum_channels)
112
+ *
113
+ * Returns a hash of valid channels for the mixer, on success.
114
+ * On failure, returns +nil+.
115
+ *
116
+ * Takes no arguments.
117
+ *
118
+ *---
119
+ * Document-method: pro_cvolume_get
120
+ *
121
+ * (method_base_mixer_cvolume_get)
122
+ * Gets the volume settings for a selected channel on the mixer.
123
+ *
124
+ * Returns a hash with the volume, on success.
125
+ *
126
+ * On failure, returns either:
127
+ * +nil+, if the mixer is disconnected
128
+ * +false+, if the mixer doesn't have this channel
129
+ *
130
+ * Takes 1 argument, the channel number to check.
131
+ *
132
+ * pro_cvolume_get(1) -> {:name, :max, :min, :volume, :percent}
133
+ *
134
+ *---
135
+ * Document-method: pro_cvolume_set
136
+ *
137
+ * (method_base_mixer_cvolume_set)
138
+ *
139
+ * Sets the volume for a specific channel.
140
+ *
141
+ * Takes 2 arguments, the channel number (+int+) and the volume to use (+long+).
142
+ * When inheriting this class, you need to convert the values correctly,
143
+ * as 1 will not be 1%, but more like 0.1%, if your mixer uses 2^16 or something.
144
+ *
145
+ * Returns the output of method_base_mixer_cvolume_get for the same channel,
146
+ * on success.
147
+ *
148
+ * Returns +nil+, on failure.
149
+ *
150
+ * pro_cvolume_set(1, 32768) -> Sets channel 1 to 32768 (usually 50%)
151
+ *
152
+ *---
153
+ * Document-method: pro_volume_set
154
+ *
155
+ * (method_base_mixer_volume_set_all)
156
+ *
157
+ * Sets the volumes for the entire mixer, all channels. This method
158
+ * uses a separate call from ALSA, +snd_mixer_selem_set_playback_volume_all+,
159
+ * which does not require any channels.
160
+ *
161
+ * Takes 1 argument, the volume (+long+).
162
+ *
163
+ * pro_volume_set(32768) -> Sets all channels to 32768 (probably 50%)
164
+ */
165
+
166
+ // ---------------------------
167
+ // MIXER STRUCT BELOW
168
+ // ---------------------------
169
+
170
+ // Define the base mixer struct
171
+ typedef struct {
172
+ snd_mixer_t* handle; // Mixer handle
173
+ snd_mixer_elem_t* element; // Mixer element
174
+ } base_mixer_obj; // End Mixer struct def
175
+
176
+ // ---------------------------
177
+ // MIXER METHODS BELOW
178
+ // ---------------------------
179
+
180
+ // Free method for base mixer
181
+ void base_mixer_free(void* data) {
182
+ free(data);
183
+ } // End mixer free
184
+
185
+ // Size of mixer
186
+ size_t base_mixer_size(const void* data) {
187
+ return sizeof(data);
188
+ } // End size of mixer
189
+
190
+ // Define the Ruby struct type to encapsulate the
191
+ // master sound mixer
192
+ static const rb_data_type_t base_mixer_type = {
193
+ .wrap_struct_name = "base_mixer_obj",
194
+ .function = {
195
+ .dmark = NULL,
196
+ .dfree = base_mixer_free,
197
+ .dsize = base_mixer_size,
198
+ },
199
+ .data = NULL,
200
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
201
+ }; // End base mixer type definition
202
+
203
+ // Define allocation of mixer handle object
204
+ VALUE base_mixer_alloc(VALUE self) {
205
+ // Allocate the size for a mixer handle and its opened element
206
+ base_mixer_obj* mixer = malloc(sizeof(base_mixer_obj));
207
+ return TypedData_Wrap_Struct(self, &base_mixer_type, mixer); // Wrap it up!
208
+ } // End mixer handle allocation
209
+
210
+ // Initialize / constructor
211
+ VALUE base_mixer_m_initialize(VALUE self) {
212
+ // The below are set on connect() and voided on disconnect()
213
+ snd_mixer_t *handle = NULL; // NULL handle
214
+ snd_mixer_elem_t* elem = NULL; // NULL element
215
+
216
+ // Create a Base Mixer struct pointer to access
217
+ // from the class
218
+ base_mixer_obj mm = {
219
+ .handle = handle,
220
+ .element = elem,
221
+ };
222
+
223
+ MIXLOADER; // Shorthand load the mixer
224
+ *mixer = mm; // Bind the struct to the class object's pointer
225
+ return self;
226
+ } // End mix handle initializer
227
+
228
+ // Connect with the ALSA server
229
+ VALUE method_base_mixer_connect(VALUE self, VALUE card_name, VALUE elem_name) {
230
+ int err = 0; // Placeholder for errors
231
+ snd_mixer_selem_id_t *sid; // Create mixer element ID
232
+
233
+ // Default mixer will be default/Master
234
+ const char *card = StringValueCStr(card_name); // ex: "default"
235
+ const char *selem_name = StringValueCStr(elem_name); // ex: "Master"
236
+
237
+ MIXLOADER; // Shorthand load the mixer
238
+ // If the mixer is already connected, raise an error
239
+ if (mixer->handle != NULL || mixer->element != NULL) {
240
+ rb_raise(rb_eRuntimeError,
241
+ "\n::ERROR: Already connected to mixer! Disconnect first!::\n");
242
+ return Qnil;
243
+ }
244
+
245
+ // The below will use CRIT_CHECK, as these errors are
246
+ // non-recoverable, and continuation in other methods
247
+ // will result in seg faults.
248
+ //
249
+ // Open and load a mixer object
250
+ err = snd_mixer_open(&mixer->handle, 0);
251
+ CRIT_CHECK;
252
+ err = snd_mixer_attach(mixer->handle, card);
253
+ CRIT_CHECK;
254
+ err = snd_mixer_selem_register(mixer->handle, NULL, NULL);
255
+ CRIT_CHECK;
256
+ err = snd_mixer_load(mixer->handle);
257
+ CRIT_CHECK;
258
+
259
+ // Attach a mixer element to the handle
260
+ snd_mixer_selem_id_alloca(&sid);
261
+ snd_mixer_selem_id_set_index(sid, 0);
262
+ snd_mixer_selem_id_set_name(sid, selem_name);
263
+
264
+ // Instantiate the mixer element attached to the handle
265
+ mixer->element = snd_mixer_find_selem(mixer->handle, sid);
266
+
267
+ // Checker for valid channels moved to Ruby side
268
+ return self;
269
+ } // End connect method
270
+
271
+ // Disconnect from the ALSA server
272
+ VALUE method_base_mixer_disconnect(VALUE self) {
273
+ MIXLOADER; // Shorthand load the mixer
274
+ if (mixer->handle != NULL) {
275
+ int err = snd_mixer_close(mixer->handle); // Close the mixer
276
+ CRIT_CHECK;
277
+ mixer->element = NULL;
278
+ mixer->handle = NULL;
279
+ } // If it's already NULL, there is nothing to close,
280
+ // So returning TRUE is still saying the mixer is disconnected
281
+ return Qtrue;
282
+ } // End disconnect method
283
+
284
+ // Enumerate the channels
285
+ VALUE method_base_mixer_enum_channels(VALUE self) {
286
+ MIXLOADER; // Shorthand load the mixer
287
+ CHK_MIX; // Check if mixer is null or not, before attempting operations
288
+ VALUE channels = rb_hash_new(); // Hash of channels
289
+ for (int i = 0; i < (MAX_CHANNELS); i++) {
290
+ // Check if the channel exists; if so, give it a true key.
291
+ // Ignore any channels not present -- a bad index returns NIL, anyway
292
+ if (snd_mixer_selem_has_playback_channel(mixer->element, i)) {
293
+ rb_hash_aset(channels, INT2NUM(i), Qtrue);
294
+ }
295
+ }
296
+ return channels;
297
+ } // End channel enumerator
298
+
299
+ // Get the volume for a single channel
300
+ VALUE method_base_mixer_cvolume_get(VALUE self, VALUE cid) {
301
+ long min, max, current; // Volume values
302
+ MIXLOADER; // Shorthand load the mixer
303
+ CHK_MIX; // Check if mixer is null or not, before attempting operations
304
+ snd_mixer_handle_events(mixer->handle); // Refresh events on handle
305
+ snd_mixer_selem_get_playback_volume_range(mixer->element, &min, &max); // Get volume range
306
+
307
+ // Get the volume for the channel, but only if cid (channel id) is valid
308
+ if (RB_TYPE_P(cid, T_FIXNUM)){
309
+ int cid_fix = NUM2INT(cid);
310
+ if (snd_mixer_selem_has_playback_channel(mixer->element, cid_fix)) {
311
+ snd_mixer_selem_get_playback_volume(mixer->element, cid_fix, &current);
312
+ VALUE channel = rb_hash_new(); // Channel has volume and percent
313
+ // Name of channel, if available
314
+ rb_hash_aset(channel, rb_id2sym(rb_intern("name")),
315
+ rb_str_new2(snd_mixer_selem_channel_name(cid_fix)));
316
+ // Minimum and maximum values
317
+ rb_hash_aset(channel, rb_id2sym(rb_intern("max")), LONG2NUM(max));
318
+ rb_hash_aset(channel, rb_id2sym(rb_intern("min")), LONG2NUM(min));
319
+ // Current channel volume
320
+ rb_hash_aset(channel, rb_id2sym(rb_intern("volume")), LONG2NUM(current));
321
+ // Get the percentage of the total (round to 0 decimals,
322
+ // for reasons logical to audio use)
323
+ int perc = (int) round(( ((double) current / (double) max) ) * 100);
324
+ rb_hash_aset(channel, rb_id2sym(rb_intern("percent")), INT2NUM(perc));
325
+ return channel;
326
+ } else { return Qfalse; } // False if channel doesn't exist
327
+ } else { return Qnil; } // Nil if channel id is not a number
328
+ } // End getter for single-channel volume
329
+
330
+ // Set volume on a single channel
331
+ VALUE method_base_mixer_cvolume_set(VALUE self, VALUE channel, VALUE volume) {
332
+ MIXLOADER; // Shorthand load the mixer
333
+ CHK_MIX; // Check if mixer is null or not, before attempting operations
334
+ int ch = NUM2INT(channel); // Cast the channel to an INT
335
+ // Set the volume for one channel
336
+ snd_mixer_selem_set_playback_volume(mixer->element, ch, NUM2LONG(volume));
337
+ return method_base_mixer_cvolume_get(self, channel);
338
+ } // End set channel volume to specified
339
+
340
+ // Set ALL the channels' volumes on the mixer
341
+ VALUE method_base_mixer_volume_set_all(VALUE self, VALUE volume) {
342
+ MIXLOADER; // Shorthand load the mixer
343
+ CHK_MIX; // Check if mixer is null or not, before attempting operations
344
+ // Set the volume for all channels
345
+ int err = snd_mixer_selem_set_playback_volume_all(mixer->element, NUM2LONG(volume));
346
+ return (err ? Qfalse : Qtrue);
347
+ } // End set volume to specified
348
+
349
+ // Initialize the AlsaCore module
350
+ void Init_alsacore() {
351
+ AlsaCore = rb_define_module("AlsaCore"); // Module
352
+
353
+ // Mixer class
354
+ BaseMixer = rb_define_class_under(AlsaCore, "BaseMixer", rb_cData);
355
+ rb_define_alloc_func(BaseMixer, base_mixer_alloc);
356
+ rb_define_protected_method(BaseMixer, "pro_initialize", base_mixer_m_initialize, 0);
357
+
358
+ // Connection methods
359
+ rb_define_protected_method(BaseMixer, "pro_connect", method_base_mixer_connect, 2);
360
+ rb_define_protected_method(BaseMixer, "pro_disconnect", method_base_mixer_disconnect, 0);
361
+ rb_define_protected_method(BaseMixer, "pro_close", method_base_mixer_disconnect, 0);
362
+
363
+ // Enumerate the channels
364
+ rb_define_protected_method(BaseMixer, "pro_enum", method_base_mixer_enum_channels, 0);
365
+
366
+ // Get a channel's volume
367
+ rb_define_protected_method(BaseMixer, "pro_cvolume_get", method_base_mixer_cvolume_get, 1);
368
+
369
+ // Setters
370
+ rb_define_protected_method(BaseMixer, "pro_volume_set", method_base_mixer_volume_set_all, 1);
371
+ rb_define_protected_method(BaseMixer, "pro_cvolume_set", method_base_mixer_cvolume_set, 2);
372
+ } // End init
@@ -1,10 +1,10 @@
1
1
  # ext/alsacore/extconf.rb
2
2
  require 'mkmf'
3
3
 
4
- # Build MixCore
4
+ # Build AlsaCore
5
5
  $LFLAGS = '-lasound'
6
6
  have_library("asound")
7
7
  have_header("alsa/asoundlib.h")
8
8
  have_func("snd_mixer_open")
9
9
  have_func("snd_mixer_close")
10
- create_makefile("alsacore/alsacore")
10
+ create_makefile("alsacore")
@@ -1,6 +1,6 @@
1
1
  # lib/alsactl/mixer.rb
2
2
 
3
- require_relative "../alsacore/alsacore"
3
+ require "alsacore.so"
4
4
 
5
5
  ##
6
6
  # This extends the regular AlsaCore module and
@@ -6,5 +6,5 @@ module AlsaCtl
6
6
  ##
7
7
  # Current version -- I'm only documenting this, so
8
8
  # +rdoc+ doesn't complain.
9
- VERSION = "0.1.0"
9
+ VERSION = "0.1.1"
10
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alsactl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Edelweiss
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-09 00:00:00.000000000 Z
11
+ date: 2021-07-20 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  ALSA wrapper in Ruby, utilizing a purely-abstract C-Ruby API backend
@@ -21,12 +21,15 @@ extra_rdoc_files:
21
21
  - README.md
22
22
  - CHANGELOG.md
23
23
  - LICENSE.txt
24
+ - changes/0.1.1-critical.md
24
25
  files:
25
26
  - CHANGELOG.md
26
27
  - LICENSE.txt
27
28
  - README.md
29
+ - Rakefile
30
+ - changes/0.1.1-critical.md
31
+ - ext/alsacore/alsacore.c
28
32
  - ext/alsacore/extconf.rb
29
- - lib/alsacore/alsacore.so
30
33
  - lib/alsactl.rb
31
34
  - lib/alsactl/mixers.rb
32
35
  - lib/alsactl/version.rb
Binary file